Skip to content

feat(sessions): restore agent sessions through one "after restart" model - #82

Closed
BIackFIame wants to merge 2 commits into
howdeploy:mainfrom
BIackFIame:core/1-session-model
Closed

BIackFIame wants to merge 2 commits into
howdeploy:mainfrom
BIackFIame:core/1-session-model

Conversation

@BIackFIame

@BIackFIame BIackFIame commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Goal

Replace "Windows after restart" with one model: Don't save, Reopen windows or Continue conversations. Every card comes back to its own conversation, and later plugin extension points get a place to keep per-card state.

What changes

  • This keeps fix(terminal): restore Codex cards to their own conversations #80's Codex id capture, validated id per card, codex resume <id> and picker fallback, and extends the same exact resume to Claude Code (--resume <id>) and OpenCode (--session <id>). There is one per-provider id check (normalizeThreadId), used by the hook client, the gateway, the store and the launch.
  • Settings v21: the old boolean migrates true to continue and false to off. Session records are v2 and v1 stays readable. A record saves the last state, the thread id, a restore flag, and two validated opaque plugin slots (launch options and an environment ref, 4 KB each). It saves no scrollback, prompts or secrets.
  • Restore brings parents back before children. It uses "latest in this folder" only when that CLI has one card in the folder; otherwise it starts fresh with a note. Finished cards come back stopped with Restart / Continue. A card whose environment is unavailable stays stopped with the reason and never runs locally. The card menu gets "Don't restore this card".

Extension point for plugins

The v2 slots (options[pluginId], environment) are where #84 saves launch options and #85 saves environment refs, so a restored card can be prepared or placed again.

Tests and checks

  • tests/session-restore-v2.test.mjs and the updated store, gateway and launch tests. Suite 894/894 and typecheck with a fake HOME.
  • Live, with hidden windows and a fake Claude that emits UUID ids:
    • the old boolean migrated to continue;
    • a card with an id resumed --resume <uuid>;
    • two no-hook cards in one folder started fresh with a note, and a solo card used --continue;
    • a finished card came back stopped, and Continue resumed its own id;
    • a skipped card stayed gone;
    • Reopen started everything fresh, and Off cleared the store.

Dependency

Stacked on #80 (teo-nex). Review only the top commit; the commit below it is #80. Note for #80: the field is renamed to threadId, and v1 codexThreadId is still read.

Used by canvastty-plugin-environments (restores worktree and container cards in place) and canvastty-plugin-accounts (saved account choice).

@howdeploy

Copy link
Copy Markdown
Owner

One correction is needed before merging, based on static review of 437f4347:

P2 — Reopen windows starts a fresh conversation but retains the previous conversation ID.

planSessionRestore() returns launch: null for a running card in reopen mode, so the agent starts without resume arguments. However, TerminalManager.ts:688 unconditionally copies descriptor.threadId into the new managed session, and restorePersistedSessions() then persists it again.

Until a lifecycle hook reports the new conversation ID, the card still points to its previous conversation. With lifecycle hooks disabled or unavailable, that stale ID can survive indefinitely. After the fresh process exits, choosing Continue can therefore resume the old conversation rather than the one that just ran. Switching the restore setting to Continue conversations can also persist the stale association for the next application restart.

Please clear the previous threadId when the restore plan actually starts a fresh conversation. Preserve it when resuming the recorded conversation and for stopped cards that still need their ID for Continue, including cards held because their environment is unavailable.

Please extend the existing Reopen regression to cover the saved state as well as launch arguments:

  1. Save a running card with conversation ID A.
  2. Restore it in reopen mode, with no new lifecycle ID reported.
  3. Verify both the initial launch and the persisted record contain no association with A.
  4. Exit the fresh process and choose Continue; verify A is not passed as the resume target.

The dependency on #80 looks consistent: #82 contains its exact commit, preserves Codex's exact-resume/picker behavior, and reads v1 codexThreadId records through the migration. This finding concerns the new Reopen path, not a conflict between the two PRs.

