fix(sidechat): stamp the seed with fork markers so the child inherits no pending inbox
sidechat.start passed `seed` without `meta.isSeeded: true` + `inheritedEventCount`, so dsh-session treated the whole seed as the child's OWN events and the child's Inbox constructor replayed the parent's `agent/inbox/spliced` history — inheriting whatever input sat UNCLAIMED in the parent at the click moment: - next-turn: a queued follow-up behind a still-running turn; - next-step (the reported long-context case, parent queue empty): a tool-result context / steering spliced between step boundaries. The first side prompt then claimed [phantom, boundary, question] — the stale parent message was sent to the model BEFORE the boundary and logged as a user/message. The fix passes the exact marker pair the host's own session.fork uses (api-session-controller); ownEvents() now stops at the end-seed boundary and the inherited inbox replays empty. Regression tests run the REAL Session.create + Inbox in both directions (old shape inherits the phantom, marked shape stays empty); routes tests pin the two create options. Verified: pnpm typecheck / test (5 pre-existing unrelated UI-spec failures on main unchanged) / build.
This commit is contained in:
@@ -27,6 +27,7 @@ import type { Agent, AgentSetup, CreateAgentOptions, ResumeAgentOptions } from '
|
||||
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
|
||||
import type { Context as CordisContext } from '@deepseek-ai/cordis'
|
||||
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { SessionLogOffset } from '@deepseek-ai/dsh-session'
|
||||
import type {
|
||||
Context,
|
||||
SidebarAgentPresetsService,
|
||||
@@ -238,16 +239,28 @@ export function buildSidechatApi(ctx: Context): SidechatRoutes {
|
||||
data: descriptor as unknown as Record<string, unknown>,
|
||||
}
|
||||
const seed = [...inheritance.seed, descriptorEvent]
|
||||
// Fork-marker fields (the exact shape the host's own session.fork uses,
|
||||
// api-session-controller): without `isSeeded` + `inheritedEventCount` the
|
||||
// session treats the whole seed as the child's OWN events, so the child's
|
||||
// Inbox constructor replays the parent's `agent/inbox/spliced` events and
|
||||
// inherits whatever input sat UNCLAIMED in the parent at the click moment
|
||||
// (a queued follow-up, or a tool-result context spliced into next-step
|
||||
// between step boundaries of a long-running turn). The first side prompt
|
||||
// would then claim and send that stale message BEFORE the boundary +
|
||||
// question. The marker keeps `ownEvents()` at the end-seed boundary, so
|
||||
// the inherited inbox replays to empty.
|
||||
const options: CreateAgentOptions = {
|
||||
sessionId: childId,
|
||||
meta: {
|
||||
...(parentSession.header.cwd === undefined ? {} : { cwd: parentSession.header.cwd }),
|
||||
parentSession: parentSession.id,
|
||||
isSeeded: true,
|
||||
origin: 'subagent',
|
||||
delegationDepth: (parentSession.header.delegationDepth ?? 0) + 1,
|
||||
...(agentPreset === undefined ? {} : { agentPreset }),
|
||||
},
|
||||
seed: seed as unknown as readonly SessionEvent[],
|
||||
inheritedEventCount: SessionLogOffset(seed.length),
|
||||
agentOptions: { ...parent.options },
|
||||
setup,
|
||||
signal: AbortSignal.timeout(CREATE_TIMEOUT_MS),
|
||||
|
||||
Reference in New Issue
Block a user