@@ -159,14 +159,6 @@ function buildRepairCloseFailureResponse(session: SessionState, error: AppError)
159159 } ;
160160}
161161
162- /**
163- * ADR 0012 decision 6 (BLOCKER 2, new): normalizes a repair-armed session's
164- * FAILED platform close into a distinct, surfaceable AppError, mirroring
165- * `toRepairCommitFailure` in `session-script-writer.ts`. An AppError from the
166- * platform close (e.g. a device-unavailable failure) already carries its own
167- * code/details/hint and passes through unchanged; anything else is wrapped
168- * with a clear message so the agent can tell this apart from a write failure.
169- */
170162function toRepairPlatformCloseFailure ( error : unknown ) : AppError {
171163 if ( error instanceof AppError ) return error ;
172164 const detail = error instanceof Error ? error . message : String ( error ) ;
@@ -189,10 +181,6 @@ type SessionCloseTeardownResult = {
189181 saveScriptError ?: AppError ;
190182} ;
191183
192- // Runs the failure-isolated resource teardown and the targeted platform close
193- // (#1225). Returns the preserved platform-close error (if any); best-effort
194- // cleanup failures are pushed into `cleanupFailures`. Never throws for a cleanup
195- // step so the caller can make an explicit decision about lease/session commit.
196184async function runSessionCloseTeardown ( params : {
197185 req : DaemonRequest ;
198186 session : SessionState ;
@@ -201,11 +189,6 @@ async function runSessionCloseTeardown(params: {
201189 sessionStore : SessionStore ;
202190 cleanupFailures : SessionCleanupFailure [ ] ;
203191 repairArmed : boolean ;
204- // ADR 0012 decision 6 (BLOCKER 2): a repair-armed session already dispatched
205- // (and confirmed the success of) its platform close BEFORE this teardown —
206- // see `handleCloseCommand`. Dispatching it again here would be redundant at
207- // best and a double-close at worst, so it is skipped for that case only.
208- skipPlatformClose : boolean ;
209192} ) : Promise < SessionCloseTeardownResult > {
210193 const { req, session, sessionName, logPath, sessionStore, cleanupFailures, repairArmed } = params ;
211194 const attemptCleanup = async ( step : string , run : ( ) => Promise < void > ) : Promise < void > => {
@@ -215,27 +198,13 @@ async function runSessionCloseTeardown(params: {
215198 cleanupFailures . push ( { step, error } ) ;
216199 }
217200 } ;
218- // Decide runner retention from the PRE-teardown state: an active recording
219- // defeats retention, and `stopBestEffortSessionResources` below finalizes (and
220- // clears) that recording, so the decision must be captured before it runs.
221201 const retainAppleRunner = shouldRetainAppleRunnerAfterClose ( req , session ) ;
222202 await stopBestEffortSessionResources ( session , sessionStore , attemptCleanup ) ;
223- // The targeted platform close is the primary operation, not best-effort cleanup:
224- // its AppError (code/details/hint) is preserved and returned for the caller to
225- // rethrow, and a failed close must not be recorded as `Closed`. Subsequent
226- // resource cleanup still runs regardless.
227- const platformCloseError = params . skipPlatformClose
203+ const platformCloseError = repairArmed
228204 ? undefined
229205 : await dispatchTargetedPlatformClose ( { req, session, logPath } ) ;
230206 await stopOrRetainAppleRunnerAfterClose ( retainAppleRunner , session , attemptCleanup ) ;
231207 await clearSessionRuntimeHints ( session , sessionStore , sessionName ) ;
232- // ADR 0012 decision 6 (BLOCKER 2): a repair-armed session already recorded its
233- // finalize `close` and committed (or aborted) its healed `.ad` BEFORE this
234- // teardown (commit-state machine — the single commit path), and only AFTER
235- // its platform close (dispatched above `handleCloseCommand`) was confirmed to
236- // succeed. Only an ordinary (non-repair) session records `close` + writes its
237- // session log here, and — per #1225 — a failed platform close is not recorded
238- // as `Closed`.
239208 const saveScriptError = repairArmed
240209 ? undefined
241210 : finalizeOrdinaryCloseScript ( { req, session, sessionStore, platformCloseError } ) ;
@@ -328,11 +297,7 @@ async function stopBestEffortSessionResources(
328297 sessionStore : SessionStore ,
329298 attemptCleanup : CleanupRunner ,
330299) : Promise < void > {
331- // Finalize any still-active recording first so closing a session mid-recording
332- // (without an explicit `record stop`) cannot leak the recorder process; must
333- // run before the Apple runner is stopped below since overlay finalization
334- // consults the runner. `shouldRetainAppleRunnerAfterClose` then observes the
335- // now-cleared `session.recording`.
300+ // Recording overlay finalization needs the Apple runner.
336301 await attemptCleanup ( 'recording' , ( ) => stopSessionRecordingForTeardown ( session ) ) ;
337302 await attemptCleanup ( 'app_log' , ( ) => stopSessionAppLog ( session ) ) ;
338303 await attemptCleanup ( 'audio_probe' , async ( ) => {
@@ -346,22 +311,7 @@ async function stopBestEffortSessionResources(
346311 ) ;
347312}
348313
349- /**
350- * ADR 0012 decision 6 (BLOCKER 3, third follow-up): identifies WHICH close
351- * request's platform close succeeded — not merely THAT one did. Only the
352- * request's TARGET (`positionals`) can change what `dispatchTargetedPlatformClose`
353- * actually does: `shouldDispatchPlatformClose` decides purely from
354- * `hasCloseTarget(req)` (plus the `web` special case, constant for a given
355- * session), and the dispatch itself is `dispatchCommand(device, 'close',
356- * req.positionals, ...)`. `close`'s only other flags (`shutdown`, `saveScript`
357- * — see `closeCliSchema`) feed the post-teardown shutdown and the commit path
358- * respectively, never this call, so they carry no identity here. Binding the
359- * marker to this identity means an untargeted close's "succeeded" (a no-op,
360- * since `shouldDispatchPlatformClose` was false) can never be misread as "the
361- * platform close for THIS target already ran" by a later retry that adds or
362- * changes the target — that retry's identity differs, so it re-dispatches.
363- */
364- function repairPlatformCloseIdentity ( req : DaemonRequest ) : string {
314+ function buildRepairPlatformCloseReceipt ( req : DaemonRequest ) : string {
365315 return JSON . stringify ( req . positionals ?? [ ] ) ;
366316}
367317
@@ -377,11 +327,8 @@ async function prepareRepairClose(params: {
377327} ) : Promise < RepairClosePreparation > {
378328 const { req, session, logPath, sessionStore } = params ;
379329 const repairArmed = session . saveScriptBoundary !== undefined ;
380- const closeIdentity = repairPlatformCloseIdentity ( req ) ;
381- if (
382- repairArmed &&
383- ! ( session . repairPlatformCloseSucceeded && session . repairPlatformCloseIdentity === closeIdentity )
384- ) {
330+ const closeReceipt = buildRepairPlatformCloseReceipt ( req ) ;
331+ if ( repairArmed && session . repairPlatformCloseReceipt !== closeReceipt ) {
385332 const platformCloseError = await dispatchTargetedPlatformClose ( { req, session, logPath } ) ;
386333 if ( platformCloseError ) {
387334 return {
@@ -391,15 +338,13 @@ async function prepareRepairClose(params: {
391338 ) ,
392339 } ;
393340 }
394- session . repairPlatformCloseSucceeded = true ;
395- session . repairPlatformCloseIdentity = closeIdentity ;
341+ session . repairPlatformCloseReceipt = closeReceipt ;
396342 }
397343 const repairCommit = commitRepairBeforeClose ( sessionStore , session , req ) ;
398344 if ( repairCommit . kind === 'failed' ) {
399345 return { response : buildRepairCloseFailureResponse ( session , repairCommit . error ) } ;
400346 }
401- session . repairPlatformCloseSucceeded = false ;
402- session . repairPlatformCloseIdentity = undefined ;
347+ session . repairPlatformCloseReceipt = undefined ;
403348 return {
404349 repairArmed,
405350 ...( repairCommit . kind === 'committed' && repairCommit . path
@@ -531,24 +476,6 @@ export async function handleCloseCommand(params: {
531476 if ( req . internal ?. closeAppOnly === true ) {
532477 return await closeAppWithoutEndingSession ( { req, session, logPath } ) ;
533478 }
534- // ADR 0012 decision 6 (BLOCKER 2): for a repair-armed session, the platform
535- // close must run and SUCCEED before anything is committed or torn down —
536- // otherwise a committed healed `.ad` could claim a successful `close` that
537- // never actually happened on the device. On failure, return without
538- // touching the session at all (mirrors the commit-failure path below): it
539- // stays addressable so the agent can fix the cause and retry.
540- //
541- // BLOCKER 3: a PRIOR close attempt on this same session may already have
542- // dispatched the platform close and confirmed its success, then failed to
543- // commit (the session is retained for exactly that retry — see below). A
544- // retry must never re-dispatch a (possibly non-idempotent) platform close
545- // against an already-closed target; `repairPlatformCloseSucceeded` +
546- // `repairPlatformCloseIdentity` together record that the platform-level
547- // close already happened FOR THIS EXACT request identity, so a same-identity
548- // retry consumes it and goes straight to the commit instead. A retry whose
549- // identity DIFFERS (third follow-up: e.g. untargeted -> targeted, or a
550- // changed target) never matches — the marker only ever attests to the
551- // identity it was recorded under, so the platform close runs (again).
552479 const repair = await prepareRepairClose ( { req, session, logPath, sessionStore } ) ;
553480 if ( 'response' in repair ) return repair . response ;
554481 const closed = await runCloseTeardownAndRelease ( {
@@ -622,9 +549,6 @@ async function runCloseTeardownAndRelease(params: {
622549 sessionStore,
623550 cleanupFailures,
624551 repairArmed : params . repairArmed ,
625- // The platform close for a repair-armed session already ran (and was
626- // confirmed to succeed) above, before the commit — never dispatch it twice.
627- skipPlatformClose : params . repairArmed ,
628552 } ) ;
629553 const leaseRelease = await releaseProviderLeaseForClose ( {
630554 session,
0 commit comments