Skip to content

Commit d5fb3f7

Browse files
committed
refactor(daemon): merge blockingError to state, not explain, the save-script exclusion
Following up on the comment-trimming pass already on this branch: the device-claim condition (!platformCloseError && !cleanupAggregate) and the two-line throw sequence right below it both needed a paragraph explaining why saveScriptError is excluded from one but not the other. Merge platformCloseError and cleanupAggregate into a single named blockingError — its name now states the exclusion the comment used to argue for, and the throw sequence collapses from two ifs to one. Trimmed the remaining long docblocks in this file the same way: state what's non-obvious in 1-3 lines instead of re-deriving it in prose.
1 parent bdc21e8 commit d5fb3f7

2 files changed

Lines changed: 21 additions & 68 deletions

File tree

src/daemon/__tests__/request-router-typed-error.test.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,9 @@ test('BLOCKER 2 (second follow-up): a repair-close platform-close failure surfac
169169
expect(sessionStore.get('typed-error')).toBeDefined();
170170
});
171171

172-
// #1391 P2: the handler-level test (`session-device-claims.test.ts`) proves
173-
// `toOrdinaryCloseSaveScriptFailure` builds the right `AppError`, but calls
174-
// `handleCloseCommand` directly — it never exercises `normalizeError`/
175-
// `enrichDaemonError`'s own hoisting of `details.retriable` to the TOP-level
176-
// wire field, the same layer BLOCKER 2's test above exists to catch a
177-
// regression in. Exercised through the REAL router boundary so a regression
178-
// in either layer is caught, unlike the direct-call test.
172+
// Unlike the handler-level test in session-device-claims.test.ts, this goes
173+
// through the real router boundary, so it also covers normalizeError's
174+
// details.retriable hoisting.
179175
test('#1391: an ordinary close-time script-save failure surfaces details.reason/path and retriable:false through the router, and the session is torn down', async () => {
180176
const { sessionStore, handler } = makeHandler();
181177
const session = makeIosSession('typed-error');

src/daemon/handlers/session-close.ts

Lines changed: 18 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -169,15 +169,8 @@ function toRepairPlatformCloseFailure(error: unknown): AppError {
169169

170170
type SessionCloseTeardownResult = {
171171
platformCloseError: unknown;
172-
// #1391: an ordinary (non-repair) session's close-time script write (implicit
173-
// from `open --save-script`, or this close's own `--save-script`) can refuse
174-
// to publish (no-clobber target-exists, or any other fs AppError). Unlike the
175-
// repair-armed commit (which keeps its session alive for a `--force` retry —
176-
// ADR 0012 BLOCKER 2b), an ordinary session has no transaction to retry: its
177-
// `close` already ran (or was skipped) and its teardown below must still
178-
// release the lease/device claim and delete the session, exactly as a
179-
// `platformCloseError` does not block them either. Surfaced separately so
180-
// `handleCloseCommand` can report it AFTER teardown completes, never before.
172+
// #1391: a failed close-time script write (see finalizeOrdinaryCloseScript).
173+
// Never blocks teardown — see runCloseTeardownAndRelease's blockingError.
181174
saveScriptError?: AppError;
182175
};
183176

@@ -214,22 +207,10 @@ async function runSessionCloseTeardown(params: {
214207
return { platformCloseError, saveScriptError };
215208
}
216209

217-
/**
218-
* ADR 0012 decision 6 (BLOCKER 2): only an ordinary (non-repair) session
219-
* records its finalize `close` and writes its session log here — see the
220-
* call site's own comment for why a repair-armed session skips this entirely.
221-
*
222-
* #1391: the write can refuse to publish (no-clobber target-exists, or any
223-
* other fs `AppError`). Unlike a repair-armed commit failure — which keeps
224-
* its session alive so `commitRepairBeforeClose` must roll back its
225-
* just-recorded `close` action to keep a later retry from duplicating it —
226-
* this caller (`runSessionCloseTeardown`) always completes teardown and
227-
* deletes the session regardless of the outcome returned here, so there is
228-
* no surviving session for a retry to duplicate anything on. No rollback:
229-
* the recorded `close` action (and its durable `events.ndjson` entry) stay
230-
* exactly as they are — an accurate record that the close itself happened,
231-
* independent of whether the script also got saved.
232-
*/
210+
// #1391: unlike commitRepairBeforeClose, this never rolls back the recorded
211+
// `close` action on a write failure — the caller always tears the session
212+
// down regardless, so there's no surviving session for a retry to duplicate
213+
// it on.
233214
function finalizeOrdinaryCloseScript(params: {
234215
req: DaemonRequest;
235216
session: SessionState;
@@ -254,21 +235,9 @@ function finalizeOrdinaryCloseScript(params: {
254235
}
255236
}
256237

257-
/**
258-
* #1391: normalizes an ordinary (non-repair) session's close-time script-write
259-
* failure. `SessionScriptWriter.write()` only ever throws a genuine `AppError`
260-
* here (a non-clobber refusal or another fs AppError — see
261-
* `handleSessionScriptWriteFailure`'s non-repair, non-active-publication
262-
* branch); anything else is already swallowed into a silent `{written:false}`
263-
* there. Only the message/hint/retriable are corrected for THIS call site (the
264-
* shared `publishHealedScriptAtomically` wording, "retry close --save-script",
265-
* describes the repair-commit retry contract, which does not apply here — by
266-
* the time the agent sees this, `close` has already released the device and
267-
* deleted the session, so there is nothing left to retry in place); the
268-
* original error's machine-readable `details` (e.g. `reason:
269-
* "script_target_exists"`, `path`) are preserved so a caller dispatching on
270-
* them still can.
271-
*/
238+
// #1391: the shared write error's message/details/reason are preserved
239+
// as-is; only hint/retriable are overridden, since its default "retry close
240+
// --save-script" wording no longer applies once the session is gone.
272241
function toOrdinaryCloseSaveScriptFailure(error: unknown): AppError {
273242
const overrides = {
274243
hint: 'Remove the existing target (or pass --force/--overwrite), then re-record with open --save-script.',
@@ -506,21 +475,10 @@ type SessionCloseFinalization =
506475
| { kind: 'response'; response: DaemonResponse }
507476
| { kind: 'closed'; providerData?: Record<string, unknown> };
508477

509-
/**
510-
* Everything between a settled repair decision and a success response: the
511-
* failure-isolated resource teardown, the provider lease release, and — only
512-
* once both have run — the device-claim clear and session delete. A rejected
513-
* cleanup step is collected instead of short-circuiting the rest, so every
514-
* subsequent resource (and the runner stop) is still attempted; the provider
515-
* lease is released only after that teardown, and a failed release keeps the
516-
* session retryable (returned as `{kind:'response'}`, mirroring the
517-
* repair-commit-failure path above it). The platform-close failure is thrown
518-
* as the primary error with its original code/details/hint intact; the
519-
* cleanup aggregate has already been emitted as a diagnostic by this point so
520-
* per-resource failures stay visible; a failed script save (#1391) is thrown
521-
* last since — unlike the two above it — the session has already ended and
522-
* the device is already released by the time it surfaces.
523-
*/
478+
// Everything between a settled repair decision and a success response:
479+
// failure-isolated resource teardown, provider lease release, then (only
480+
// once both are known) the device-claim clear and session delete. A failed
481+
// lease release keeps the session retryable instead (`{kind:'response'}`).
524482
async function runCloseTeardownAndRelease(params: {
525483
req: DaemonRequest;
526484
session: SessionState;
@@ -561,15 +519,14 @@ async function runCloseTeardownAndRelease(params: {
561519
phase: 'session_close_cleanup_failed',
562520
failures: cleanupFailures,
563521
});
564-
// #1391: a failed script save is never a reason to keep the device claimed —
565-
// only a genuine platform-close failure (the device may still be busy) or a
566-
// resource-cleanup failure withholds it, exactly as before.
567-
if (!platformCloseError && !cleanupAggregate) {
522+
// Only these two withhold the device claim (the device may still be busy);
523+
// a failed script save (#1391) never does — it always releases below.
524+
const blockingError = platformCloseError ?? cleanupAggregate;
525+
if (!blockingError) {
568526
await clearAdvisoryDeviceClaim(session.deviceClaim);
569527
}
570528
sessionStore.delete(sessionName);
571-
if (platformCloseError) throw platformCloseError;
572-
if (cleanupAggregate) throw cleanupAggregate;
529+
if (blockingError) throw blockingError;
573530
if (saveScriptError) throw saveScriptError;
574531
return { kind: 'closed', providerData: leaseRelease.providerData };
575532
}

0 commit comments

Comments
 (0)