Merge remote-tracking branch 'origin/worktree/session-format-04-live-assistant-stream' into worktree/session-format-05-v1-v2-chunk-migration

# Conflicts:
#	apps/web/tests/scaffold.ts
This commit is contained in:
Tianyi Cui
2026-09-02 22:19:50 +08:00
3 changed files with 38 additions and 33 deletions
+8 -3
View File
@@ -133,8 +133,9 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
beforeAll(async () => {
scaffold = await launchWebScaffold({})
await seedSession(scaffold, sourceSessionFixture(), SOURCE_SESSION_ID)
await seedSession(scaffold, targetSessionFixture(), TARGET_SESSION_ID)
const targetCreatedAt = Date.now() - 60_000
await seedSession(scaffold, sourceSessionFixture(), SOURCE_SESSION_ID, undefined, undefined, { createdAt: targetCreatedAt - 1 })
await seedSession(scaffold, targetSessionFixture(), TARGET_SESSION_ID, undefined, undefined, { createdAt: targetCreatedAt })
browser = await chromium.launch()
page = await newEnglishPage(browser)
tripwire = watchConsole(page)
@@ -377,7 +378,11 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
const group = page.getByRole('treeitem', { name: /Ungrouped/ })
await group.waitFor({ timeout: 15_000 })
if (await group.getAttribute('aria-expanded') !== 'true') await group.click()
const target = page.getByRole('treeitem', { name: /Reference order target/ })
// Both logs were written behind the running Host and have no cache rows, so
// cold listing uses their shared Workspace fallback. Explicit creation
// times keep the target first without opening either body for a title.
const groupSection = group.locator('xpath=ancestor::*[contains(@class, "groupSection")][1]')
const target = groupSection.locator('[role="treeitem"]').nth(1)
await target.waitFor({ timeout: 15_000 })
await target.click()
await page.getByRole('button', { name: /^Session recall\s*Research notes$/ }).waitFor({ timeout: 15_000 })
+4 -2
View File
@@ -1227,6 +1227,7 @@ export async function seedSession(
id: string,
agentPreset?: string,
fixturePath?: string,
options: { readonly createdAt?: number } = {},
): Promise<SessionId> {
const decoded = parseSeedFixture(realizeSeedFixture(scaffold, fixtureText, id), fixturePath)
const events = decoded.events
@@ -1235,10 +1236,11 @@ export async function seedSession(
// An open final turn would be mutated by resume's crash repair on first
// open; a committed seed must be a closed recording.
if (last.type !== 'turn/end') throw new Error(`seed fixture must end in turn/end, got ${last.type}`)
const createdAt = options.createdAt ?? Date.now() - 60_000
const meta: SessionHeader = {
version: SESSION_FORMAT_VERSION,
id: SessionId(id),
createdAt: Date.now() - 60_000,
createdAt,
isSeeded: false,
cwd: scaffold.workspaceCwd,
delegationDepth: 0,
@@ -1248,7 +1250,7 @@ export async function seedSession(
if (typeof fixtureCreatedAt !== 'number') {
throw new Error('seed fixture requires a numeric createdAt header')
}
const timeAnchor = fixtureCreatedAt === 0 ? meta.createdAt : fixtureCreatedAt
const timeAnchor = fixtureCreatedAt === 0 ? createdAt : fixtureCreatedAt
let nextTime = timeAnchor
const materializedEvents: SessionEvent[] = events.map((event) => {
const time = nextTime
@@ -172,42 +172,39 @@ describe('sessions.list cold merge', () => {
expect(inspect).not.toHaveBeenCalled()
})
it('prefers a live row attached during the query without folding its seed', async () => {
it('prefers a live row attached during cache lookup without folding its seed', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
const meta = header('attached-during-list', 100)
const started = Promise.withResolvers<undefined>()
const release = Promise.withResolvers<undefined>()
providePersistence(ctx, {
list: async () => {
started.resolve(undefined)
await release.promise
return [meta]
},
list: () => Promise.resolve([meta]),
})
const cacheLookup = vi.fn(() => {
const session = ctx.sessions.create(meta.id, {
seed: [
{ type: 'turn/start', seq: SessionSeq(0), time: 200, data: { turn: 1 } },
{
type: 'user/message', seq: SessionSeq(1), time: 300,
data: createUserMessage({ content: [{ type: 'text', text: 'live' }], source: { kind: 'user' } }),
surfaceOp: 'append',
},
],
meta: {
...meta.cwd === undefined ? {} : { cwd: meta.cwd },
createdAt: meta.createdAt,
},
})
ctx.agents.register({ id: session.id, session, status: 'running', ctx } as Agent)
return undefined
})
ctx.provide('sessionProjectionCache', {
cachedSnapshot: cacheLookup,
cachedPredecessorTitle: () => undefined,
} as never)
const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
const listing = remote.list(request({}))
await started.promise
const session = ctx.sessions.create(meta.id, {
seed: [
{ type: 'turn/start', seq: SessionSeq(0), time: 200, data: { turn: 1 } },
{
type: 'user/message', seq: SessionSeq(1), time: 300,
data: createUserMessage({ content: [{ type: 'text', text: 'live' }], source: { kind: 'user' } }),
surfaceOp: 'append',
},
],
meta: {
...meta.cwd === undefined ? {} : { cwd: meta.cwd },
createdAt: meta.createdAt,
},
})
ctx.agents.register({ id: session.id, session, status: 'running', ctx } as Agent)
release.resolve(undefined)
const response = await listing
const response = await remote.list(request({}))
if (!response.ok) throw new Error('list failed')
expect(response.value.items).toEqual([
expect.objectContaining({
@@ -217,6 +214,7 @@ describe('sessions.list cold merge', () => {
updatedAt: 100,
}),
])
expect(cacheLookup).toHaveBeenCalledOnce()
})
})