Skip to content

fix(sidechat): cut the inherited prefix before the thread's own descriptor - #1039

Open
fbeeg wants to merge 1 commit into
omdsh-dev:devfrom
fbeeg:fix/sidechat-descriptor-cut-before-own-events
Open

fbeeg wants to merge 1 commit into
omdsh-dev:devfrom
fbeeg:fix/sidechat-descriptor-cut-before-own-events

Conversation

@fbeeg

@fbeeg fbeeg commented Oct 22, 2026

Copy link
Copy Markdown

Summary

sidechat.start creates the side thread with inheritedEventCount: seed.length (src/sidechat-routes.ts:713 on dev at 0fab0af), and the seed ends with the thread's own subagent/descriptor (line 673: [...inheritance.seed, ...directorySeed, descriptorEvent]). The cut is therefore one past the descriptor, so the descriptor sits inside the inherited prefix.

This PR builds the seed the way the host's own session.fork does (buildForkSeed in @deepseek-ai/dsh-session): the copied parent history, then a session/end-seed {inherited: true} marker at the cut, then the thread's own working-directory/change stamp and descriptor. inheritedEventCount becomes the length of the copied history. The seeded Session constructor accepts a cut shorter than the seed only when the event at the cut is that marker, so the marker is required, not decorative.

Why

The host looks for a child's descriptor only in the child's own events, events.slice(inheritedEventCount):

  • cold resume in @deepseek-ai/dsh-subagent (packages/subagent/subagent/src/manager.ts, foldSubagentDescriptor(source.events.slice(source.inheritedEventCount)), about line 1348 at 0.2.1-rc.2; the same code ships in the 0.2.1-rc.1 package this repo pins)
  • the persisted-session facts (packages/session/session-format-v3-to-v4/src/facts.ts:17 at 0.2.1-rc.2)

With the descriptor inside the prefix, a side thread has no descriptor as far as the host can tell. It lists as corrupt, cannot be resumed as a continuable child, and is never cached, so every read decodes the whole log again. Measured on DSH 0.1.7 with a large thread (about 47k events), each decode blocked the event loop for about 1.8 s.

Existing threads keep their header. Threads created after this change get the corrected layout. The plugin's own readers already handle it: the transcript cuts at the last session/end-seed, the cold-resume sweep folds only [0, inheritedEventCount), and the inbox clear after create is unchanged.

Test plan

  • New case in tests/sidechat-routes.spec.ts ("puts the side thread descriptor after the inherited cut, where the host looks for it"). It runs sidechat.start, feeds the options into the real Session.create from @deepseek-ai/dsh-session 0.2.1-rc.1, and asserts that foldSubagentDescriptor(events.slice(inheritedEventCount)) finds the continuable / sidechat descriptor. It also checks that the inherited prefix is exactly the copied parent history and that the own range is session/end-seed, working-directory/change, subagent/descriptor.
    • Before the fix: fails with expected undefined to match object { mode: 'continuable', … }.
    • After the fix: passes.
  • Updated the existing seed-shape assertions in tests/sidechat-routes.spec.ts, the route-shaped helper in tests/sidechat-seed-validation.spec.ts, and the source-text guard in tests/host-runtime-imports.spec.ts.
  • docs/host-contracts.md item 10: one paragraph on where the cut goes and why.
  • pnpm test: 221 files, 2683 passed, 17 skipped. pnpm typecheck and pnpm lint are clean.

This is independent of #936. That PR also touches sidechat.start (the create timeout), but different lines, and the two do not conflict.

🤖 Generated with Claude Code

…iptor

sidechat.start passed inheritedEventCount = seed.length while the thread's
own subagent/descriptor was the last seed event, so the descriptor sat
inside the inherited prefix. The host reads a child's descriptor only from
events.slice(inheritedEventCount), so side threads had none: they listed as
corrupt and could not be resumed as continuable children.

Build the seed the way the host's own session.fork does (buildForkSeed):
the copied parent history, an inherited session/end-seed marker at the cut,
then the thread's own working-directory stamp and descriptor. The cut is
the copied history's length.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant