diff --git a/docs/plans/2026-09-26-undo-close-derived-identity.md b/docs/plans/2026-09-26-undo-close-derived-identity.md new file mode 100644 index 000000000..1a286d490 --- /dev/null +++ b/docs/plans/2026-09-26-undo-close-derived-identity.md @@ -0,0 +1,10 @@ +# Undo Close keeps a derived TLDR identity (#1347) + +## Problem +Undo Close respawns a closed agent with `tldrIdentity: meta.tldrIdentity`. An agent whose identity is DERIVED has no explicit field. That happens when reporting domains are on but main created the agent, or its metadata arrived after it acquired TLDR or Goal. The rule is `tldrIdentityForSession`: the identity is the session id. Undo Close passes `undefined` for such an agent, so `spawn` mints a fresh UUID: the restored agent gets a new TLDR/Goal identity, and Agent Analytics (#1342 keys by it) splits its time. Reload Agents already passes `tldrIdentityForSession(oldId, meta)`. + +## Decision +The Undo Close respawn uses `tldrIdentityForSession(closedSessionId, meta)`, the same rule as reload. `respawn` now takes the closed id; every caller has it (a single pane's entry, a project member). + +## Test +`undoCloseFailure.renderer.test.tsx`: a closed agent with a reporting domain and no explicit identity is respawned with its closed session id as `tldrIdentity`; an explicit identity is still passed through unchanged. Red on main. diff --git a/docs/plans/2026-09-27-cli-update-could-not-start.md b/docs/plans/2026-09-27-cli-update-could-not-start.md new file mode 100644 index 000000000..69bb4a547 --- /dev/null +++ b/docs/plans/2026-09-27-cli-update-could-not-start.md @@ -0,0 +1,81 @@ +# An update that cannot start says so and stays dismissable (#1425) + +Size: short plan. The root cause is known and the change is bounded +(found by #1423's review b). + +## Outcome + +When **Update Now** or an automatic update cannot start, the CLI update +banner stops showing an undismissable "Updating…" row forever. It shows a +dismissable failure in fixed words, with **Update now** to retry. A click +whose IPC call rejects is also said instead of dropped. + +## Evidence (verified 2026-09-27, do not re-derive) + +- `CliUpdateOrchestrator.runUpdate` publishes `updating`, then awaits + `openLog(cli)`, which `mkdir`s `CLI_UPDATE_LOG_DIR` outside any catch. + An EACCES or ENOSPC there rejects, and the snapshot stays `updating`. + `describeState` marks `updating` as `undismissable`. +- Every later await in `runUpdate` already swallows or returns a result: + - `appendLog` and `appendDiagnostics` catch; + - `readInstalledVersion` returns `{ ok }`; + - the command runs inside a try. + + So `openLog` is the one throw that strands the state. +- `CliUpdateBanner` fires `cliUpdatesUpdateNow` with `void` in both the + `notify` and the user-`deferred` rows. The IPC handler's rejection + (`updateOnce` throwing) is dropped, and nothing is said. +- #1423 already gave banner actions a result contract: `onClick` may + resolve `false`, and the row then shows `failureText`, keyed by + `resultKey`. +- `failed.logPath` is read only by the `cli-updates:open-log` handler and + the banner's `resultKey`. + +## Change + +- `@shared/types/cliUpdate`: + - `failed.logPath: string | null` (null = there is no log, because the + update never started); + - `CliUpdateFailureReason` adds `'could-not-start'`. +- `runUpdate`: `openLog` runs in a try. On rejection it warns the cause + in main's log (never the renderer; q22) and publishes + `failed { reason: 'could-not-start', logPath: null }`, then returns + without running the command. + - Ruling: do not run the update without a log. A state dir we cannot + write to (ENOSPC, EACCES) makes a package install likely to fail too, + and a failure with no log would leave the user nothing to look at. + Cost if wrong: one retry after the user fixes the disk. +- `cli-updates:open-log`: a `failed` state with `logPath === null` opens + nothing and answers `false`. The banner shows no View Log for it anyway. +- Banner: + - `could-not-start` reads: "Couldn't start the