Ver Fonte

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
Tianyi Cui há 1 semana atrás
pai
commit
ac8b58241d

+ 8 - 3
apps/web/tests/reference-composer.e2e.ts

@@ -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
apps/web/tests/scaffold.ts

@@ -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

+ 25 - 27
packages/api/session-controller/tests/session-cold.host.spec.ts

@@ -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 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',
+    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,
         },
-      ],
-      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.agents.register({ id: session.id, session, status: 'running', ctx } as Agent)
-    release.resolve(undefined)
+    ctx.provide('sessionProjectionCache', {
+      cachedSnapshot: cacheLookup,
+      cachedPredecessorTitle: () => undefined,
+    } as never)
+    const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
 
-    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()
   })
 
 })