Skip to content

fix(cli): preserve sessions across ao stop → ao update → ao start (closes #1743) - #1780

Closed
suraj-markup wants to merge 1 commit into
mainfrom
fix/1743-session-restore-after-update
Closed

fix(cli): preserve sessions across ao stop → ao update → ao start (closes #1743)#1780
suraj-markup wants to merge 1 commit into
mainfrom
fix/1743-session-restore-after-update

Conversation

@suraj-markup

Copy link
Copy Markdown
Collaborator

Summary

Closes #1743.

The ao stopao updateao start flow was silently dropping the restore prompt because last-stop.json could be missing from disk by the time ao start checked for it. Three layered fixes harden the path:

Root cause

ao stop (and the SIGTERM shutdown handler in lib/shutdown.ts) only wrote last-stop.json AFTER finishing the kill loop. If the CLI was killed mid-shutdown — SIGKILL, hard crash, or an impatient re-run — the record was never written and ao start had nothing to restore.

The atomic write also went through writeFileSync + renameSync without an explicit fsync, so a hard kill that landed immediately after the call could lose the file's data even though the rename had committed the dirent.

Fix

  1. Pre-write before kill, with fsync. ao stop and the shutdown handler now write last-stop.json with every active session id BEFORE running the kill loop, then reconcile (rewrite or clearLastStop) once the loop's actual results are known. The new atomicWriteFileSyncDurable calls fsyncSync on the temp file before renameSync, so the bytes are durable even if the process is killed milliseconds later.
  2. Fallback in ao start. When last-stop.json is missing or empty, scan recently manually_killed sessions (terminated within the last 10 minutes) via findRecentlyKilledSessions and synthesize a LastStopState. The restore prompt fires with the same wording as the existing record. This protects against future regressions in the write pipeline.
  3. Regression test that ao update does NOT touch last-stop.json. The current ao update command never reads or writes anything under ~/.agent-orchestrator/, but adding state-clearing logic in the future would re-introduce the bug. The new test stages a last-stop.json in a temp HOME and asserts it is byte-for-byte identical after ao update runs.

Before vs. after of the file-write timing

Before:                                     After:
sm.list()                                   sm.list()
filter active sessions                      filter active sessions
for s in active: kill(s) ─┐                 writeLastStop(active)        ◄── pre-write, fsync'd
write last-stop.json      │                 for s in active: kill(s) ─┐
killProcessTree(parent) ◄─┘ races with      reconcile last-stop.json  │
                            shutdown.ts     killProcessTree(parent) ◄─┘

If the CLI is killed between kill(s) and write last-stop.json in the old order, the record vanishes. The new order moves the durable write before any teardown work.

Tests added

  • packages/cli/__tests__/lib/last-stop-fallback.test.ts (5) — unit tests for buildLastStopFallback: window respect, primary vs other-project routing, missing/unparseable timestamps, custom window
  • packages/cli/__tests__/lib/stop-update-start-flow.test.ts (2) — hermetic stop → simulated update → start flow against a temp HOME, plus fallback path
  • writeLastStop fsyncs the temp file before renaming in running-state.test.ts
  • does not touch ~/.agent-orchestrator/last-stop.json in update.test.ts
  • pre-writes last-stop.json before the kill loop runs in start.test.ts
  • clears last-stop.json when every kill fails in start.test.ts
  • falls back to recently manually-killed sessions when last-stop.json is missing in start.test.ts
  • does not surface fallback candidates older than the recent window in start.test.ts

pnpm typecheck and pnpm lint pass. pnpm test shows the same 10 pre-existing failures on this branch as on main; my 7 new test cases all pass.

Test plan

  • pnpm typecheck clean
  • pnpm lint no errors (warnings only, all pre-existing)
  • pnpm exec vitest run lib/last-stop-fallback lib/stop-update-start-flow lib/running-state — all green
  • pnpm exec vitest run commands/start.test.ts -t "issue #1743|every kill fails" — 4 pass
  • pnpm exec vitest run commands/update.test.ts — 26 pass (including new regression)
  • Manual: ao stopao updateao start on a checkout with active sessions reproduces the restore prompt

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

Test Coverage Report

Metric Value
Lines covered 1361/1874
Lines not covered 513/1874
Overall coverage 72.6%
Per-file breakdown
File Coverage
packages/cli/src/commands/start.ts 1128/1549 (72.8%)
packages/cli/src/lib/last-stop-fallback.ts 60/62 (96.8%)
packages/cli/src/lib/running-state.ts 173/263 (65.8%)

Uncovered lines

  • packages/cli/src/commands/start.ts: L138-L139, L150-L152, L155-L157, L159, L161-L165, L168-L169, L171, L173-L177, L179-L186, L188-L190, L230-L231, L236-L254, L256-L263, L265-L266, L272-L278, L280, L311-L312, L326-L328, L337-L338, L341-L365, L410-L424, L426-L429, L431-L445, L473, L484, L508-L509, L512-L513, L521-L522, L529-L534, L572-L573, L597-L600, L614-L617, L627-L628, L635-L645, L648-L649, L673-L674, L677-L681, L689-L696, L703-L704, L741-L744, L759-L764, L781-L783, L785-L792, L794-L796, L798-L803, L805-L806, L917-L920, L967, L1000-L1019, L1132-L1134, L1206-L1207, L1216-L1217, L1219-L1220, L1223-L1230, L1232, L1280-L1285, L1318-L1319, L1325-L1326, L1350-L1354, L1359-L1364, L1367-L1372, L1379-L1385, L1387-L1394, L1396-L1402, L1451, L1471-L1472, L1577-L1578, L1630-L1647, L1711, L1745-L1747, L1770-L1777, L1804-L1812, L1833-L1834, L1868-L1869, L1877-L1888, L1891, L1919, L1925-L1926, L1929-L1937, L1947-L1948, L1957-L1958, L2005-L2007, L2036-L2038, L2116-L2121, L2151-L2162, L2195-L2196
  • packages/cli/src/lib/last-stop-fallback.ts: L120-L121
  • packages/cli/src/lib/running-state.ts: L59-L60, L74-L75, L78-L85, L98-L99, L133-L136, L169-L170, L176, L201-L208, L216-L227, L238-L248, L262-L276, L288-L290, L304-L311, L341-L343, L374, L383-L390

@greptile-apps

greptile-apps Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR hardens the ao stop → ao update → ao start restore flow by addressing the three root causes of a silently dropped restore prompt (issue #1743): an fsync-backed atomic write in atomicWriteFileSyncDurable, a fallback session scan (findRecentlyKilledSessions) when last-stop.json is absent, and an empty-sentinel strategy (markLastStopAcknowledged) that prevents the fallback from re-prompting after the user has already acted.

  • running-state.ts: replaces atomicWriteFileSync with atomicWriteFileSyncDurable (fsync before rename) and adds markLastStopAcknowledged which writes sessionIds: [] so readLastStop returns a non-null record and blocks the !lastStop fallback gate on subsequent starts.
  • last-stop-fallback.ts (new): buildLastStopFallback scans all sessions, filters to manually_killed within a 10-minute window, groups by project into primary/other, and returns a synthesized LastStopState; findRecentlyKilledSessions wraps it with error-swallowing so it never blocks startup.
  • start.ts: adds the fallback branch (using the global config to include cross-project sessions) and replaces every clearLastStop() call with markLastStopAcknowledged(lastStop.projectId) to leave the sentinel on disk.

Confidence Score: 5/5

Safe to merge — the three root-cause fixes (durable fsync write, fallback scan, empty-sentinel acknowledgment) are all correctly implemented and the previously-flagged issues from prior review rounds have been resolved.

All control paths that call markLastStopAcknowledged are guarded by the outer if (lastStop && totalLastStopSessions > 0) check, so lastStop is never null at those call sites. The empty sentinel is correctly readable by readLastStop (passes all validation) yet produces totalLastStopSessions === 0, which prevents re-prompting and closes the fallback gate. The global config is now used for the fallback session manager, resolving the cross-project session visibility gap. Temp-file cleanup on write failure is handled in the finally block, and the fsync path is verified by new tests. No correctness gaps remain in the changed files.

No files require special attention.

Important Files Changed

Filename Overview
packages/cli/src/lib/running-state.ts Adds atomicWriteFileSyncDurable (fsync before rename for durability) and markLastStopAcknowledged (empty sentinel to block fallback re-prompts); temp-file cleanup on write failure is correct
packages/cli/src/lib/last-stop-fallback.ts New module: buildLastStopFallback synthesizes a LastStopState from recently manually-killed sessions; findRecentlyKilledSessions wraps it with sm.list() and silences errors; window filtering and project grouping are correct
packages/cli/src/commands/start.ts Replaces clearLastStop with markLastStopAcknowledged on all decision paths and adds the fallback scan guarded by !lastStop; all markLastStopAcknowledged calls occur inside if (lastStop && totalLastStopSessions > 0) so lastStop is never null there
packages/cli/tests/lib/last-stop-fallback.test.ts New unit tests for buildLastStopFallback covering window filtering, primary/other-project routing, bad timestamps, and custom window; comprehensive
packages/cli/tests/lib/running-state.test.ts Adds fsync-call verification, temp-file cleanup-on-failure test, otherProjects round-trip, and markLastStopAcknowledged sentinel test; node:fs mock correctly intercepts fsyncSync/writeFileSync without breaking other tests
packages/cli/tests/commands/start.test.ts Adds mock for markLastStopAcknowledged, renames the acknowledge-vs-unlink test, and adds four new scenarios: no-re-prompt after decline, fallback fires when missing, fallback respects window, and fallback uses global config for cross-project sessions
packages/cli/tests/commands/update.test.ts Adds regression test that ao update does not touch last-stop.json by staging a real file in a temp HOME and asserting byte-for-byte identity after the update command runs
packages/cli/tests/lib/stop-update-start-flow.test.ts New integration test exercising the full stop→simulated-update→start round-trip and the fallback path against a temp HOME; correctly isolates os.homedir() via vi.mock

Reviews (7): Last reviewed commit: "fix(cli): preserve sessions across ao st..." | Re-trigger Greptile

Comment thread packages/cli/src/lib/running-state.ts
Comment thread packages/cli/src/commands/start.ts Outdated
@harshitsinghbhandari

Copy link
Copy Markdown
Collaborator

Hey @suraj-markup, thanks for the layered fix.

Before approving I'd like a deterministic repro on fresh main. I ran the issue's exact flow (ao stopao updateao start) on latest upstream/main (macOS) and the restore prompt fires for me — await writeLastStop(...) in registerStop (packages/cli/src/commands/start.ts:1781) completes before the SIGTERM to the parent, so the file is on disk.

Why I'm asking:

  1. The pre-write before kill change in registerStop sits inside the inner try/catch — if writeLastStop throws (lock contention, disk full, etc.), the outer catch fires and the kill loop gets skipped. That's a behavior change vs main (where a write failure didn't block kills), and worth confirming it's solving a real path before we ship it.
  2. Without a hard repro I can't tell whether the pre-write + reconcile state machine actually closes the gap, or whether the fallback in ao start is doing all the work on its own. The fallback is unambiguously safe; the pre-write + reconcile is a much bigger surface for a scenario nobody can hand-trigger.
  3. The original report's running.json showed "projects": [] at the time of the curl — which hints the new ao start hadn't finished registering yet. Wondering if some of what looked like "missing restore prompt" was actually a separate timing symptom.

If the race is hard to trigger by hand, an instrumented test that exercises the SIGKILL / mid-shutdown window would work just as well. Happy to approve the fallback piece on its own in the meantime if that helps unblock.

suraj-markup added a commit that referenced this pull request May 10, 2026
)

PR #1780 review (Harshit): the `ao stop` CLI is a separate process from
the `ao start` parent and `await writeLastStop(...)` completes before
the CLI sends SIGTERM to the parent — so the original race the
pre-write+reconcile branch was guarding against is not reproducible by
hand on main. The fallback in `ao start` is doing the user-visible work.

Drop the higher-surface pre-write+reconcile changes; keep what is
unambiguously safer or independently useful:

- Keep `last-stop-fallback.ts` + its integration in runStartup so a
  missing/malformed last-stop.json still surfaces a restore prompt.
- Keep the fsync'd `atomicWriteFileSyncDurable` so the write survives a
  hard kill that lands immediately after rename.
- Also fix Greptile P2: `atomicWriteFileSyncDurable` now unlinks the
  temp file on `writeFileSync`/`fsyncSync` throw (previously leaked).
- Revert `registerStop` and the SIGTERM shutdown handler to their
  pre-PR post-kill write shape — same as origin/main.
- Drop the two pre-write ordering tests; this removes the Greptile P2
  unreachable `clearLastStop` branch naturally.

Tests retained / added:
- last-stop-fallback unit tests (5)
- stop → simulated update → start integration test (2)
- writeLastStop fsync test
- NEW: writeLastStop leaves no temp file on write-throw
- ao update no-touch regression test

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@suraj-markup

Copy link
Copy Markdown
Collaborator Author

Thanks @harshitsinghbhandari — all three concerns are fair and I agree with the downsize. Pushed 72beb03.

On the repro (your #1 and #2). You're right. The ao stop CLI is a separate process from ao start, so await writeLastStop(...) returns before this CLI sends SIGTERM to the parent — the file is on disk before the parent dies. The only ways the pre-write+reconcile branch closed a real gap were (a) the ao stop CLI itself getting SIGKILL'd between the kill loop and the writeLastStop call, or (b) an fsync race where the parent dies before the kernel flushes data blocks. Neither is hand-triggerable, and I couldn't write an instrumented test that wasn't just exercising the mock. So per your suggestion in #3b, I dropped that surface.

What's in the new commit:

  • registerStop and the SIGTERM shutdown handler — reverted to the original post-kill write shape (same as origin/main).
  • Pre-write ordering tests in start.test.ts — dropped (with them, your behavior-change concern in feat: implement web dashboard with attention-zone UI and API routes #1 also goes away naturally: a writeLastStop throw can no longer block the kill loop, because the pre-write doesn't exist).
  • Greptile P2 unreachable clearLastStop branch — gone with the reconciliation code.

Kept (unambiguously safer or independently useful):

  • last-stop-fallback.ts + the runStartup integration — your defense-in-depth point.
  • atomicWriteFileSyncDurable with fsync in running-state.ts — durability is strictly safer; not behavior-changing in any code path either of us cares about.
  • Greptile P2 feat: implement web dashboard with attention-zone UI and API routes #1 fix — temp file is now unlinked on writeFileSync/fsyncSync throw (previously leaked). Test added in running-state.test.ts.
  • ao update no-touch regression test (update.test.ts).
  • stop → simulated update → start integration test (stop-update-start-flow.test.ts).

On your #3 (projects: [] in running.json). Strong hint; not investigating in this PR since it's a separate timing symptom and the fallback covers the user-visible regression either way. Happy to file a follow-up issue if you'd like.

Net diff vs your last review: −238 / +83 across 5 files. pnpm typecheck clean; same 4 pre-existing failures on this branch as on the prior commit (none in the files touched here).

Comment thread packages/cli/src/commands/start.ts Outdated
suraj-markup added a commit that referenced this pull request May 10, 2026
…surface (#1743)

Greptile P1 on PR #1780. `getSessionManager(config)` in the fallback
path was built from the current project's config, so `sm.list()` only
saw that project's sessions. When `ao stop` ran globally and killed
sessions across multiple projects, the synthesized `LastStopState`
would have an empty `otherProjects` — defeating the cross-project
restore that the pre-existing `readLastStop` path already supports
(because `ao stop` writes the cross-project rows at stop time).

Mirror the global-config load that the restore step on line ~1002
already does: if the global config exists, prefer it when constructing
the fallback session manager. The downstream restore code already
loads the global config when `otherProjects` is non-empty, so this
just lets the fallback populate that array in the first place.

Regression test in start.test.ts asserts both the in-project and a
cross-project session get routed to `sm.restore()` after the fallback
synthesizes a record from a global-scope `sm.list()`. The two existing
fallback tests now pin AO_GLOBAL_CONFIG to a non-existent path so they
don't accidentally read the host's real ~/.agent-orchestrator/config.yaml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@suraj-markup

Copy link
Copy Markdown
Collaborator Author

Greptile P1 (fallback session manager using project-scoped config) → fixed in 1339d9c. The fallback now loads the global config (mirroring the restore step at ~start.ts:1002) before constructing the session manager, so cross-project sessions are surfaced into otherProjects and routed through the normal restore path. Regression test in start.test.ts: fallback uses the global config so cross-project sessions appear in otherProjects (PR #1780) — asserts both an in-project and a cross-project session get sm.restore() after a global-scope sm.list(). The two existing fallback tests are now pinned to a non-existent AO_GLOBAL_CONFIG so they don't read the host's real ~/.agent-orchestrator/config.yaml. Thread resolved.

suraj-markup added a commit that referenced this pull request May 14, 2026
)

PR #1780 review (Harshit): the `ao stop` CLI is a separate process from
the `ao start` parent and `await writeLastStop(...)` completes before
the CLI sends SIGTERM to the parent — so the original race the
pre-write+reconcile branch was guarding against is not reproducible by
hand on main. The fallback in `ao start` is doing the user-visible work.

Drop the higher-surface pre-write+reconcile changes; keep what is
unambiguously safer or independently useful:

- Keep `last-stop-fallback.ts` + its integration in runStartup so a
  missing/malformed last-stop.json still surfaces a restore prompt.
- Keep the fsync'd `atomicWriteFileSyncDurable` so the write survives a
  hard kill that lands immediately after rename.
- Also fix Greptile P2: `atomicWriteFileSyncDurable` now unlinks the
  temp file on `writeFileSync`/`fsyncSync` throw (previously leaked).
- Revert `registerStop` and the SIGTERM shutdown handler to their
  pre-PR post-kill write shape — same as origin/main.
- Drop the two pre-write ordering tests; this removes the Greptile P2
  unreachable `clearLastStop` branch naturally.

Tests retained / added:
- last-stop-fallback unit tests (5)
- stop → simulated update → start integration test (2)
- writeLastStop fsync test
- NEW: writeLastStop leaves no temp file on write-throw
- ao update no-touch regression test

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
suraj-markup added a commit that referenced this pull request May 14, 2026
…surface (#1743)

Greptile P1 on PR #1780. `getSessionManager(config)` in the fallback
path was built from the current project's config, so `sm.list()` only
saw that project's sessions. When `ao stop` ran globally and killed
sessions across multiple projects, the synthesized `LastStopState`
would have an empty `otherProjects` — defeating the cross-project
restore that the pre-existing `readLastStop` path already supports
(because `ao stop` writes the cross-project rows at stop time).

Mirror the global-config load that the restore step on line ~1002
already does: if the global config exists, prefer it when constructing
the fallback session manager. The downstream restore code already
loads the global config when `otherProjects` is non-empty, so this
just lets the fallback populate that array in the first place.

Regression test in start.test.ts asserts both the in-project and a
cross-project session get routed to `sm.restore()` after the fallback
synthesizes a record from a global-scope `sm.list()`. The two existing
fallback tests now pin AO_GLOBAL_CONFIG to a non-existent path so they
don't accidentally read the host's real ~/.agent-orchestrator/config.yaml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@suraj-markup
suraj-markup force-pushed the fix/1743-session-restore-after-update branch from 1339d9c to 54f36ca Compare May 14, 2026 22:36
Comment thread packages/cli/src/commands/start.ts Outdated
suraj-markup added a commit that referenced this pull request May 14, 2026
…prompt (#1743)

Greptile P1 on PR #1780. After the user declined a restore (or after a
successful restore), `clearLastStop()` unlinked last-stop.json. On the
next `ao start` within 10 minutes, `readLastStop()` returned null, the
fallback gate `!lastStopHasContent` fired, and the same recently
manually_killed sessions were surfaced again — re-prompting after a
decline.

Fix:

- Add `markLastStopAcknowledged(projectId)` in running-state.ts: writes
  a valid LastStopState with empty `sessionIds`. The file is present
  on disk but has no content, so the restore prompt does not fire.
- Tighten the fallback gate in runStartup from `!lastStopHasContent`
  to `!lastStop` (file truly absent). The empty marker keeps the gate
  closed across subsequent `ao start` runs.
- Replace the four `clearLastStop()` call sites in the prompt path
  (declined, all-restored, partial-all-resolved, empty-restore-list)
  with `markLastStopAcknowledged(lastStop.projectId)`.
- `clearLastStop` stays exported because tests still observe it (and
  no current code path calls it after this change — removing the
  export would be a separate cleanup).

Tests:

- NEW: `does not re-prompt with fallback after user declines (PR #1780)`
  in start.test.ts — primes readLastStop with an empty marker, leaves
  a recent manually_killed session in sm.list(), asserts neither
  promptConfirm nor sm.restore was called.
- NEW: `markLastStopAcknowledged writes a present but empty marker
  readable by readLastStop` in running-state.test.ts.
- UPDATED: the "every session restored successfully" test now expects
  `markLastStopAcknowledged` instead of `clearLastStop`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@suraj-markup

Copy link
Copy Markdown
Collaborator Author

Greptile P1 (fallback re-prompts after user declines) → fixed in 7d94ba3.

New helper markLastStopAcknowledged(projectId) in running-state.ts writes an empty but present last-stop.json after any user decision (declined, all restored, etc.). The fallback gate in runStartup is tightened from !lastStopHasContent to !lastStop (file truly absent), so an empty marker keeps the fallback dormant across subsequent ao start runs.

Replaced all four clearLastStop() call sites in the prompt path with the new helper. The next ao stop overwrites the marker with real session IDs, so the legitimate flow is unchanged.

Regression test in start.test.ts: does not re-prompt with fallback after user declines (PR #1780) — primes readLastStop with an empty marker, keeps a recent manually_killed session in sm.list(), asserts neither promptConfirm nor sm.restore is invoked. Also updated the existing "every session restored successfully" test to assert the new helper, and added a round-trip test in running-state.test.ts. Thread resolved.

suraj-markup added a commit that referenced this pull request May 19, 2026
)

The ao stop → ao update → ao start flow could drop the restore prompt
when last-stop.json went missing on disk by the time ao start looked
for it. Two layered fixes plus a cross-project follow-up:

1. ao start falls back to scanning recently `manually_killed` sessions
   (terminated within 10 minutes, reason=manually_killed) when
   last-stop.json is absent. The restore prompt surfaces them with the
   same UX as the existing record so a regression in the write
   pipeline cannot silently drop the user's in-flight work. The
   fallback uses the GLOBAL config so cross-project sessions appear in
   `otherProjects` (mirrors the existing restore step's global-config
   load).

2. writeLastStop now uses an fsync'd atomic write (atomicWriteFileSyncDurable)
   so the record survives a hard kill that lands immediately after the
   rename — renameSync is atomic but the data blocks aren't durable
   until fsync. The temp file is also unlinked on writeFileSync /
   fsyncSync throw (Greptile P2 #1 follow-up).

3. After any user decision on a restore prompt (declined / all-restored /
   empty), runStartup writes an empty marker via the new
   markLastStopAcknowledged() helper instead of unlinking the file.
   The fallback gate is tightened from `!lastStopHasContent` to
   `!lastStop` (file truly absent) so a second ao start within the
   10-minute window does not re-surface sessions the user already
   declined (Greptile P1 review on PR #1780).

Tests:
- last-stop-fallback unit tests (5)
- stop → simulated update → start integration test (2)
- writeLastStop fsync test (1)
- writeLastStop temp-file cleanup on write throw (1)
- otherProjects round-trip through readLastStop (1)
- markLastStopAcknowledged round-trip (1)
- ao update no-touch regression (1)
- start.ts: fallback finds recent killed sessions (1)
- start.ts: fallback respects window (1)
- start.ts: fallback uses global config for cross-project (1)
- start.ts: empty marker prevents re-prompt after decline (1)
- start.ts: every-session-restored writes marker (not unlink) (1)

Closes #1743

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@suraj-markup
suraj-markup force-pushed the fix/1743-session-restore-after-update branch from 7d94ba3 to cb25be9 Compare May 19, 2026 15:35
)

The ao stop → ao update → ao start flow could drop the restore prompt
when last-stop.json went missing on disk by the time ao start looked
for it. Two layered fixes plus a cross-project follow-up:

1. ao start falls back to scanning recently `manually_killed` sessions
   (terminated within 10 minutes, reason=manually_killed) when
   last-stop.json is absent. The restore prompt surfaces them with the
   same UX as the existing record so a regression in the write
   pipeline cannot silently drop the user's in-flight work. The
   fallback uses the GLOBAL config so cross-project sessions appear in
   `otherProjects` (mirrors the existing restore step's global-config
   load).

2. writeLastStop now uses an fsync'd atomic write (atomicWriteFileSyncDurable)
   so the record survives a hard kill that lands immediately after the
   rename — renameSync is atomic but the data blocks aren't durable
   until fsync. The temp file is also unlinked on writeFileSync /
   fsyncSync throw (Greptile P2 #1 follow-up).

3. After any user decision on a restore prompt (declined / all-restored /
   empty), runStartup writes an empty marker via the new
   markLastStopAcknowledged() helper instead of unlinking the file.
   The fallback gate is tightened from `!lastStopHasContent` to
   `!lastStop` (file truly absent) so a second ao start within the
   10-minute window does not re-surface sessions the user already
   declined (Greptile P1 review on PR #1780).

Tests:
- last-stop-fallback unit tests (5)
- stop → simulated update → start integration test (2)
- writeLastStop fsync test (1)
- writeLastStop temp-file cleanup on write throw (1)
- otherProjects round-trip through readLastStop (1)
- markLastStopAcknowledged round-trip (1)
- ao update no-touch regression (1)
- start.ts: fallback finds recent killed sessions (1)
- start.ts: fallback respects window (1)
- start.ts: fallback uses global config for cross-project (1)
- start.ts: empty marker prevents re-prompt after decline (1)
- start.ts: every-session-restored writes marker (not unlink) (1)

Closes #1743

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@suraj-markup
suraj-markup force-pushed the fix/1743-session-restore-after-update branch from cb25be9 to 945a7ef Compare May 21, 2026 13:16
@harshitsinghbhandari

Copy link
Copy Markdown
Collaborator

Closing as part of a backlog sweep: Agent Orchestrator was rewritten from the ground up (Go backend + Electron/React frontend), and this PR's changes target files from the pre-rewrite TypeScript codebase that no longer exist in the current tree, so it cannot be rebased onto main. If the underlying change still makes sense for the current app, please open a fresh PR against the new codebase. Thanks for the contribution!

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.

bug(cli): ao stop → ao update → ao start leaves sessions terminated, no restore prompt

2 participants