fix(sidechat): read the transcript through the plugin's own events route
DSH 0.1.2-alpha.1 removed the client `ctx.connection.api` face (Remote gateway migration), so every Side Chat transcript poll (`sessions.history`) threw TypeError into the view's catch and was swallowed — the tab rendered an empty transcript forever, silently (no console error, so the mount lane never caught it). The successor APIs are not a drop-in either: session/follow|page reject plain session addresses for origin:'subagent' logs (agent-busy fence) and paging cannot read past the current cursor. Move the transcript to the plugin's own wire — one transport for every DSH generation: - new `sidechat.events` route: live threads read the in-memory `agent.session.events`; cold threads read `sessionPersistence.inspect` (both seams exist since 0.1.0-rc.8; alpha.1's inspect returns the expanded logical log, never the packed chunk-row encoding). The inherited seed is cut host-side (`threadOwnLogEvents`), polls ride an `afterSeq` delta, and the response is tail-capped at 8000 events (the old client walk ceiling). - SideChatView.fetchThread is single-path; collectOwnEvents (the client walk) is gone, and so are the ctx.connection mirror and inject entry (no consumer left). - transcriptRows keeps re-cutting at session/end-seed as defense in depth. Verification: - unit: sidechat.events route (seed cut, afterSeq delta + never resurrecting the seed, cap, cold/live sources, 404/503/bad-request), full suite 1104 passed; typecheck and build (purity gate) green. - mount e2e vs a real dsh-v0.1.2-alpha.1 source build: 14/14, including the Side Chat tab sweep polling sidechat.events (performance resource assertion) and live + post-dispose (persisted) route reads. - mount e2e vs PATH dsh 0.1.0-rc.8: 14/14 (no regression).
This commit is contained in:
@@ -288,6 +288,13 @@ test('plugin mounts into the DSH shell and survives a built-in tab sweep', async
|
||||
await assertNoCrash()
|
||||
}
|
||||
|
||||
// The sweep opened the Side Chat tab, whose view auto-creates a thread and
|
||||
// polls the transcript — that poll MUST ride the plugin's own
|
||||
// sidechat.events route (the DSH-0.1.2-safe transport this lane locks).
|
||||
const polledTranscript = await page.evaluate(() =>
|
||||
performance.getEntriesByType('resource').some(entry => entry.name.includes('/sidebar/api/sidechat.events')))
|
||||
expect(polledTranscript, 'the Side Chat tab must poll sidechat.events for its transcript').toBe(true)
|
||||
|
||||
// Side Chat host-route smoke against the REAL host: create a thread child
|
||||
// under the seeded session (custom-seed creation through AgentRegistry),
|
||||
// deliver a follow-up, cancel, and release it. The turn itself cannot run
|
||||
@@ -315,12 +322,24 @@ test('plugin mounts into the DSH shell and survives a built-in tab sweep', async
|
||||
listedItems.some(item => item.sessionId === childId),
|
||||
'the thread child must appear in the host session list',
|
||||
).toBe(true)
|
||||
// Transcript read from the LIVE agent's log (the transport the tab polls;
|
||||
// the inherited seed is cut host-side, so with no model route the own
|
||||
// slice carries no message events).
|
||||
const eventsLive = await api.post(sidebarApi('sidechat.events'), { data: { childId } })
|
||||
expect(eventsLive.ok(), `sidechat.events (live): ${eventsLive.status()} ${await eventsLive.text()}`).toBe(true)
|
||||
const eventsLiveBody = (await eventsLive.json()) as { ok: boolean; value?: { events?: Array<{ type: string }> } }
|
||||
expect(eventsLiveBody.ok, `sidechat.events envelope: ${JSON.stringify(eventsLiveBody)}`).toBe(true)
|
||||
expect(Array.isArray(eventsLiveBody.value?.events), 'sidechat.events must answer an events array').toBe(true)
|
||||
for (const method of ['sidechat.prompt', 'sidechat.cancel', 'sidechat.dispose']) {
|
||||
const response = await api.post(sidebarApi(method), {
|
||||
data: method === 'sidechat.prompt' ? { childId, text: 'follow-up' } : { childId },
|
||||
})
|
||||
expect(response.ok(), `${method}: ${response.status()} ${await response.text()}`).toBe(true)
|
||||
}
|
||||
// After dispose the agent is gone: the same read must fall back to the
|
||||
// PERSISTED log (the cold path a re-opened tab polls).
|
||||
const eventsCold = await api.post(sidebarApi('sidechat.events'), { data: { childId } })
|
||||
expect(eventsCold.ok(), `sidechat.events (cold): ${eventsCold.status()} ${await eventsCold.text()}`).toBe(true)
|
||||
|
||||
// The Codex-style immediate-create flow: a blank question creates an
|
||||
// EMPTY thread (no prompt admitted), sidechat.info reports the live
|
||||
|
||||
Reference in New Issue
Block a user