fix(session-projection-cache): keep upgraded caches readable and boots safe across domain versions
The session_projcache domain declares compatibleVersions: [3, 4] and invalidRecords: 'backup-and-skip'. The two lineage identity fields become optional — records admitted from older versions predate them, and the single reader (identityMatches) interprets absence as the unseeded lineage: exact for unseeded sessions, while a seeded caller fails the match and refolds cold, so the lineage binding keeps its protection. Upgraded homes therefore boot and serve their cached listing titles immediately, including homes whose new tree already holds current-stamped documents without lineage fields, and a record failing validation anyway is backed up and skipped instead of refusing the plugin tree. tests/fixtures/ archives the real on-disk media of every shipped generation (v3 whole-unit file, v4 and v5 per-record documents, and the lineage-less current-stamped shape); fixtures.spec.ts proves each recovers through the real storage stack, rewrites to the current format on the next live write, and that a hopeless record is salvaged without costing the boot.
This commit is contained in:
parent
fcd109d29a
commit
49df707c86
8 changed files with 1029 additions and 6 deletions
|
|
@ -367,12 +367,18 @@ function identityOf(
|
|||
}
|
||||
}
|
||||
|
||||
/** Whether a stored record's bound identity names the caller's lifecycle. */
|
||||
/**
|
||||
* Whether a stored record's bound identity names the caller's lifecycle.
|
||||
* Absent lineage fields (records admitted via `compatibleVersions` predate
|
||||
* them) read as the unseeded lineage: exact for an unseeded caller, and a
|
||||
* seeded caller's expectation then fails the match, discarding the record to
|
||||
* a cold rebuild.
|
||||
*/
|
||||
function identityMatches(stored: CheckpointIdentity, expected: CheckpointIdentity): boolean {
|
||||
return stored.createdAt === expected.createdAt
|
||||
&& stored.cwd === expected.cwd
|
||||
&& stored.isSeeded === expected.isSeeded
|
||||
&& stored.inheritedEventCount === expected.inheritedEventCount
|
||||
&& (stored.isSeeded ?? false) === expected.isSeeded
|
||||
&& (stored.inheritedEventCount ?? 0) === expected.inheritedEventCount
|
||||
}
|
||||
|
||||
export default SessionProjectionCache
|
||||
|
|
|
|||
|
|
@ -38,12 +38,19 @@ export const checkpointRow = z.object({
|
|||
* old record pass every watermark check and seed state folded from an
|
||||
* unrelated log. Reads validate this against the live header (listing) or
|
||||
* the stored header (cold read) before accepting any record.
|
||||
*
|
||||
* The lineage fields are optional because records admitted through
|
||||
* `compatibleVersions` predate them. The reader (`identityMatches`)
|
||||
* interprets their absence as the unseeded lineage — exact for an unseeded
|
||||
* session, while a seeded expectation fails the match and the record is
|
||||
* discarded to a cold rebuild. Current-version writes always store both
|
||||
* fields.
|
||||
*/
|
||||
export const checkpointIdentity = z.object({
|
||||
createdAt: z.number().int().nonnegative(),
|
||||
cwd: z.string().optional(),
|
||||
isSeeded: z.boolean(),
|
||||
inheritedEventCount: z.number().int().nonnegative().transform(SessionLogOffset),
|
||||
isSeeded: z.boolean().optional(),
|
||||
inheritedEventCount: z.number().int().nonnegative().transform(SessionLogOffset).optional(),
|
||||
})
|
||||
|
||||
/** The identity fields a record is bound to, inferred from {@link checkpointIdentity}. */
|
||||
|
|
@ -68,11 +75,27 @@ export type CheckpointRecord = z.infer<typeof checkpointRecord>
|
|||
* bumps per session: after a bump, a stale session document is discarded on
|
||||
* open (cache semantics — a stale or unreadable cache costs a longer tail
|
||||
* replay, never a wrong value) while the rest of the domain stays usable,
|
||||
* instead of rejecting the whole medium.
|
||||
* instead of rejecting the whole medium. The `compatibleVersions` entries
|
||||
* are declared because those records differ from the current version only
|
||||
* by the absent optional lineage fields, so upgraded homes keep serving
|
||||
* their cached listing projections instead of dropping every title until
|
||||
* each session is reopened; the per-record version map lives in the
|
||||
* read-compat Agent Note
|
||||
* (.agents/notes/proposed/architecture/2026-09-02-projcache-cross-version-read-compat.md).
|
||||
* The per-row `ver` guard and the identity match still discard anything the
|
||||
* current fold semantics cannot vouch for.
|
||||
*
|
||||
* `invalidRecords: 'backup-and-skip'`: a stored record that fails the schema
|
||||
* anyway is disposable derived data, so it must never cost the boot — the
|
||||
* domain layer moves the document aside as `<key>.json.bak.<stamp>`, logs
|
||||
* the concrete validation failure, and serves the session as uncached (a
|
||||
* cold read rebuilds and rewrites it).
|
||||
*/
|
||||
export const projectionCacheDomainSpec = defineDomain({
|
||||
name: 'session_projcache',
|
||||
version: 5,
|
||||
compatibleVersions: [3, 4],
|
||||
invalidRecords: 'backup-and-skip',
|
||||
layout: 'per-record',
|
||||
tables: { sessions: domainTable<SessionId, CheckpointRecord>(checkpointRecord) },
|
||||
})
|
||||
|
|
|
|||
|
|
@ -42,9 +42,11 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
|
|||
'cache-test/marks2': Map<string, string>
|
||||
'cache-test/count': number
|
||||
'cache-test/secret': string
|
||||
'cache-test/marks3': MarksState
|
||||
}
|
||||
interface SessionProjectionMap {
|
||||
'cache-test/marks': { marks: string[] }
|
||||
'cache-test/marks3': { marks: string[] }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -71,6 +73,18 @@ const marksUnit = (stateVersion = 1) => ({
|
|||
stateVersion,
|
||||
}) satisfies ProjectionDefinition<'cache-test/marks', MarksState>
|
||||
|
||||
const marks3Unit = {
|
||||
key: 'cache-test/marks3',
|
||||
stateSchema: z.object({ marks: z.array(z.string()) }).nullable(),
|
||||
init: () => null,
|
||||
apply: state => state,
|
||||
wire: {
|
||||
viewSchema: z.object({ marks: z.array(z.string()) }),
|
||||
view: state => state ?? { marks: [] },
|
||||
},
|
||||
stateVersion: 1,
|
||||
} satisfies ProjectionDefinition<'cache-test/marks3', MarksState>
|
||||
|
||||
const secretUnit = {
|
||||
key: 'cache-test/secret',
|
||||
stateSchema: z.string(),
|
||||
|
|
@ -376,6 +390,26 @@ describe('SessionProjectionCache listing read', () => {
|
|||
.toBeUndefined()
|
||||
})
|
||||
|
||||
it('carries ONE cut across multiple served rows: the lowest watermark wins', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-'))
|
||||
roots.push(root)
|
||||
// Equal watermarks: whichever row is visited second cannot lower the cut,
|
||||
// so the one-cut fold sees both a lowering and a non-lowering row in
|
||||
// every iteration order.
|
||||
await seedRecord(root, 'multi-row', {
|
||||
'cache-test/marks': { ver: 1, seq: SessionSeq(4), val: { marks: ['a'] } },
|
||||
'cache-test/marks3': { ver: 1, seq: SessionSeq(4), val: { marks: ['b'] } },
|
||||
})
|
||||
const { ctx, cache } = await harness({ root })
|
||||
ctx.sessionProjections.register(marks3Unit)
|
||||
const block = cache.cachedSnapshot(headerOf(SessionId('multi-row')), SessionLogOffset(0))
|
||||
expect(block?.values).toEqual({
|
||||
'cache-test/marks': { marks: ['a'] },
|
||||
'cache-test/marks3': { marks: ['b'] },
|
||||
})
|
||||
expect(block?.asOfSeq).toBe(4)
|
||||
})
|
||||
|
||||
it('returns undefined when the stored record is version-mismatched', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-'))
|
||||
roots.push(root)
|
||||
|
|
@ -394,6 +428,30 @@ describe('SessionProjectionCache listing read', () => {
|
|||
.toBeUndefined()
|
||||
})
|
||||
|
||||
it('serves a pre-lineage record (accepted old version) to an unseeded caller only', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-'))
|
||||
roots.push(root)
|
||||
// A document stamped with an accepted older version whose identity
|
||||
// predates the lineage fields: absent lineage reads as unseeded.
|
||||
const path = recordPath(root, SessionId('pre-lineage'))
|
||||
await mkdir(dirname(path), { recursive: true })
|
||||
await writeFile(path, JSON.stringify({
|
||||
version: 4,
|
||||
record: {
|
||||
identity: { createdAt: 0 },
|
||||
rows: { 'cache-test/marks': { ver: 1, seq: 4, val: { marks: ['kept'] } } },
|
||||
},
|
||||
}))
|
||||
const { cache } = await harness({ root })
|
||||
const id = SessionId('pre-lineage')
|
||||
// Unseeded caller: the absent lineage is exactly its identity — served.
|
||||
expect(cache.cachedSnapshot(headerOf(id), SessionLogOffset(0)))
|
||||
.toEqual({ asOfSeq: 4, values: { 'cache-test/marks': { marks: ['kept'] } } })
|
||||
// Seeded caller: the lineage-less record cannot vouch for the cut — refused.
|
||||
expect(cache.cachedSnapshot({ ...headerOf(id), isSeeded: true }, SessionLogOffset(2)))
|
||||
.toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined when every stored row is version-mismatched', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-'))
|
||||
roots.push(root)
|
||||
|
|
|
|||
240
packages/session/session-projection-cache/tests/fixtures.spec.ts
Normal file
240
packages/session/session-projection-cache/tests/fixtures.spec.ts
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
/**
|
||||
* Cross-version recovery over archived on-disk artifacts. `fixtures/` holds
|
||||
* real `session_projcache` media produced by released builds through their
|
||||
* web apps (see scripts/releasefix): the v3 whole-unit file, a v4 per-record
|
||||
* document, a current v5 document, and the v5-stamped lineage-less document
|
||||
* that the formerly unguarded legacy bootstrap wrote over v3 records. Each
|
||||
* must recover through the real storage stack — the domain opens and the
|
||||
* listing read serves the archived title — and a record that fails schema
|
||||
* validation anyway is backed up and skipped instead of failing the boot.
|
||||
*/
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { z } from 'zod'
|
||||
import SessionStore, { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionHeader } from '@deepseek-ai/dsh-session'
|
||||
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
|
||||
import Storage from '@deepseek-ai/dsh-storage'
|
||||
import {
|
||||
apply as storageJsonApply, Config as storageJsonConfig, inject as storageJsonInject, name as storageJsonName,
|
||||
} from '@deepseek-ai/dsh-storage-json'
|
||||
import {
|
||||
apply as storageDomainApply, Config as storageDomainConfig, inject as storageDomainInject, name as storageDomainName,
|
||||
} from '@deepseek-ai/dsh-storage-domain'
|
||||
import SessionProjectionCache from '../src/index.ts'
|
||||
import { projectionCacheDomainSpec } from '../src/spec.ts'
|
||||
|
||||
// Declarations must match the shipped title unit's exactly (the repo-wide
|
||||
// compile face sees both).
|
||||
declare module '@deepseek-ai/dsh-session-projection/types' {
|
||||
interface SessionProjectionStateMap {
|
||||
title: string | null
|
||||
}
|
||||
interface SessionProjectionMap {
|
||||
title: string | null
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-session/types' {
|
||||
interface SessionEventMap {
|
||||
'fixtures-test/set-title': { title: string }
|
||||
}
|
||||
|
||||
interface OutOfBandSessionEventMap {
|
||||
'fixtures-test/set-title': true
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors the shipped title unit's storage face: stateVersion 1, bare-string
|
||||
// state (the fixture rows carry exactly this shape in every archived
|
||||
// version), folding a test event so the rewrite path has fresh data.
|
||||
const titleUnit = {
|
||||
key: 'title',
|
||||
stateSchema: z.string().nullable(),
|
||||
init: () => null,
|
||||
apply: (state, event) => (event.type === 'fixtures-test/set-title' ? event.data.title : state),
|
||||
wire: { viewSchema: z.string().nullable(), view: state => state },
|
||||
stateVersion: 1,
|
||||
} satisfies ProjectionDefinition<'title', string | null>
|
||||
|
||||
const FIXTURES = fileURLToPath(new URL('./fixtures/', import.meta.url))
|
||||
|
||||
/** One archived per-record document (`{version, record}`). */
|
||||
interface FixtureDoc {
|
||||
version: number
|
||||
record: {
|
||||
identity: { createdAt: number; cwd?: string }
|
||||
rows: Record<string, { ver: number; seq: number; val: unknown }>
|
||||
}
|
||||
}
|
||||
|
||||
async function fixtureJson<T>(name: string): Promise<T> {
|
||||
return JSON.parse(await readFile(join(FIXTURES, name), 'utf8')) as T
|
||||
}
|
||||
|
||||
/** Header for the session a fixture record is bound to (identity witness). */
|
||||
function headerFor(id: SessionId, identity: FixtureDoc['record']['identity']): SessionHeader {
|
||||
return {
|
||||
version: 0,
|
||||
id,
|
||||
createdAt: identity.createdAt,
|
||||
isSeeded: false,
|
||||
...identity.cwd === undefined ? {} : { cwd: identity.cwd },
|
||||
}
|
||||
}
|
||||
|
||||
const contexts: Context[] = []
|
||||
const roots: string[] = []
|
||||
|
||||
async function harness(root: string) {
|
||||
roots.push(root)
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
await ctx.plugin(Storage)
|
||||
await ctx.plugin({ name: storageJsonName, inject: storageJsonInject, apply: storageJsonApply, Config: storageJsonConfig }, { root })
|
||||
await ctx.plugin({ name: storageDomainName, inject: storageDomainInject, apply: storageDomainApply, Config: storageDomainConfig }, { backend: 'json' })
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionProjectionRegistry)
|
||||
ctx.sessionProjections.register(titleUnit)
|
||||
await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 })
|
||||
return { ctx, cache: ctx.sessionProjectionCache }
|
||||
}
|
||||
|
||||
/** Lay one per-record fixture document into a fresh backend root. */
|
||||
async function placeDoc(root: string, id: string, name: string): Promise<FixtureDoc> {
|
||||
const path = join(root, projectionCacheDomainSpec.name, 'sessions', `${id}.json`)
|
||||
await mkdir(dirname(path), { recursive: true })
|
||||
await cp(join(FIXTURES, name), path)
|
||||
return fixtureJson<FixtureDoc>(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Drive a live write over a recovered session id and assert the archived
|
||||
* document is replaced by a current-version one: v5 stamp, lineage present,
|
||||
* and the freshly folded title — the write path never keeps the old format.
|
||||
*/
|
||||
async function assertRewrite(ctx: Context, root: string, id: SessionId): Promise<void> {
|
||||
const session = ctx.sessions.create(id)
|
||||
session.append('fixtures-test/set-title', { title: '重写标题' })
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
const path = join(root, projectionCacheDomainSpec.name, 'sessions', `${id}.json`)
|
||||
await vi.waitFor(async () => {
|
||||
const doc = JSON.parse(await readFile(path, 'utf8')) as FixtureDoc
|
||||
expect(doc.version).toBe(projectionCacheDomainSpec.version)
|
||||
expect(doc.record.identity).toMatchObject({ isSeeded: false, inheritedEventCount: 0 })
|
||||
expect(doc.record.rows['title']?.val).toBe('重写标题')
|
||||
}, { timeout: 5_000 })
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
|
||||
await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 })))
|
||||
})
|
||||
|
||||
describe('archived version recovery', () => {
|
||||
it('recovers the v3 whole-unit archive through the legacy bootstrap', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-fx-'))
|
||||
await cp(join(FIXTURES, 'v3-single-unit.json'), join(root, `${projectionCacheDomainSpec.name}.json`))
|
||||
type SingleUnit = {
|
||||
unit: { version: number }
|
||||
tables: { sessions: Record<string, FixtureDoc['record']> }
|
||||
}
|
||||
const archive = await fixtureJson<SingleUnit>('v3-single-unit.json')
|
||||
expect(archive.unit.version).toBe(3) // the fixture IS the old format
|
||||
const [sid, record] = Object.entries(archive.tables.sessions)[0]!
|
||||
|
||||
const { ctx, cache } = await harness(root)
|
||||
const snapshot = cache.cachedSnapshot(headerFor(SessionId(sid), record.identity), SessionLogOffset(0), ['title'])
|
||||
expect(snapshot?.values.title).toBe(record.rows['title']!.val)
|
||||
|
||||
// The one-time bootstrap materialized a current-version document.
|
||||
const migrated = JSON.parse(
|
||||
await readFile(join(root, projectionCacheDomainSpec.name, 'sessions', `${sid}.json`), 'utf8'),
|
||||
) as { version: number }
|
||||
expect(migrated.version).toBe(projectionCacheDomainSpec.version)
|
||||
|
||||
await assertRewrite(ctx, root, SessionId(sid))
|
||||
})
|
||||
|
||||
for (const [fixture, storedVersion] of [
|
||||
['v4-session-doc.json', 4],
|
||||
['v5-session-doc.json', 5],
|
||||
['v5-lineageless-doc.json', 5],
|
||||
] as const) {
|
||||
it(`serves the archived title from ${fixture}, then rewrites it current`, async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-fx-'))
|
||||
const id = SessionId('fixture-session')
|
||||
const doc = await placeDoc(root, id, fixture)
|
||||
expect(doc.version).toBe(storedVersion)
|
||||
|
||||
const { ctx, cache } = await harness(root)
|
||||
const snapshot = cache.cachedSnapshot(headerFor(id, doc.record.identity), SessionLogOffset(0), ['title'])
|
||||
expect(snapshot?.values.title).toBe(doc.record.rows['title']!.val)
|
||||
|
||||
await assertRewrite(ctx, root, id)
|
||||
})
|
||||
}
|
||||
|
||||
it('refuses a lineage-less archive for a seeded caller (identity mismatch, cold rebuild)', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-fx-'))
|
||||
const id = SessionId('fixture-seeded')
|
||||
const doc = await placeDoc(root, id, 'v5-lineageless-doc.json')
|
||||
|
||||
const { cache } = await harness(root)
|
||||
const seeded = { ...headerFor(id, doc.record.identity), isSeeded: true }
|
||||
expect(cache.cachedSnapshot(seeded, SessionLogOffset(2), ['title'])).toBeUndefined()
|
||||
})
|
||||
|
||||
it('backs up and skips a record that fails schema validation instead of failing the boot', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-fx-'))
|
||||
roots.push(root)
|
||||
const sessionsDir = join(root, projectionCacheDomainSpec.name, 'sessions')
|
||||
await mkdir(sessionsDir, { recursive: true })
|
||||
// Current-version stamp, hopeless record content: no compat rung can save it.
|
||||
await writeFile(join(sessionsDir, 'broken.json'), JSON.stringify({
|
||||
version: projectionCacheDomainSpec.version,
|
||||
record: { identity: { createdAt: 'not-a-number' }, rows: 'not-an-object' },
|
||||
}))
|
||||
const good = await placeDoc(root, SessionId('survivor'), 'v5-session-doc.json')
|
||||
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
await ctx.plugin(Storage)
|
||||
await ctx.plugin({ name: storageJsonName, inject: storageJsonInject, apply: storageJsonApply, Config: storageJsonConfig }, { root })
|
||||
await ctx.plugin({ name: storageDomainName, inject: storageDomainInject, apply: storageDomainApply, Config: storageDomainConfig }, { backend: 'json' })
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionProjectionRegistry)
|
||||
ctx.sessionProjections.register(titleUnit)
|
||||
const error = vi.spyOn(ctx.logger, 'error').mockImplementation(() => {})
|
||||
// The boot survives the broken record — this line rejecting IS the fixed bug.
|
||||
await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 })
|
||||
|
||||
// Concrete console diagnostics: which record, where it went, and why.
|
||||
expect(error).toHaveBeenCalledWith(expect.stringContaining("record 'broken'"))
|
||||
expect(error).toHaveBeenCalledWith(expect.stringContaining('.json.bak.'))
|
||||
|
||||
// The document was moved aside as <key>.json.bak.<YYYYMMDDHHmm>, bytes intact.
|
||||
const entries = await readdir(sessionsDir)
|
||||
expect(entries).not.toContain('broken.json')
|
||||
const backup = entries.find(name => /^broken\.json\.bak\.\d{12}$/.test(name))
|
||||
expect(backup).toBeDefined()
|
||||
expect(JSON.parse(await readFile(join(sessionsDir, backup!), 'utf8')))
|
||||
.toMatchObject({ record: { rows: 'not-an-object' } })
|
||||
|
||||
// The broken record reads as absent; its neighbors still serve.
|
||||
const cache = ctx.sessionProjectionCache
|
||||
expect(cache.cachedSnapshot(headerFor(SessionId('broken'), { createdAt: 0 }), SessionLogOffset(0)))
|
||||
.toBeUndefined()
|
||||
expect(cache.cachedSnapshot(
|
||||
headerFor(SessionId('survivor'), good.record.identity),
|
||||
SessionLogOffset(0),
|
||||
['title'],
|
||||
)?.values.title).toBe(good.record.rows['title']!.val)
|
||||
})
|
||||
})
|
||||
136
packages/session/session-projection-cache/tests/fixtures/v3-single-unit.json
vendored
Normal file
136
packages/session/session-projection-cache/tests/fixtures/v3-single-unit.json
vendored
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
{
|
||||
"unit": {
|
||||
"name": "session_projcache",
|
||||
"version": 3
|
||||
},
|
||||
"global": null,
|
||||
"tables": {
|
||||
"sessions": {
|
||||
"session-1374fa81-15da-44ca-be12-b4c6fe8076a3": {
|
||||
"identity": {
|
||||
"createdAt": 1788286864454,
|
||||
"cwd": "/tmp"
|
||||
},
|
||||
"rows": {
|
||||
"sessionStats": {
|
||||
"ver": 1,
|
||||
"seq": 54,
|
||||
"val": {
|
||||
"turns": 2,
|
||||
"steps": 2,
|
||||
"llmMs": 2169,
|
||||
"toolMs": 0,
|
||||
"ttftMs": 1872,
|
||||
"ttftSteps": 2,
|
||||
"decodeMs": 297,
|
||||
"decodeTokens": 20,
|
||||
"lastTurn": 2,
|
||||
"openStep": null,
|
||||
"pendingCalls": {}
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"ver": 1,
|
||||
"seq": 54,
|
||||
"val": "验收标题-rc2"
|
||||
},
|
||||
"goal": {
|
||||
"ver": 4,
|
||||
"seq": 54,
|
||||
"val": null
|
||||
},
|
||||
"tokenUsage": {
|
||||
"ver": 1,
|
||||
"seq": 54,
|
||||
"val": {
|
||||
"totals": {
|
||||
"uncachedInputTokens": 6083,
|
||||
"outputTokens": 20,
|
||||
"cacheReadTokens": 9728,
|
||||
"cacheWriteTokens": 0
|
||||
},
|
||||
"last": {
|
||||
"turn": 2,
|
||||
"step": 1,
|
||||
"buckets": {
|
||||
"uncachedInputTokens": 112,
|
||||
"outputTokens": 4,
|
||||
"cacheReadTokens": 7808,
|
||||
"cacheWriteTokens": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"contextPressure": {
|
||||
"ver": 4,
|
||||
"seq": 54,
|
||||
"val": {
|
||||
"surfaceTokens": 178,
|
||||
"contextWindow": 1000000,
|
||||
"pressureTokens": 7920,
|
||||
"sampledSurfaceTokens": 168
|
||||
}
|
||||
},
|
||||
"contextBreakdown": {
|
||||
"ver": 2,
|
||||
"seq": 54,
|
||||
"val": {
|
||||
"systemTokens": 1620,
|
||||
"toolsTokens": 6475,
|
||||
"messageTokens": 178
|
||||
}
|
||||
},
|
||||
"subagentTiming": {
|
||||
"ver": 2,
|
||||
"seq": 54,
|
||||
"val": {
|
||||
"descriptorSeen": false,
|
||||
"settledMs": 0
|
||||
}
|
||||
},
|
||||
"subagent": {
|
||||
"ver": 2,
|
||||
"seq": 54,
|
||||
"val": {}
|
||||
},
|
||||
"permissions": {
|
||||
"ver": 1,
|
||||
"seq": 54,
|
||||
"val": {
|
||||
"preset": "workspace-write",
|
||||
"sandbox": "workspace-write",
|
||||
"approval": "ask"
|
||||
}
|
||||
},
|
||||
"sessionListMetadata": {
|
||||
"ver": 1,
|
||||
"seq": 54,
|
||||
"val": {
|
||||
"blank": false,
|
||||
"lastPromptAt": 1788286867410
|
||||
}
|
||||
},
|
||||
"imageLimits": {
|
||||
"ver": 1,
|
||||
"seq": 54,
|
||||
"val": null
|
||||
},
|
||||
"todos": {
|
||||
"ver": 2,
|
||||
"seq": 54,
|
||||
"val": null
|
||||
},
|
||||
"plan": {
|
||||
"ver": 2,
|
||||
"seq": 54,
|
||||
"val": {
|
||||
"active": false,
|
||||
"wanted": null,
|
||||
"running": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
212
packages/session/session-projection-cache/tests/fixtures/v4-session-doc.json
vendored
Normal file
212
packages/session/session-projection-cache/tests/fixtures/v4-session-doc.json
vendored
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
{
|
||||
"version": 4,
|
||||
"record": {
|
||||
"identity": {
|
||||
"createdAt": 1788286912530,
|
||||
"cwd": "/tmp"
|
||||
},
|
||||
"rows": {
|
||||
"title": {
|
||||
"ver": 1,
|
||||
"seq": 68,
|
||||
"val": "验收标题-alpha3"
|
||||
},
|
||||
"titleInput": {
|
||||
"ver": 3,
|
||||
"seq": 68,
|
||||
"val": {
|
||||
"first": {
|
||||
"seq": 7,
|
||||
"text": "请只回复一个词:pong"
|
||||
},
|
||||
"count": 2,
|
||||
"lastSeq": 58
|
||||
}
|
||||
},
|
||||
"llmRetry": {
|
||||
"ver": 1,
|
||||
"seq": 68,
|
||||
"val": {}
|
||||
},
|
||||
"sandboxMode": {
|
||||
"ver": 1,
|
||||
"seq": 68,
|
||||
"val": "workspace-write"
|
||||
},
|
||||
"goal": {
|
||||
"ver": 6,
|
||||
"seq": 68,
|
||||
"val": {
|
||||
"current": null,
|
||||
"seenGoalIds": [],
|
||||
"failure": null
|
||||
}
|
||||
},
|
||||
"tokenUsage": {
|
||||
"ver": 2,
|
||||
"seq": 68,
|
||||
"val": {
|
||||
"totals": {
|
||||
"uncachedInputTokens": 8192,
|
||||
"outputTokens": 35,
|
||||
"cacheReadTokens": 8064,
|
||||
"cacheWriteTokens": 0
|
||||
},
|
||||
"last": {
|
||||
"turn": 2,
|
||||
"step": 1,
|
||||
"buckets": {
|
||||
"uncachedInputTokens": 86,
|
||||
"outputTokens": 4,
|
||||
"cacheReadTokens": 8064,
|
||||
"cacheWriteTokens": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"contextPressure": {
|
||||
"ver": 4,
|
||||
"seq": 68,
|
||||
"val": {
|
||||
"surfaceTokens": 193,
|
||||
"contextWindow": 1000000,
|
||||
"pressureTokens": 8150,
|
||||
"sampledSurfaceTokens": 183
|
||||
}
|
||||
},
|
||||
"contextBreakdown": {
|
||||
"ver": 2,
|
||||
"seq": 68,
|
||||
"val": {
|
||||
"systemTokens": 1760,
|
||||
"toolsTokens": 6579,
|
||||
"messageTokens": 193
|
||||
}
|
||||
},
|
||||
"turnBoundary": {
|
||||
"ver": 2,
|
||||
"seq": 68,
|
||||
"val": {
|
||||
"openTurnStartSeq": null,
|
||||
"lastStepStartSeq": 57,
|
||||
"lastStepBoundary": {
|
||||
"kind": "end",
|
||||
"seq": 67
|
||||
},
|
||||
"lastTurn": 2
|
||||
}
|
||||
},
|
||||
"sessionStats": {
|
||||
"ver": 1,
|
||||
"seq": 68,
|
||||
"val": {
|
||||
"turns": 2,
|
||||
"steps": 2,
|
||||
"llmMs": 3486,
|
||||
"toolMs": 0,
|
||||
"ttftMs": 3199,
|
||||
"ttftSteps": 2,
|
||||
"decodeMs": 287,
|
||||
"decodeTokens": 35,
|
||||
"lastTurn": 2,
|
||||
"openStep": null,
|
||||
"pendingCalls": {}
|
||||
}
|
||||
},
|
||||
"turnOutline": {
|
||||
"ver": 2,
|
||||
"seq": 68,
|
||||
"val": {
|
||||
"turns": [
|
||||
{
|
||||
"turn": 1,
|
||||
"seq": 4,
|
||||
"prompt": "请只回复一个词:pong",
|
||||
"response": "pong"
|
||||
},
|
||||
{
|
||||
"turn": 2,
|
||||
"seq": 55,
|
||||
"prompt": "请只回复一个词:pong2",
|
||||
"response": "pong2"
|
||||
}
|
||||
],
|
||||
"draft": ""
|
||||
}
|
||||
},
|
||||
"agentPreset": {
|
||||
"ver": 1,
|
||||
"seq": 68,
|
||||
"val": "standard"
|
||||
},
|
||||
"subagentTiming": {
|
||||
"ver": 2,
|
||||
"seq": 68,
|
||||
"val": {
|
||||
"descriptorSeen": false,
|
||||
"settledMs": 0
|
||||
}
|
||||
},
|
||||
"subagent": {
|
||||
"ver": 2,
|
||||
"seq": 68,
|
||||
"val": {}
|
||||
},
|
||||
"permissions": {
|
||||
"ver": 2,
|
||||
"seq": 68,
|
||||
"val": {
|
||||
"preset": "workspace-write",
|
||||
"sandbox": "workspace-write",
|
||||
"approval": "ask",
|
||||
"seeded": false
|
||||
}
|
||||
},
|
||||
"modelSelection": {
|
||||
"ver": 2,
|
||||
"seq": 68,
|
||||
"val": {
|
||||
"lastUsed": {
|
||||
"provider": "deepseek-official",
|
||||
"model": "deepseek-v4-flash",
|
||||
"reasoningEffort": "high"
|
||||
},
|
||||
"pending": null
|
||||
}
|
||||
},
|
||||
"sessionListMetadata": {
|
||||
"ver": 1,
|
||||
"seq": 68,
|
||||
"val": {
|
||||
"blank": false,
|
||||
"lastPromptAt": 1788286917834
|
||||
}
|
||||
},
|
||||
"imageLimits": {
|
||||
"ver": 1,
|
||||
"seq": 68,
|
||||
"val": null
|
||||
},
|
||||
"todos": {
|
||||
"ver": 2,
|
||||
"seq": 68,
|
||||
"val": null
|
||||
},
|
||||
"plan": {
|
||||
"ver": 3,
|
||||
"seq": 68,
|
||||
"val": {
|
||||
"active": false,
|
||||
"wanted": null,
|
||||
"running": null,
|
||||
"activeAtLastHeader": false
|
||||
}
|
||||
},
|
||||
"subagentModelSelectionPolicy": {
|
||||
"ver": 1,
|
||||
"seq": 68,
|
||||
"val": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
128
packages/session/session-projection-cache/tests/fixtures/v5-lineageless-doc.json
vendored
Normal file
128
packages/session/session-projection-cache/tests/fixtures/v5-lineageless-doc.json
vendored
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
{
|
||||
"version": 5,
|
||||
"record": {
|
||||
"identity": {
|
||||
"createdAt": 1788286864454,
|
||||
"cwd": "/tmp"
|
||||
},
|
||||
"rows": {
|
||||
"sessionStats": {
|
||||
"ver": 1,
|
||||
"seq": 54,
|
||||
"val": {
|
||||
"turns": 2,
|
||||
"steps": 2,
|
||||
"llmMs": 2169,
|
||||
"toolMs": 0,
|
||||
"ttftMs": 1872,
|
||||
"ttftSteps": 2,
|
||||
"decodeMs": 297,
|
||||
"decodeTokens": 20,
|
||||
"lastTurn": 2,
|
||||
"openStep": null,
|
||||
"pendingCalls": {}
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"ver": 1,
|
||||
"seq": 54,
|
||||
"val": "\u9a8c\u6536\u6807\u9898-rc2"
|
||||
},
|
||||
"goal": {
|
||||
"ver": 4,
|
||||
"seq": 54,
|
||||
"val": null
|
||||
},
|
||||
"tokenUsage": {
|
||||
"ver": 1,
|
||||
"seq": 54,
|
||||
"val": {
|
||||
"totals": {
|
||||
"uncachedInputTokens": 6083,
|
||||
"outputTokens": 20,
|
||||
"cacheReadTokens": 9728,
|
||||
"cacheWriteTokens": 0
|
||||
},
|
||||
"last": {
|
||||
"turn": 2,
|
||||
"step": 1,
|
||||
"buckets": {
|
||||
"uncachedInputTokens": 112,
|
||||
"outputTokens": 4,
|
||||
"cacheReadTokens": 7808,
|
||||
"cacheWriteTokens": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"contextPressure": {
|
||||
"ver": 4,
|
||||
"seq": 54,
|
||||
"val": {
|
||||
"surfaceTokens": 178,
|
||||
"contextWindow": 1000000,
|
||||
"pressureTokens": 7920,
|
||||
"sampledSurfaceTokens": 168
|
||||
}
|
||||
},
|
||||
"contextBreakdown": {
|
||||
"ver": 2,
|
||||
"seq": 54,
|
||||
"val": {
|
||||
"systemTokens": 1620,
|
||||
"toolsTokens": 6475,
|
||||
"messageTokens": 178
|
||||
}
|
||||
},
|
||||
"subagentTiming": {
|
||||
"ver": 2,
|
||||
"seq": 54,
|
||||
"val": {
|
||||
"descriptorSeen": false,
|
||||
"settledMs": 0
|
||||
}
|
||||
},
|
||||
"subagent": {
|
||||
"ver": 2,
|
||||
"seq": 54,
|
||||
"val": {}
|
||||
},
|
||||
"permissions": {
|
||||
"ver": 1,
|
||||
"seq": 54,
|
||||
"val": {
|
||||
"preset": "workspace-write",
|
||||
"sandbox": "workspace-write",
|
||||
"approval": "ask"
|
||||
}
|
||||
},
|
||||
"sessionListMetadata": {
|
||||
"ver": 1,
|
||||
"seq": 54,
|
||||
"val": {
|
||||
"blank": false,
|
||||
"lastPromptAt": 1788286867410
|
||||
}
|
||||
},
|
||||
"imageLimits": {
|
||||
"ver": 1,
|
||||
"seq": 54,
|
||||
"val": null
|
||||
},
|
||||
"todos": {
|
||||
"ver": 2,
|
||||
"seq": 54,
|
||||
"val": null
|
||||
},
|
||||
"plan": {
|
||||
"ver": 2,
|
||||
"seq": 54,
|
||||
"val": {
|
||||
"active": false,
|
||||
"wanted": null,
|
||||
"running": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
220
packages/session/session-projection-cache/tests/fixtures/v5-session-doc.json
vendored
Normal file
220
packages/session/session-projection-cache/tests/fixtures/v5-session-doc.json
vendored
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
{
|
||||
"version": 5,
|
||||
"record": {
|
||||
"identity": {
|
||||
"createdAt": 1788286864454,
|
||||
"cwd": "/tmp",
|
||||
"isSeeded": false,
|
||||
"inheritedEventCount": 0
|
||||
},
|
||||
"rows": {
|
||||
"title": {
|
||||
"ver": 1,
|
||||
"seq": 71,
|
||||
"val": "验收标题-rc2"
|
||||
},
|
||||
"titleInput": {
|
||||
"ver": 3,
|
||||
"seq": 71,
|
||||
"val": {
|
||||
"first": {
|
||||
"seq": 7,
|
||||
"text": "请只回复一个词:pong"
|
||||
},
|
||||
"count": 3,
|
||||
"lastSeq": 60
|
||||
}
|
||||
},
|
||||
"llmRetry": {
|
||||
"ver": 1,
|
||||
"seq": 71,
|
||||
"val": {}
|
||||
},
|
||||
"sandboxMode": {
|
||||
"ver": 1,
|
||||
"seq": 71,
|
||||
"val": "workspace-write"
|
||||
},
|
||||
"goal": {
|
||||
"ver": 6,
|
||||
"seq": 71,
|
||||
"val": {
|
||||
"current": null,
|
||||
"seenGoalIds": [],
|
||||
"failure": null
|
||||
}
|
||||
},
|
||||
"tokenUsage": {
|
||||
"ver": 2,
|
||||
"seq": 71,
|
||||
"val": {
|
||||
"totals": {
|
||||
"uncachedInputTokens": 14219,
|
||||
"outputTokens": 24,
|
||||
"cacheReadTokens": 9728,
|
||||
"cacheWriteTokens": 0
|
||||
},
|
||||
"last": {
|
||||
"turn": 3,
|
||||
"step": 1,
|
||||
"buckets": {
|
||||
"uncachedInputTokens": 8136,
|
||||
"outputTokens": 4,
|
||||
"cacheReadTokens": 0,
|
||||
"cacheWriteTokens": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"contextPressure": {
|
||||
"ver": 4,
|
||||
"seq": 71,
|
||||
"val": {
|
||||
"surfaceTokens": 200,
|
||||
"contextWindow": 1000000,
|
||||
"pressureTokens": 8136,
|
||||
"sampledSurfaceTokens": 190
|
||||
}
|
||||
},
|
||||
"contextBreakdown": {
|
||||
"ver": 2,
|
||||
"seq": 71,
|
||||
"val": {
|
||||
"systemTokens": 1729,
|
||||
"toolsTokens": 6611,
|
||||
"messageTokens": 200
|
||||
}
|
||||
},
|
||||
"sessionStats": {
|
||||
"ver": 1,
|
||||
"seq": 71,
|
||||
"val": {
|
||||
"turns": 3,
|
||||
"steps": 3,
|
||||
"llmMs": 4368,
|
||||
"toolMs": 0,
|
||||
"ttftMs": 4030,
|
||||
"ttftSteps": 3,
|
||||
"decodeMs": 338,
|
||||
"decodeTokens": 24,
|
||||
"lastTurn": 3,
|
||||
"openStep": null,
|
||||
"pendingCalls": {}
|
||||
}
|
||||
},
|
||||
"agentPreset": {
|
||||
"ver": 1,
|
||||
"seq": 71,
|
||||
"val": "standard"
|
||||
},
|
||||
"subagentTiming": {
|
||||
"ver": 2,
|
||||
"seq": 71,
|
||||
"val": {
|
||||
"descriptorSeen": false,
|
||||
"settledMs": 0
|
||||
}
|
||||
},
|
||||
"subagent": {
|
||||
"ver": 2,
|
||||
"seq": 71,
|
||||
"val": {}
|
||||
},
|
||||
"turnBoundary": {
|
||||
"ver": 2,
|
||||
"seq": 71,
|
||||
"val": {
|
||||
"openTurnStartSeq": null,
|
||||
"lastStepStartSeq": 59,
|
||||
"lastStepBoundary": {
|
||||
"kind": "end",
|
||||
"seq": 70
|
||||
},
|
||||
"lastTurn": 3
|
||||
}
|
||||
},
|
||||
"turnOutline": {
|
||||
"ver": 2,
|
||||
"seq": 71,
|
||||
"val": {
|
||||
"turns": [
|
||||
{
|
||||
"turn": 1,
|
||||
"seq": 4,
|
||||
"prompt": "请只回复一个词:pong",
|
||||
"response": "pong"
|
||||
},
|
||||
{
|
||||
"turn": 2,
|
||||
"seq": 40,
|
||||
"prompt": "请只回复一个词:pong2",
|
||||
"response": "pong2"
|
||||
},
|
||||
{
|
||||
"turn": 3,
|
||||
"seq": 57,
|
||||
"prompt": "请只回复一个词:pong3",
|
||||
"response": "pong3"
|
||||
}
|
||||
],
|
||||
"draft": ""
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"ver": 2,
|
||||
"seq": 71,
|
||||
"val": {
|
||||
"preset": "workspace-write",
|
||||
"sandbox": "workspace-write",
|
||||
"approval": "ask",
|
||||
"seeded": true
|
||||
}
|
||||
},
|
||||
"modelSelection": {
|
||||
"ver": 2,
|
||||
"seq": 71,
|
||||
"val": {
|
||||
"lastUsed": {
|
||||
"provider": "deepseek-official",
|
||||
"model": "deepseek-v4-flash",
|
||||
"reasoningEffort": "high"
|
||||
},
|
||||
"pending": null
|
||||
}
|
||||
},
|
||||
"sessionListMetadata": {
|
||||
"ver": 1,
|
||||
"seq": 71,
|
||||
"val": {
|
||||
"blank": false,
|
||||
"lastPromptAt": 1788287660106
|
||||
}
|
||||
},
|
||||
"imageLimits": {
|
||||
"ver": 1,
|
||||
"seq": 71,
|
||||
"val": null
|
||||
},
|
||||
"todos": {
|
||||
"ver": 2,
|
||||
"seq": 71,
|
||||
"val": null
|
||||
},
|
||||
"plan": {
|
||||
"ver": 3,
|
||||
"seq": 71,
|
||||
"val": {
|
||||
"active": false,
|
||||
"wanted": null,
|
||||
"running": null,
|
||||
"activeAtLastHeader": false
|
||||
}
|
||||
},
|
||||
"subagentModelSelectionPolicy": {
|
||||
"ver": 1,
|
||||
"seq": 71,
|
||||
"val": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue