Skip to content

fix: apply the chosen runtime to continued sessions and keep continue/fork top-level - #702

Merged
pedronauck merged 2 commits into
mainfrom
fix-continue-agent
Oct 7, 2026
Merged

pedronauck merged 2 commits into
mainfrom
fix-continue-agent

Conversation

@pedronauck

@pedronauck pedronauck commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Problem

Two bugs Pedro reported on desktop v0.3.0, both in "Continue with another agent" / "Fork from here" (spec .compozy/tasks/session-continue-fork/, shipped in #684).

Bug 1: the chosen runtime/model was not applied to the continued session.
Repro: create a session on Codex GPT-6.1-Sol and send "oi". Open the session menu → Continue with another agent. Leave Agent = general, pick Runtime → Fable 5 (Claude Code), set Open in = This window, and click Continue. The new session opens ("Continued from Saudação inicial ao Pedro", "Nothing said here yet"), but the composer model selector shows GPT-6.1-Sol. Sending a message would bind Codex, not Claude.

Bug 2: continued and forked sessions rendered nested under their source.
The sidebar showed the source row with "⌄ 1" and the new session indented under it. Product rule: sessions spawned by an agent (sub-agents, orchestration children) are children and may nest. Sessions the user creates with Continue or Fork are new top-level sessions. Their lineage ("Continued from X" / "Forked from X") is provenance metadata (pill, divider, inspector), not hierarchy.

Root cause

Bug 1: the composer never sees the chosen runtime

  • The dialog sends the runtime correctly (web/src/systems/session/hooks/use-session-continue-dialog.ts runtimeRequest), and the handler passes it through (internal/api/core/session_derive.go deriveRuntimeFromPayload).
  • The derive applies it only as create-time fields (Provider/Model/... in applyDeriveRuntime, internal/session/derive_prepare.go). It never sets the session's SelectedRuntime, the durable runtime intent.
  • A derived child is a logical session created through CreateAccepted, so it stays unbound until the first prompt (internal/session/manager_create_accepted.go). While unbound, the API leaves out runtime.effective (runtimeHasEffectiveSelection in internal/api/core/conversions.go: Unbound with TransitionNone → no effective runtime), and runtime.selected is nil.
  • The composer seeds from selected and falls back to effective, then to the agent's default runtime (promptRuntimeDefaults → resolveAgentRuntimeValue(agent) in web/src/systems/session/hooks/use-session-prompt-runtime.ts). So it showed Codex, and its first prompt sent Codex as an explicit runtime snapshot, overriding the create-time fields.
  • The same class of bug hit route continues: the composer sent the agent default, which then failed pendingRouteMatchesSelection and silently dropped the pending route. It also hit forks whose source ran on a runtime other than the agent default: the child is locked to the source runtime by forkRuntimeFromMeta, but its composer showed the agent default.

Bug 2: hierarchy consumers ignored the typed lineage edge

  • feat: continue and fork sessions across agents, with fallback account routes #684 already gave every lineage a typed kind ("", provenance, recovery, spawn, continue, fork). It is stored as a catalog column and in meta.json, with a lossless backfill (internal/store/session_lineage.go). The store already separates the two meanings.
  • Consumers that build hierarchy still treated every parent_session_id as a parent:
    • Web tree: buildSessionTree (web/src/systems/session/lib/session-hierarchy.ts) nested any session whose lineage.parent_session_id was loaded. The spec had decided "nesting unchanged" (_uiux.md S10), and Pedro's new rule overrides that.
    • Supervision: sessionWorkSources.children (internal/daemon/session_work_sources.go) counted every row with parent=<id> as an active_child work signal, and that signal defers inactivity stops. A continued child (an accepted logical session is active) kept its source session alive.
    • Goal authorization: authorizeGoalDescendant (internal/daemon/loop_goal_command.go) walked parent_session_id, so the source session's agent could command Goals on a session the user had continued from it.

Fix

Session runtime (Go, internal/session)

  • applyDeriveRuntime marks the create opts with deriveSelectsRuntime whenever an explicit runtime, a declared route, or the fork source runtime applies. A speed left unset takes the target agent's default speed, as the bind would resolve it.
  • prepareCreateStart turns those (post-hook) runtime fields into the child's SelectedRuntime at revision 1 (deriveSelectedRuntime, manager_create_prepare.go). This goes through the existing start-spec → Session → meta.json / catalog path, so the existing payload mapper exposes it as runtime.selected. A runtime without a provider (a model or speed alone) still refines the agent default at bind and records no selection.
  • Result: the unbound child's composer shows the chosen runtime, and a prompt without its own runtime binds it (normalizePromptRuntimeSelection already prefers SelectedRuntime). A routed child's selection matches its pending route, so the route is no longer dropped by the composer's default.

Hierarchy vs provenance (Go + web)

  • store.HierarchyParentSessionID(lineage) (internal/store/session_lineage.go) is the single rule: it returns the parent for root/provenance/recovery/spawn edges and "" for continue/fork.
  • internal/daemon/session_work_sources.go: a continued or forked session is not active-child work, so it never defers the source's inactivity stop.
  • internal/daemon/loop_goal_command.go: Goal descendant authorization walks hierarchy parents only.
  • web/src/systems/session/lib/session-hierarchy.ts: sessionTreeParentId reuses sessionOriginKind (the same predicate as the origin pill), so continue/fork rows are roots in every list built on the shared tree (window sidebar, sessions modal, agent detail, the cycle shortcut order, and thread filtering). Spawned, provenance, and recovery children keep nesting, including sub-agents a continued session spawns, which nest under it.
  • Provenance is untouched: lineage.parent_session_id still names the source for the origin pill/divider, the inspector, compozy session status Origin, and the CLI PARENT column.

Reference research

  • t3code (docs/orchestration-v2/core-graph-and-data-model.md, packages/client-runtime/src/state/threadRelationships.ts, apps/web/src/components/Sidebar.logic.ts): uses one lineage record { parentThreadId, relationshipToParent: "fork" | "subagent", rootThreadId } plus forkedFrom. Forks keep parentThreadId = source, and the sidebar decides by relationship kind. Forks render as ordinary top-level threads, and subagents are handled separately (isSidebarSubagentThread). Adopted: one parent pointer plus a typed relationship, with the UI and daemon deciding hierarchy by kind. Compozy already persists this shape as lineage.kind.
  • synara (packages/contracts/src/orchestration.ts, apps/server/src/orchestration/decider.ts): uses separate fields, with parentThreadId for subagents, forkSourceThreadId for forks (created with parentThreadId: null), and handoff.sourceThreadId for provider handoffs. Its sidebar nests by parentThreadId only. Same product rule (forks/handoffs are top-level). I did not adopt the field split, because Compozy can express the rule with the existing typed kind without a migration or a DTO break.
  • monocode (src/features/sessions/model/session.ts): handoff is an in-session harness switch (HandoffMeta), and subagents are mirrored as blocks inside the parent transcript. There is no fork-as-child nesting, so it agrees with the rule but has no data-model equivalent.

Compatibility notes (SD-013)

  • User state: no SQLite or meta.json shape change, so no migration. Existing continue/fork rows already carry lineage_kind (continue/fork), written by feat: continue and fork sessions across agents, with fallback account routes #684 and backfilled for older rows, so they become top-level immediately and losslessly. Existing unbound derived children made before this fix have no selected runtime. They keep today's behavior (agent default until the user picks a model). New derives get the selection.
  • Public surfaces: no wire shape change. runtime.selected / selection_revision are existing fields that are now populated for derived children. lineage.parent_session_id keeps naming the source. The parent= / root= catalog filters (CLI --parent/--root, compozy__session_list) still match stored fields as-is, so they include continued/forked sessions. The docs say to read lineage.kind to tell them apart. No OpenAPI or codegen change, and codegen-check passed locally.
  • Internal: CreateOpts.deriveSelectsRuntime is unexported and set only by the derive path.

Tests added/changed

Invariant Suite
A continued child selects the chosen runtime (rev 1), the selection survives restart + resume, and a runtime-less first prompt binds it internal/session/manager_derive_test.go TestContinueSession "Should select the chosen runtime on the unbound child and bind it"
A routed continue selects the route's provider same suite, existing route subtest (one added assertion)
A fork's selection is the source runtime same file, TestForkSession whole-session subtest (added assertion)
The composer prefers an unbound session's selection over the agent default web/.../hooks/__tests__/use-session-prompt-runtime.test.tsx "Should prefer an unbound session's selected runtime over the agent default"
Hierarchy parent rule per lineage kind internal/store/session_lineage_test.go TestHierarchyParentSessionID
Continued/forked sessions are top-level; spawned children (also of a continued session) nest web/.../lib/__tests__/session-hierarchy.test.ts "Should keep continued and forked sessions top-level while spawned children nest"
A continued session is not the source's active-child work internal/daemon/session_work_sources_test.go (new, there was no suite for sessionWorkSources.children)
Goal authorization rejects a session continued from the caller internal/daemon/loop_goal_command_test.go "Should reject a session continued from the caller"

Each new Go regression fails without the production change: I checked this by temporarily reverting each fix.

Focused checks run

  • go test ./internal/session/ ./internal/store/: 1651 passed. 4 failures (TestManagerSpawnHooksCarryLineageAndCannotWidenPermissions, TestSpawnGovernanceUsesOnlyContiguousSpawnedLineage) were Stop(...) context deadline exceeded while the daemon suite ran concurrently on the same machine. They pass 3/3 with -count=3 alone and don't touch the derive path.
  • go test ./internal/daemon/ ./internal/api/core/: 4015 passed.
  • go test ./skills/: 33 passed.
  • golangci-lint run ./internal/session/ ./internal/store/ ./internal/daemon/: clean.
  • vitest run src/systems/session/lib src/systems/session/components/session-list …: 36 files / 278 tests passed. use-session-prompt-runtime.test.tsx: 6 passed.
  • turbo run typecheck lint --filter=compozy-web: passed (including codegen-check).

Left to CI

make gate / full Go race lanes, full Vitest, Playwright E2E (session-derive.spec.ts), and Storybook. Per the controller's instructions I did not run these locally.

QA scenarios touched

  • ET-web-session-continue: new steps 10 (runtime) and 11 (top-level row), fix note, reset to untested.
  • ET-web-session-fork-from-here: fix note and walk, reset to untested.
  • ET-web-session-sidebar-threads, ET-web-sessions-catalog-modal: qa-impact flags (continue/fork no longer nest).
  • Cross-surface impact entry: docs/_memory/change-impact.md (2026-10-06). Docs: packages/site/content/docs/sessions/lifecycle.mdx. Official skill: skills/compozy/references/runtime-operations.md.

Risks / follow-ups

  • Recovery children ("Restart in a new session") are user-created too, but this PR keeps them nested, because the request named only Continue/Fork. Making them top-level is a one-line change in the predicate if wanted.
  • root_session_id / spawn_depth of a derived child still point at the source's tree, and spawned sessions under a continued child inherit that root. This only affects observability grouping and root= filters. Re-rooting would mean a data migration plus a filter-semantics change, so I left it out of scope.
  • Derived children created before this fix (unbound, no message yet) still show the agent default until the user picks a model.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Continued and forked sessions now appear as top-level sessions rather than children of their source. Spawned sessions remain nested under their parent.
    • Goal commands and active-child work signals now follow the correct session hierarchy.
    • Unbound sessions retain their selected runtime and use it ahead of agent defaults, including after resuming.

pedronauck and others added 2 commits October 6, 2026 12:30
A derived child got the chosen runtime as create-time fields only. While
unbound it publishes no effective runtime, so the web composer fell back to
the agent default and its first prompt bound that instead. The derive now
records the explicit runtime, route, or fork source runtime as the child's
selected runtime at revision 1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Continue and fork record their source as provenance, not hierarchy. One
predicate over lineage.kind now keeps them out of the session list tree,
the supervision active-child signal, and Goal descendant authorization,
while spawned, provenance, and recovery children keep nesting.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@vercel

vercel Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
compozy-site Ready Ready Preview Oct 6, 2026 7:34pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: compozy/compozy/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 294d68c8-e098-4ffb-aef8-b11daedf3b69
📥 Commits

Reviewing files that changed from the base of the PR and between 0f55cc5 and 854fd0e.

⛔ Files ignored due to path filters (7)
  • docs/_memory/change-impact.md is excluded by !**/*.md
  • docs/qa/scenarios/ET-web-session-continue.md is excluded by !**/*.md
  • docs/qa/scenarios/ET-web-session-fork-from-here.md is excluded by !**/*.md
  • docs/qa/scenarios/ET-web-session-sidebar-threads.md is excluded by !**/*.md
  • docs/qa/scenarios/ET-web-sessions-catalog-modal.md is excluded by !**/*.md
  • packages/site/content/docs/sessions/lifecycle.mdx is excluded by !**/*.mdx
  • skills/compozy/references/runtime-operations.md is excluded by !**/*.md
📒 Files selected for processing (13)
  • internal/daemon/loop_goal_command.go
  • internal/daemon/loop_goal_command_test.go
  • internal/daemon/session_work_sources.go
  • internal/daemon/session_work_sources_test.go
  • internal/session/derive_prepare.go
  • internal/session/manager_create_prepare.go
  • internal/session/manager_derive_test.go
  • internal/session/manager_types.go
  • internal/store/session_lineage.go
  • internal/store/session_lineage_test.go
  • web/src/systems/session/hooks/__tests__/use-session-prompt-runtime.test.tsx
  • web/src/systems/session/lib/__tests__/session-hierarchy.test.ts
  • web/src/systems/session/lib/session-hierarchy.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.


Walkthrough

The changes separate lineage provenance from session hierarchy in daemon authorization, child-work discovery, and the web session tree. They also add selected runtime derivation for continued and forked sessions, including default-speed handling, selection revisions, and runtime-selection coverage.

Changes

Session hierarchy

Layer / File(s) Summary
Hierarchy parent semantics
internal/store/session_lineage.go, internal/store/session_lineage_test.go
Adds HierarchyParentSessionID to exclude continued and forked sessions from hierarchy parent lookups. Tests cover supported, unsupported, and missing lineage.
Daemon hierarchy lookups
internal/daemon/loop_goal_command.go, internal/daemon/loop_goal_command_test.go, internal/daemon/session_work_sources.go, internal/daemon/session_work_sources_test.go
Goal ancestry checks use hierarchy parents. Child-work discovery excludes sessions whose lineage parent differs from the queried session. Tests cover both behaviors.
Web session tree
web/src/systems/session/lib/session-hierarchy.ts, web/src/systems/session/lib/__tests__/session-hierarchy.test.ts
The session tree treats continued and forked sessions as roots. Tests verify that spawned sessions nest under loaded parents.

Derived-session runtime selection

Layer / File(s) Summary
Derive and prepare runtime selection
internal/session/manager_types.go, internal/session/derive_prepare.go, internal/session/manager_create_prepare.go
Runtime derivation applies the target agent’s default speed when a runtime or route is selected. Create preparation normalizes the selected runtime and adds its selection and revision to the start specification.
Runtime selection coverage
internal/session/manager_derive_test.go, web/src/systems/session/hooks/__tests__/use-session-prompt-runtime.test.tsx
Tests cover selected runtime state for continued and forked sessions, retention through restart and resume, provider binding, and precedence over agent defaults in the prompt hook.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 854fd

No actionable issue remains in the reviewed change; it is ready to merge after normal checks.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.06% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 13 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes both main changes: applying the selected runtime to continued sessions and keeping continue/fork sessions top-level.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit 854fd0e.

@pedronauck pedronauck left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #702 review

Verdict: approve — 0 blocking findings.

Reviewed 0f55cc51535da12c13abdd8abe45a060050e22b6...854fd0eef3d03c528939d90dbf513078ce0c59d3 (main...HEAD). The remote PR head matched the local reviewed commit before publication. Acceptance contract: /tmp/herdr-packets/p2.md.

Findings

No actionable correctness or regression findings in the reviewed diff.

  • Runtime: the derive path now persists the chosen runtime as the existing selected-runtime record at revision 1. I traced it through accepted logical creation, metadata persistence, resume, prompt normalization, runtime binding, and composer hydration. The route-binding predicate continues to recognize the chosen provider/model; fork selection comes from the source runtime. The new focused tests exercise restart/resume and the first runtime-less prompt, rather than only create-time fields.
  • Hierarchy: the existing typed lineage distinguishes Continue/Fork provenance from nesting without dropping source metadata. The shared Web tree, active-child supervision, and Goal descendant authorization exclude these edges. Ordinary spawned/provenance/recovery children keep their existing treatment. Archive and deletion operate on individual session targets; the change introduces no cascade over provenance edges. Public parent/root catalog filters intentionally retain their stored-field semantics, documented in the PR and accompanying docs.
  • Compatibility: no persisted or public DTO shape changes were introduced. Existing typed Continue/Fork rows immediately receive the corrected placement. Existing source links and runtime selection fields remain compatible. No migration/codegen change is necessary for these changes.
  • Scope and conventions: all seven changed production source files remain below 500 lines. Existing tests were extended without weakened assertions. The new daemon suite owns the active-child work-source behavior. QA scenarios, the cross-surface audit, lifecycle docs, and the official skill reference were updated.
  • Reference research: spot-checked the named local reference sources. t3code distinguishes subagent relationships in its sidebar, synara gives handoffs a null hierarchy parent, and monocode models handoffs separately from subagent transcript blocks. The adopted typed-edge approach fits the existing Compozy representation.

Focused checks performed

  1. go test ./internal/session ./internal/store ./internal/daemon -run 'TestContinueSession|TestForkSession|TestForkAccountInheritance|TestHierarchyParentSessionID|TestSessionWorkSourcesChildren|TestDaemonGoalCommandHandlerShouldExecuteCanonicalSessionLifecycle' -count=1
    • PASS: session (6.089s), store (0.020s), daemon (1.018s).
    • Log: /tmp/herdr-packets/checks-702-go.log.
  2. bunx turbo run test --filter=compozy-web -- src/systems/session/lib/__tests__/session-hierarchy.test.ts src/systems/session/hooks/__tests__/use-session-prompt-runtime.test.tsx
    • PASS: 2 files, 16 tests. Turborepo's required codegen-check dependency also passed.
    • Log: /tmp/herdr-packets/checks-702-web.log.
  3. Read the PR body, full changed-file diff, applicable root/internal/web/site instructions, SD-013, and adjacent runtime/hierarchy consumers. Verified the working tree remained clean.

Limits and delivery status

  • This is a focused code-review approval, not a claim that release/CI gates or scenario QA have passed. No make gate, gate-full, full suite, E2E, or live-provider run was performed. The changed scenarios explicitly remain pending/untested.
  • As disclosed in the PR, previously created unbound derived sessions are not backfilled with a selected runtime; the selection fix applies to new derives. Stored parent/root filter semantics remain unchanged.
  • The historical .compozy/tasks/session-continue-fork/ spec artifacts were unavailable in this checkout; review used the supplied acceptance packet, current code/tests, and PR's documented rationale.
  • Controller handoff is blocked by the environment: test "${HERDR_ENV:-}" = 1 failed. The Herdr skill explicitly prohibits controlling a Herdr session from outside Herdr, so no pane command was sent.

No product files were edited and no commits were created or pushed.

@greptile-apps

greptile-apps Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Changes how sessions nest in the UI and authorization logic.

The PR appears safe to merge; no actionable issues were found.

What we checked:

  • Source agents cannot cross the boundary: The server walks each parent link through HierarchyParentSessionID. Continue/fork links return no parent, so the walk stops before reaching the source.
  • Missing models still get defaults: The bind resolver resolves the agent and provider first. If the model remains unset, it fills the runtime choice from runtime.agent.Model.
Summary

This PR saves the runtime chosen for new continued and forked sessions and separates their source history from parent-child behavior.

  • New derived sessions record SelectedRuntime at revision 1.
  • Continue/fork rows become top-level without removing their source metadata.
  • Continue/fork sessions no longer keep their source active or grant its agent Goal access.
  • Focused tests cover runtime selection, restart, hierarchy, supervision, and Goal access.

No actionable issues were found. Tests were inspected, not executed during this review.

Acknowledged scope:

  • pedronauck explicitly deferred full gates, E2E, and Storybook under the controller’s instructions.
  • pedronauck explicitly left recovery nesting, inherited root/depth, and older unbound sessions unchanged and described their scope and compatibility reasons.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Derive[Continue or Fork] --> Selection[Save chosen runtime at revision 1]
  Selection --> Composer[Composer reads selected runtime]
  Selection --> Bind[First prompt binds selected runtime]
  Derive --> Lineage[Keep source ID and lineage kind]
  Lineage --> Boundary{Continue or Fork?}
  Boundary -->|Yes| Root[Show as top-level session]
  Boundary -->|Yes| Isolate[Exclude source child-work and Goal access]
  Boundary -->|No| Child[Keep parent-child behavior]
Loading

Reviews (1) · Last reviewed commit: "fix: keep continued and forked sessions ..."

@pedronauck
pedronauck merged commit ec14723 into main Oct 7, 2026
48 of 50 checks passed
@pedronauck
pedronauck deleted the fix-continue-agent branch October 7, 2026 02:42

This branch was successfully deployed

1 active deployment
Preview — 854fd0ee Deployed Oct 6, 2026 by vercel[bot]
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