I have not run the tests or reproduced this in the UI; the finding follows from the restore plan, session initialization, persistence, and Continue code paths.

Builds on howdeploy#80 (teo-nex, "restore each Codex card to its own
conversation"): its capture of the conversation id from authenticated
lifecycle hooks, the validated id saved per card, `codex resume <id>`,
the resume picker when no id is known and a plain restart forgetting
the id are kept as they are. This extends the same exact resume to
Claude Code (`claude --resume <id>`) and OpenCode (`opencode --session
<id>`); the field is renamed from codexThreadId to threadId for that,
with one per-provider check (canonical UUID for Codex and Claude, `ses_`
id for OpenCode) shared by the hook client, the gateway, the store and
the launch, and v1 records' codexThreadId still read.

Settings → General now offers Don't save / Reopen windows / Continue
conversations (settings v21; the old opt-in boolean migrates
true→continue, false→off). Session records move to v2 (v1 stays
readable): last state at quit or exit, the thread id, a per-card restore
flag, and two validated opaque plugin slots (launch options and an
environment ref, 4 KB each). No scrollback, prompts or secrets are saved.

Restore puts parents before children, resumes a recorded conversation
by id, and without one uses a "latest in this folder" flag only when
that CLI has one card in the folder (otherwise it starts fresh with a
note on the card; Codex opens its picker). Finished cards come back
stopped with Restart / Continue (Continue resumes the card's own
conversation), and a card whose environment is unavailable is held
stopped with its reason instead of running locally. Cards get an
options menu with "Don't restore this card".

For plugins: the v2 record's two opaque slots are where later extension
points keep per-card state across restarts. A launch contributor's chosen
options are saved in `options[pluginId]` and an environment's ref in
`environment`, both validated and capped at 4 KB, so a restored card can
be prepared or placed again (or held stopped with a reason) without the
core knowing what the values mean.
@BIackFIame

Copy link
Copy Markdown
Contributor Author

Thanks, confirmed and fixed in c1d603a.

Root cause. The restore plan decided how a card starts, but not which conversation it stays tied to. restorePersistedSession() then copied descriptor.threadId into every restored session, so a fresh start kept the old id and persistSessions() saved it again.

Fix.

  • RestoreStep in sessionRestorePlan.ts now carries a threadId: the recorded id for stopped and held cards (finished, environment unavailable, and in feat(plugins): let plugin services contribute to agent launches #84 plugin unavailable), the resumed id for an exact resume, and none for a fresh start (Reopen windows, or Continue without an exact resume).
  • TerminalManager.restorePersistedSession() takes the plan's id when the card actually starts. A card that does not start keeps its recorded id for Continue, for example a missing folder or a CLI that fails to spawn.
  • Persistence therefore saves no stale id, including after switching the setting to Continue conversations.

Regression. The Reopen test in tests/session-restore-v2.test.mjs now covers the four steps:

  1. It saves a running card with conversation A.
  2. It restores that card in reopen mode, with no new lifecycle id reported.
  3. It checks that neither the launch arguments nor the saved record mention A, both before and after switching to Continue conversations.
  4. It exits the fresh process and chooses Continue, then checks that A is not the resume target.

The plan test and the held-card test also check that stopped and held cards keep A. The new assertions fail on the previous commit.

Other paths checked.

The stacked PRs #83–#88 are rebased onto this commit. Each is still one commit, and typecheck and the full test suite pass on every branch.

@howdeploy

Copy link
Copy Markdown
Owner

Consolidated into #88 at the maintainer's request. Its branch already includes this implementation (the #81 authentication fix is incorporated through #87). Please continue all follow-up fixes and discussion in #88. Detailed changes-requested review: #88 (review) . Closing this superseded PR preserves its branch, commits and authorship; no code is being merged into main.

@howdeploy howdeploy closed this Sep 27, 2026
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.

3 participants