@akagilnc/pi-workflow-roles 0.1.5245 → 0.1.5270

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/acp-host/production-host.js +125 -217
  2. package/dist/headless-host/production-host.js +381 -470
  3. package/dist/host-descriptions.js +0 -2
  4. package/dist/navigator-invocation-identity.js +0 -57
  5. package/dist/navigator-public-session.js +6 -1
  6. package/dist/pi/durable-principal.js +1 -12
  7. package/dist/public-cli/auto-resume.js +59 -87
  8. package/dist/public-cli/instruction-seat-run.js +3 -3
  9. package/dist/public-cli/main.js +86 -206
  10. package/dist/public-cli/post-admission.js +7 -15
  11. package/dist/public-cli/run-lifecycle.js +5 -15
  12. package/dist/public-cli/settlement.js +10 -59
  13. package/dist/public-cli/terminal.js +4 -2
  14. package/dist/session-identity.js +0 -18
  15. package/package.json +1 -1
  16. package/src/acp-host/description.ts +0 -2
  17. package/src/acp-host/production-host.ts +0 -1
  18. package/src/acp-host/role-turn-host.ts +12 -6
  19. package/src/headless-host/role-turn-host.ts +11 -3
  20. package/src/host-contracts.ts +0 -1
  21. package/src/host-descriptions.ts +0 -2
  22. package/src/navigator-invocation-identity.ts +4 -121
  23. package/src/navigator-public-session.ts +12 -1
  24. package/src/pi/durable-principal.ts +1 -11
  25. package/src/prepared-role-turn.ts +1 -1
  26. package/src/public-cli/auto-resume.ts +67 -109
  27. package/src/public-cli/instruction-seat-run.ts +3 -3
  28. package/src/public-cli/post-admission.ts +7 -21
  29. package/src/public-cli/run-lifecycle.ts +3 -21
  30. package/src/public-cli/settlement.ts +8 -75
  31. package/src/public-cli/terminal.ts +4 -2
  32. package/src/role-envelope.ts +3 -3
  33. package/src/role-runtime.ts +23 -5
  34. package/src/session-identity.ts +0 -22
@@ -53,7 +53,6 @@ const dummyIo: CliIo = { stdout: () => {}, stderr: () => {} };
53
53
  */
54
54
  export async function persistReturnedRunState(
55
55
  admitted: { runDirectory: string; principal?: DurablePrincipal },
56
- authority: Pick<DurablePrincipalAuthority, "isAvailable">,
57
56
  options?: { readonly lawful?: boolean },
58
57
  ): Promise<void> {
59
58
  if (options?.lawful === true) {
@@ -65,10 +64,8 @@ export async function persistReturnedRunState(
65
64
  });
66
65
  const typedHttp429 = resumeObservation.typedHttp429;
67
66
  if (admitted.principal !== undefined && typedHttp429 !== undefined) {
68
- if (await authority.isAvailable(admitted.principal)) {
69
- await markRunResumable(admitted.runDirectory, typedHttp429);
70
- return;
71
- }
67
+ await markRunResumable(admitted.runDirectory, typedHttp429);
68
+ return;
72
69
  }
73
70
  await markRunTerminal(admitted.runDirectory);
74
71
  }
@@ -391,16 +388,6 @@ export async function runWithAutoResumeLoop<
391
388
  projectRoot: string;
392
389
  };
393
390
  principalAuthority: DurablePrincipalAuthority;
394
- /**
395
- * Host-aware "can this principal's turn still be resumed" probe (#840 P1).
396
- * Callers that know the selected host pass
397
- * session-identity.ts's resolveHostAwareSessionAvailability(host,
398
- * principalAuthority) so an ACP/headless turn's own binding file — not
399
- * pi's session.jsonl — decides continuation for that host. Defaults to
400
- * principalAuthority.isAvailable (pi-only check) when omitted, so a
401
- * caller that never leaves the pi host keeps identical behavior.
402
- */
403
- isPrincipalAvailable?: (principal: DurablePrincipal) => Promise<boolean>;
404
391
  io: CliIo;
405
392
  sessionAppender: SessionCustomEntryAppender;
406
393
  /**
@@ -433,9 +420,6 @@ export async function runWithAutoResumeLoop<
433
420
  // `attempts >= limit` comparison (always false) — reject here, before any dispatch.
434
421
  const limit = options.autoResumeLimit ?? AUTO_RESUME_LIMIT;
435
422
  parseAutoResumeLimit(limit);
436
- const isPrincipalAvailable =
437
- options.isPrincipalAvailable ??
438
- ((principal: DurablePrincipal) => options.principalAuthority.isAvailable(principal));
439
423
  let autoResumeAttempts = 0;
440
424
  let isFirst = true;
441
425
  let currentPayload = options.buildInitialPayload();
@@ -443,6 +427,14 @@ export async function runWithAutoResumeLoop<
443
427
  let lastThrownError: unknown;
444
428
  let everyAttemptThrew = true;
445
429
  const retainedErrorFiles: string[] = [];
430
+ const failedAttempts: Array<{
431
+ attempt: number;
432
+ diagnostic: string;
433
+ decisiveFacts?: Readonly<Record<string, unknown>>;
434
+ errorFile?: string;
435
+ }> = [];
436
+ let stoppedResult: T | undefined;
437
+ let endReason: string | undefined;
446
438
 
447
439
  while (true) {
448
440
  let result: T | undefined;
@@ -465,6 +457,11 @@ export async function runWithAutoResumeLoop<
465
457
  // diagnostic-sink-isolation precedent). When a lease is held inside
466
458
  // dispatch, that path owns release in its own finally.
467
459
  lastThrownError = error;
460
+ const failedAttempt: (typeof failedAttempts)[number] = {
461
+ attempt: dispatchOrdinal,
462
+ diagnostic: describeErrorIdentity(unwrapTurnDispatchedFailure(error)),
463
+ };
464
+ failedAttempts.push(failedAttempt);
468
465
  turnStartedBeforeThrow = error instanceof TurnDispatchedFailure;
469
466
  const attempt = dispatchOrdinal;
470
467
  try {
@@ -479,6 +476,7 @@ export async function runWithAutoResumeLoop<
479
476
  error,
480
477
  );
481
478
  retainedErrorFiles.push(file);
479
+ failedAttempt.errorFile = file;
482
480
  options.io.stderr(
483
481
  `dispatch attempt ${attempt} threw (${describeErrorIdentity(error)}); full error retained at ${file}\n`,
484
482
  );
@@ -508,106 +506,31 @@ export async function runWithAutoResumeLoop<
508
506
  const terminal = (result as { terminal?: TerminalResult }).terminal;
509
507
  if (terminal !== undefined) {
510
508
  (terminal as { autoResumeCount?: number }).autoResumeCount = autoResumeAttempts;
511
- }
512
-
513
- const lawful = terminal !== undefined && isLawfulTypedTerminalOutcome(terminal.roleOutcome);
514
- if (lawful) {
515
- if (terminal !== undefined) {
516
- // Present lawful terminal once to real io (dummy was used inside dispatch)
517
- options.io.stdout(formatTerminalResult(terminal));
509
+ if (terminal.roleOutcome.kind === "failure") {
510
+ failedAttempts.push({
511
+ attempt: dispatchOrdinal - 1,
512
+ diagnostic: terminal.roleOutcome.diagnostic,
513
+ decisiveFacts: terminal.roleOutcome.decisiveFacts,
514
+ });
518
515
  }
519
- return result;
520
- }
521
- if (result.skipAutoResume === true) {
522
- if (terminal !== undefined) presentTerminal(terminal, options.io);
523
- return result;
524
516
  }
525
- // #855: process cancel — stop even if dispatch forgot skipAutoResume.
526
- if (processCancelSignalName(options.signal) !== undefined) {
527
- if (terminal !== undefined) presentTerminal(terminal, options.io);
528
- return result;
529
- }
530
- }
531
517
 
532
- if (result !== undefined) {
533
- const terminal = (result as { terminal?: TerminalResult }).terminal;
534
- if (autoResumeAttempts >= limit) {
535
- if (terminal !== undefined) presentTerminal(terminal, options.io);
536
- return result;
537
- }
538
- if (
539
- result.turnDispatched === true
540
- && !(await isPrincipalAvailable(options.admitted.principal))
541
- ) {
542
- if (terminal !== undefined) presentTerminal(terminal, options.io);
543
- return result;
518
+ if (terminal !== undefined && isLawfulTypedTerminalOutcome(terminal.roleOutcome)
519
+ || result.skipAutoResume === true
520
+ || autoResumeAttempts >= limit
521
+ || processCancelSignalName(options.signal) !== undefined) {
522
+ stoppedResult = result;
523
+ break;
544
524
  }
545
525
  } else {
546
- // Exception path: continue through the identical budget/session gates.
547
526
  if (autoResumeAttempts >= limit) {
548
- const terminal = await attachDispatchExceptionTerminal(
549
- options.admitted,
550
- dispatchExceptionFailureTerminal({
551
- role: options.admitted.role,
552
- runId: options.admitted.runId,
553
- causeError: lastThrownError,
554
- errorFiles: retainedErrorFiles,
555
- autoResumeAttempts,
556
- endReason: "auto-resume budget exhausted",
557
- everyAttemptThrew,
558
- }),
559
- options.io,
560
- );
561
- await finalizeExceptionRunBestEffort(options.admitted.runDirectory, options.io);
562
- presentTerminal(terminal, options.io);
563
- return {
564
- exitCode: 1,
565
- terminal,
566
- } as T;
527
+ endReason = "auto-resume budget exhausted";
528
+ break;
567
529
  }
568
- // #855: process cancel on the throw path — do not re-dispatch; name the signal.
569
530
  const cancelName = processCancelSignalName(options.signal);
570
531
  if (cancelName !== undefined) {
571
- const terminal = await attachDispatchExceptionTerminal(
572
- options.admitted,
573
- dispatchExceptionFailureTerminal({
574
- role: options.admitted.role,
575
- runId: options.admitted.runId,
576
- causeError: lastThrownError,
577
- errorFiles: retainedErrorFiles,
578
- autoResumeAttempts,
579
- endReason: `ak-role terminated by ${cancelName}`,
580
- everyAttemptThrew,
581
- }),
582
- options.io,
583
- );
584
- await finalizeExceptionRunBestEffort(options.admitted.runDirectory, options.io);
585
- presentTerminal(terminal, options.io);
586
- return {
587
- exitCode: 1,
588
- terminal,
589
- } as T;
590
- }
591
- if (!(await isPrincipalAvailable(options.admitted.principal))) {
592
- const terminal = await attachDispatchExceptionTerminal(
593
- options.admitted,
594
- dispatchExceptionFailureTerminal({
595
- role: options.admitted.role,
596
- runId: options.admitted.runId,
597
- causeError: lastThrownError,
598
- errorFiles: retainedErrorFiles,
599
- autoResumeAttempts,
600
- endReason: "session principal unavailable before further resume",
601
- everyAttemptThrew,
602
- }),
603
- options.io,
604
- );
605
- await finalizeExceptionRunBestEffort(options.admitted.runDirectory, options.io);
606
- presentTerminal(terminal, options.io);
607
- return {
608
- exitCode: 1,
609
- terminal,
610
- } as T;
532
+ endReason = `ak-role terminated by ${cancelName}`;
533
+ break;
611
534
  }
612
535
  }
613
536
 
@@ -621,4 +544,39 @@ export async function runWithAutoResumeLoop<
621
544
  isFirst = false;
622
545
  }
623
546
  }
547
+
548
+ const terminal = stoppedResult === undefined
549
+ ? await attachDispatchExceptionTerminal(
550
+ options.admitted,
551
+ dispatchExceptionFailureTerminal({
552
+ role: options.admitted.role,
553
+ runId: options.admitted.runId,
554
+ causeError: lastThrownError,
555
+ errorFiles: retainedErrorFiles,
556
+ autoResumeAttempts,
557
+ endReason: endReason!,
558
+ everyAttemptThrew,
559
+ }),
560
+ options.io,
561
+ )
562
+ : stoppedResult.terminal;
563
+ if (stoppedResult === undefined) {
564
+ await finalizeExceptionRunBestEffort(options.admitted.runDirectory, options.io);
565
+ }
566
+ const complete: TerminalResult | undefined = terminal !== undefined && failedAttempts.length > 0
567
+ ? { ...terminal, roleOutcome: {
568
+ ...terminal.roleOutcome,
569
+ decisiveFacts: { ...terminal.roleOutcome.decisiveFacts, failedAttempts },
570
+ } } as TerminalResult
571
+ : terminal;
572
+ if (complete !== undefined) {
573
+ if (isLawfulTypedTerminalOutcome(complete.roleOutcome)) {
574
+ options.io.stdout(formatTerminalResult(complete));
575
+ } else {
576
+ presentTerminal(complete, options.io);
577
+ }
578
+ }
579
+ return stoppedResult === undefined
580
+ ? { exitCode: 1, terminal: complete } as T
581
+ : { ...stoppedResult, ...(complete === undefined ? {} : { terminal: complete }) } as T;
624
582
  }
@@ -946,7 +946,7 @@ export async function runPublicInstructionSeatResume(
946
946
  };
947
947
  let binding: Awaited<ReturnType<typeof readAuditorResumeBinding>>;
948
948
  try {
949
- const loaded = await loadResumablePublicRole(env.home, request.runId, env.principalAuthority, true);
949
+ const loaded = await loadResumablePublicRole(env.home, request.runId, env.principalAuthority);
950
950
  binding = loaded.admitted.role === "auditor"
951
951
  ? await readAuditorResumeBinding(loaded.admitted.runDirectory)
952
952
  : undefined;
@@ -1014,7 +1014,7 @@ export async function continueParentAfterChild(
1014
1014
  env: InstructionSeatRunEnv,
1015
1015
  io: CliIo,
1016
1016
  ): Promise<SeatRunResult> {
1017
- const loaded = await loadResumablePublicRole(env.home, parentRunId, env.principalAuthority, true);
1017
+ const loaded = await loadResumablePublicRole(env.home, parentRunId, env.principalAuthority);
1018
1018
  if (loaded.run.state !== "admitted" && !AUDITED_ROLES.has(loaded.admitted.role)) {
1019
1019
  return runPublicInstructionSeatResume({ runId: parentRunId }, env, io);
1020
1020
  }
@@ -1215,7 +1215,7 @@ async function auditSubmittedRole(
1215
1215
  ?? (escalation?.runDirectory === undefined
1216
1216
  ? undefined : runIdFromRunDirectory(escalation.runDirectory));
1217
1217
  if (escalatedRunId === undefined) throw new Error("escalated audit has no resumable run identity");
1218
- const officer = await loadResumablePublicRole(env.home, escalatedRunId, env.principalAuthority, true);
1218
+ const officer = await loadResumablePublicRole(env.home, escalatedRunId, env.principalAuthority);
1219
1219
  const terminal = await trySettlePublicSeat(officer.admitted, env.principalAuthority, undefined);
1220
1220
  if (terminal === undefined) throw new Error("escalated audit has no terminal result");
1221
1221
  io.stdout(formatTerminalResult(terminal));
@@ -29,10 +29,7 @@ import { readRecordedSubmissionRows } from "../submission-ledger.ts";
29
29
  import { readDeclaredTicketNumber } from "../run-ticket-number.ts";
30
30
  import { pathContainedIn } from "../activation-ledger-topology.ts";
31
31
  import { pickEngineAxis } from "../package-resources/engine-material.ts";
32
- import {
33
- readStoredHostSessionId,
34
- resolveHostAwareSessionAvailability,
35
- } from "../session-identity.ts";
32
+ import { readStoredHostSessionId } from "../session-identity.ts";
36
33
  import { rewriteRunDirectoryPathValue } from "../role-run-relocation.ts";
37
34
 
38
35
  import type {
@@ -525,17 +522,11 @@ export async function presentControlledFailure<
525
522
  ...(session === undefined ? {} : { session }),
526
523
  });
527
524
 
528
- // #665 / #416: resume hint is seat-uniform — principal available && typed 429 → show.
529
- // Do not fork the public terminal face on per-seat hasLawful / isResumableRole (ADR 0040).
530
- let resumable = false;
531
- const typedHttp429 = resumeObservation.typedHttp429;
532
- if (admitted.principal !== undefined) {
533
- const sessionPrincipalAvailable = await authority.isAvailable(admitted.principal);
534
- resumable = sessionPrincipalAvailable && typedHttp429 !== undefined;
535
- }
525
+ // #665 / #416: typed 429 with a run principal offers resume; the host decides availability.
526
+ const resumable = admitted.principal !== undefined && resumeObservation.typedHttp429 !== undefined;
536
527
 
537
528
  if (persistRunState && !failureInput.skipRunStateWrite) {
538
- await persistReturnedRunState(admitted, authority);
529
+ await persistReturnedRunState(admitted);
539
530
  }
540
531
 
541
532
  let publishedErrorPath: string | undefined;
@@ -638,7 +629,7 @@ async function settleDeferredPersist<
638
629
  const { needsPersist: _needsPersist, ...settledResult } = result;
639
630
  const lawful = isLawfulTypedTerminalOutcome(result.terminal.roleOutcome);
640
631
  try {
641
- await persistReturnedRunState(admitted, authority, lawful ? { lawful: true } : undefined);
632
+ await persistReturnedRunState(admitted, lawful ? { lawful: true } : undefined);
642
633
  return settledResult;
643
634
  } catch (error) {
644
635
  const failed = await settleAfterTurnStarted(
@@ -1136,7 +1127,7 @@ export async function dispatchPostAdmissionTurn<
1136
1127
  }
1137
1128
  if (persistRunState) {
1138
1129
  try {
1139
- await persistReturnedRunState(admitted, env.principalAuthority, { lawful: true });
1130
+ await persistReturnedRunState(admitted, { lawful: true });
1140
1131
  } catch (error) {
1141
1132
  await recordBestEffortPostDispatchDiagnostic(
1142
1133
  admitted,
@@ -1241,7 +1232,7 @@ export async function dispatchPostAdmissionTurn<
1241
1232
  }
1242
1233
  if (persistRunState) {
1243
1234
  try {
1244
- await persistReturnedRunState(admitted, env.principalAuthority, { lawful: true });
1235
+ await persistReturnedRunState(admitted, { lawful: true });
1245
1236
  } catch (error) {
1246
1237
  await recordBestEffortPostDispatchDiagnostic(
1247
1238
  admitted,
@@ -1634,10 +1625,6 @@ export async function runPostAdmissionSeatResume<
1634
1625
  return await runWithAutoResumeLoop({
1635
1626
  admitted: loaded.admitted,
1636
1627
  principalAuthority: env.principalAuthority,
1637
- isPrincipalAvailable: resolveHostAwareSessionAvailability(
1638
- env.host,
1639
- env.principalAuthority,
1640
- ),
1641
1628
  io: input.io,
1642
1629
  sessionAppender: env.sessionAppender,
1643
1630
  autoResumeLimit: env.autoResumeLimit,
@@ -1913,7 +1900,6 @@ export async function runPostAdmissionResumable<
1913
1900
  return runWithAutoResumeLoop({
1914
1901
  admitted,
1915
1902
  principalAuthority: env.principalAuthority,
1916
- isPrincipalAvailable: resolveHostAwareSessionAvailability(env.host, env.principalAuthority),
1917
1903
  io,
1918
1904
  sessionAppender: env.sessionAppender,
1919
1905
  autoResumeLimit: env.autoResumeLimit,
@@ -687,17 +687,6 @@ export async function clearCurrentCourt(
687
687
  });
688
688
  }
689
689
 
690
- /**
691
- * True when the host authority reports the durable principal available.
692
- * Resume must reopen this exact principal; directory-latest is not identity.
693
- */
694
- export async function isDurablePrincipalAvailable(
695
- principal: DurablePrincipal,
696
- authority: DurablePrincipalAuthority,
697
- ): Promise<boolean> {
698
- return authority.isAvailable(principal);
699
- }
700
-
701
690
  export class RunWriterLeaseHeldError extends Error {
702
691
  readonly code = "AK_RUN_WRITER_LEASE_HELD" as const;
703
692
  constructor(message = "role run writer lease is already held") {
@@ -1325,7 +1314,6 @@ async function loadResumableRunRecord(
1325
1314
  home: string,
1326
1315
  runId: string,
1327
1316
  authority: DurablePrincipalAuthority,
1328
- allowUnformedAdmitted = false,
1329
1317
  ): Promise<{
1330
1318
  readonly run: RoleRunRecord;
1331
1319
  readonly principal: DurablePrincipal;
@@ -1340,19 +1328,14 @@ async function loadResumableRunRecord(
1340
1328
  if (disk === undefined) {
1341
1329
  throw new CliUsageError(`unknown role run id: ${runId}`);
1342
1330
  }
1343
- // #416: removed terminal/resumable gates per owner decision "根本不要有限制" (2026-08-22).
1344
- // Only the exact Pi session principal check remains as honest failure.
1331
+ // #416 / #1091: removed terminal/resumable and local session-file gates.
1332
+ // The load seam locates the run and recovers identity; host CLI answers continuation.
1345
1333
  // One authority.decode of the uninterpreted wire yields both principal and record.
1346
1334
  const materialized = materializeRoleRunFromDisk(disk, authority);
1347
1335
  if (materialized === undefined) {
1348
1336
  throw new CliUsageError(`unknown role run id: ${runId}`);
1349
1337
  }
1350
1338
  const { run, principal } = materialized;
1351
- if (!(allowUnformedAdmitted && run.state === "admitted") && !(await isDurablePrincipalAvailable(principal, authority))) {
1352
- throw new CliUsageError(
1353
- `role run Pi session principal is unavailable: ${runId}`,
1354
- );
1355
- }
1356
1339
  // Reconstruct admitted identity from durable run record + admitted-request.json.
1357
1340
  let instruction = "";
1358
1341
  let instructionEmpty = true;
@@ -1755,9 +1738,8 @@ export async function loadResumablePublicRole(
1755
1738
  home: string,
1756
1739
  runId: string,
1757
1740
  authority: DurablePrincipalAuthority,
1758
- allowUnformedAdmitted = false,
1759
1741
  ): Promise<LoadedResumablePublicRole> {
1760
- const loaded = await loadResumableRunRecord(home, runId, authority, allowUnformedAdmitted);
1742
+ const loaded = await loadResumableRunRecord(home, runId, authority);
1761
1743
  return seatLoadedResult(loaded, admitResumedRole(loaded));
1762
1744
  }
1763
1745
 
@@ -88,11 +88,7 @@ import {
88
88
  findLatestDurablePackagedRoleTerminal,
89
89
  hasNavigatorInfrastructureFailureBase,
90
90
  isAcceptedPackagedRoleTerminalResult,
91
- isReceiptSettlementBindingClear,
92
- NAVIGATOR_INVOCATION_ENTRY,
93
91
  NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY,
94
- parseInvocationMarkerIdentity,
95
- type InvocationMarkerIdentity,
96
92
  } from "../navigator-invocation-identity.ts";
97
93
  import {
98
94
  NO_RECEIPT_LIFECYCLE_ENTRY_TYPE,
@@ -1921,23 +1917,6 @@ export async function appendRunAttemptHistory(
1921
1917
  } catch {}
1922
1918
  }
1923
1919
 
1924
- /**
1925
- * Minimal attendance provenance against the bound marker (ADR 0043).
1926
- * Keep only invocationId + post-terminal ordering. Runtime-self-produced
1927
- * role/phase/subject/version are not re-reconciled here (ADR 0042).
1928
- */
1929
- function navigatorAttendanceCorrelatedWithBoundMarker(
1930
- details: Record<string, unknown>,
1931
- attendanceIndex: number,
1932
- terminalIndex: number,
1933
- marker: InvocationMarkerIdentity,
1934
- ): boolean {
1935
- if (attendanceIndex <= terminalIndex) return false;
1936
- // Exact current invocation token is the bound marker principal.
1937
- if (details.invocationId !== marker.invocationId) return false;
1938
- return true;
1939
- }
1940
-
1941
1920
  function parseNavigatorAttendanceDetails(
1942
1921
  details: Record<string, unknown>,
1943
1922
  ): TerminalNavigatorFact {
@@ -2216,10 +2195,9 @@ export function extractNavigatorFact(
2216
2195
  function extractNavigatorAttendanceFact(
2217
2196
  entries: readonly SessionEntry[],
2218
2197
  ): TerminalNavigatorFact {
2219
- // Affirmative attendance only. Missing / uncorrelated / unparseable is never no-advice.
2220
- // Minimal provenance: latest durable terminal + nearest preceding marker +
2221
- // invocationId + post-terminal order. Marker↔terminal cardinality belongs to
2222
- // receipt settlement (isReceiptSettlementBindingClear), not attendance extraction.
2198
+ // Affirmative attendance only. Missing / unparseable is never no-advice.
2199
+ // The shared lifecycle places the settled event on this submission's closure;
2200
+ // an unrelated attendance message cannot supply another call's result.
2223
2201
  const terminal = findLatestDurablePackagedRoleTerminal(entries);
2224
2202
  if (terminal === undefined) {
2225
2203
  return {
@@ -2229,57 +2207,12 @@ function extractNavigatorAttendanceFact(
2229
2207
  };
2230
2208
  }
2231
2209
 
2232
- let markerIndex = -1;
2233
- for (let i = terminal.index - 1; i >= 0; i -= 1) {
2234
- const entry = entries[i];
2235
- if (entry?.type === "custom" && entry.customType === NAVIGATOR_INVOCATION_ENTRY) {
2236
- markerIndex = i;
2237
- break;
2238
- }
2239
- }
2240
- if (markerIndex < 0) {
2241
- return {
2242
- disposition: "unavailable",
2243
- source: "unknown",
2244
- reason: "Navigator attendance is uncorrelated with session invocation facts",
2245
- };
2246
- }
2247
- const marker = parseInvocationMarkerIdentity(entries[markerIndex]?.data);
2248
- if (marker === undefined) {
2249
- return {
2250
- disposition: "unavailable",
2251
- source: "unknown",
2252
- reason: "Navigator attendance is uncorrelated with session invocation facts",
2253
- };
2254
- }
2255
-
2256
- for (let i = entries.length - 1; i >= 0; i -= 1) {
2210
+ for (let i = terminal.index; i < entries.length; i += 1) {
2257
2211
  const entry = entries[i];
2258
- if (entry?.type === "custom_message" && entry.customType === "ak-navigator-attendance") {
2259
- const details = entry.message?.details ?? (entry as { details?: unknown }).details;
2260
- if (!isRecord(details)) {
2261
- return {
2262
- disposition: "unavailable",
2263
- source: "unknown",
2264
- reason: "Navigator attendance is unparseable",
2265
- };
2266
- }
2267
- if (
2268
- !navigatorAttendanceCorrelatedWithBoundMarker(
2269
- details,
2270
- i,
2271
- terminal.index,
2272
- marker,
2273
- )
2274
- ) {
2275
- return {
2276
- disposition: "unavailable",
2277
- source: "unknown",
2278
- reason: "Navigator attendance is uncorrelated with session invocation facts",
2279
- };
2280
- }
2281
- return parseNavigatorAttendanceDetails(details);
2282
- }
2212
+ if (entry?.type !== "custom" || entry.customType !== "ak-role-submission-closure") continue;
2213
+ const details = isRecord(entry.data) ? entry.data.navigator : undefined;
2214
+ if (!isRecord(details)) break;
2215
+ return parseNavigatorAttendanceDetails(details);
2283
2216
  }
2284
2217
  // Absence is not successful no-advice — require affirmative typed attendance.
2285
2218
  return {
@@ -278,8 +278,10 @@ export function formatTerminalResult(result: TerminalResult): string {
278
278
  `diagnostic\t${encodeTerminalField(result.roleOutcome.diagnostic)}`,
279
279
  );
280
280
  }
281
- if (result.roleOutcome.kind === "failure" || result.roleOutcome.kind === "no_receipt" || result.roleOutcome.kind === "audit_escalation") {
282
- const facts = result.roleOutcome.decisiveFacts ?? {};
281
+ if (result.roleOutcome.kind === "failure" || result.roleOutcome.kind === "no_receipt" || result.roleOutcome.kind === "audit_escalation" || result.roleOutcome.decisiveFacts?.failedAttempts !== undefined) {
282
+ const facts = result.roleOutcome.kind === "accepted"
283
+ ? { failedAttempts: result.roleOutcome.decisiveFacts?.failedAttempts }
284
+ : result.roleOutcome.decisiveFacts ?? {};
283
285
  for (const [key, value] of Object.entries(facts)) {
284
286
  if (value === undefined) continue;
285
287
  const rendered =
@@ -78,8 +78,8 @@ export async function prepareRoleEnvelope(options: {
78
78
  readonly listTerminatingToolOnMcp?: boolean;
79
79
  /**
80
80
  * Durable principal session path (header layout only).
81
- * Production passes DurablePrincipalAuthority.decode(principal).sessionFile so
82
- * isAvailable and envelope mint the same file. Tests may omit → runDirectory default.
81
+ * Production passes DurablePrincipalAuthority.decode(principal).sessionFile.
82
+ * Tests may omit → runDirectory default.
83
83
  */
84
84
  readonly sessionFile?: string;
85
85
  }): Promise<PreparedRoleTurn> {
@@ -113,7 +113,7 @@ export async function prepareRoleEnvelope(options: {
113
113
  const runId = request.runDirectory.split("/").filter(Boolean).at(-1) ?? randomUUID();
114
114
  await mkdir(request.runDirectory, { recursive: true });
115
115
 
116
- // Durable principal file for isAvailable / resumable settlement (public-cli).
116
+ // Durable principal file for package-owned session entries.
117
117
  // #617 DK-4: never host conversation/tool writeback into Pi JSONL.
118
118
  // #959: package-owned custom entries (invocation marker, submission-closure,
119
119
  // navigator attendance, no-receipt) MUST flush so parent settlement can read
@@ -523,7 +523,7 @@ export async function projectClosedSubmissionLifecycle(
523
523
  context: HostContext,
524
524
  phase: NavigatorPhase,
525
525
  recordAccepted: () => void,
526
- settle: (settlement: NavigatorSettlement | undefined) => Promise<void>,
526
+ settle: (settlement: NavigatorSettlement | undefined) => Promise<NavigatorEvent | undefined>,
527
527
  ): Promise<void> {
528
528
  recordAccepted();
529
529
  const closure = {
@@ -531,8 +531,17 @@ export async function projectClosedSubmissionLifecycle(
531
531
  isError: false,
532
532
  details: closed.accepted,
533
533
  };
534
- context.sessionManager.appendCustomEntry?.("ak-role-submission-closure", closure);
535
- await settle(publicNavigatorSettlement(closed.role, phase, closure));
534
+ let navigator: NavigatorEvent | undefined;
535
+ try {
536
+ navigator = await settle(publicNavigatorSettlement(closed.role, phase, closure));
537
+ } finally {
538
+ // A Navigator failure cannot erase an already accepted role closure.
539
+ // Missing attendance remains a typed unavailable at public settlement.
540
+ context.sessionManager.appendCustomEntry?.("ak-role-submission-closure", {
541
+ ...closure,
542
+ ...(navigator === undefined ? {} : { navigator }),
543
+ });
544
+ }
536
545
  }
537
546
 
538
547
  /**
@@ -1032,6 +1041,8 @@ export function createRoleRuntimeExtension(
1032
1041
  let navigatorAttendance: NavigatorAttendanceDependency | undefined;
1033
1042
  // #351: session-lifecycle owner for periodic OAuth refresh (orthogonal to role admission).
1034
1043
  let pendingNavigatorPresentation: { event: import("./navigator-attendance.ts").NavigatorEvent; report: import("./navigator-attendance.ts").NavigatorReport } | undefined;
1044
+ let navigatorActivation = 0;
1045
+ let navigatorDeliveryClosed = false;
1035
1046
  let pendingNavigatorSettlement: Promise<void> | undefined;
1036
1047
  let navigatorWorkContext: NavigatorWorkContext | undefined;
1037
1048
  let navigatorSessionParent: string | undefined;
@@ -1085,7 +1096,7 @@ export function createRoleRuntimeExtension(
1085
1096
  }
1086
1097
  void Promise.resolve(pending).then(undefined, recordDisposeFailure);
1087
1098
  };
1088
- const settleNavigatorProjection = async (settlement: NavigatorSettlement | undefined) => {
1099
+ const settleNavigatorProjection = async (settlement: NavigatorSettlement | undefined): Promise<NavigatorEvent | undefined> => {
1089
1100
  const attendance = navigatorAttendance;
1090
1101
  if (settlement === undefined || attendance === undefined) return;
1091
1102
  const workContext = navigatorWorkContext;
@@ -1099,6 +1110,7 @@ export function createRoleRuntimeExtension(
1099
1110
  void settlePromise.catch(() => undefined);
1100
1111
  const raced = await raceNavigatorGrace(settlePromise, NAVIGATOR_POST_ROLE_GRACE_MS);
1101
1112
  if (raced.status !== "timeout") return;
1113
+ navigatorDeliveryClosed = true;
1102
1114
  if (pendingNavigatorPresentation === undefined) {
1103
1115
  const report: NavigatorReport = {
1104
1116
  disposition: "unavailable",
@@ -1124,6 +1136,7 @@ export function createRoleRuntimeExtension(
1124
1136
  })();
1125
1137
  pendingNavigatorSettlement = pending;
1126
1138
  await pending;
1139
+ return pendingNavigatorPresentation?.event;
1127
1140
  };
1128
1141
  projectClosedSubmission = async (closed, context) => projectClosedSubmissionLifecycle(
1129
1142
  closed,
@@ -1695,6 +1708,8 @@ export function createRoleRuntimeExtension(
1695
1708
  receiptDelivery = createReceiptDeliveryPolicy();
1696
1709
  noReceiptRecorded = false;
1697
1710
  pendingNavigatorPresentation = undefined;
1711
+ navigatorActivation += 1;
1712
+ navigatorDeliveryClosed = false;
1698
1713
  pendingNavigatorSettlement = undefined;
1699
1714
  pendingInfrastructureFailures.clear();
1700
1715
  pendingSubmissionNonPassByToolCallId.clear();
@@ -1811,6 +1826,7 @@ export function createRoleRuntimeExtension(
1811
1826
  });
1812
1827
  } catch {}
1813
1828
  }
1829
+ const activation = navigatorActivation;
1814
1830
  navigatorAttendance = await dependencies.createNavigatorAttendance({
1815
1831
  context: ctx,
1816
1832
  role: entry.role,
@@ -1821,7 +1837,9 @@ export function createRoleRuntimeExtension(
1821
1837
  invocationId,
1822
1838
  ...(contextError === undefined ? {} : { contextError }),
1823
1839
  onEvent: (navigatorEvent, report) => {
1824
- pendingNavigatorPresentation = { event: navigatorEvent, report };
1840
+ if (activation === navigatorActivation && !navigatorDeliveryClosed) {
1841
+ pendingNavigatorPresentation = { event: navigatorEvent, report };
1842
+ }
1825
1843
  },
1826
1844
  });
1827
1845
  // Concrete work context starts standby attendance (record only, no model).
@@ -42,28 +42,6 @@ export function createSessionIdentityAuthority(
42
42
  };
43
43
  }
44
44
 
45
- /**
46
- * Host-aware resumable-session probe (#840 P1). The shared auto-resume loop
47
- * (public-cli/auto-resume.ts) asks one question — "can this principal's turn
48
- * still be resumed" — without itself knowing which concrete host executed it.
49
- * Pi's own session.jsonl transcript (DurablePrincipalAuthority#isAvailable)
50
- * answers that only for the pi host; ACP and headless hosts persist their
51
- * resumable native session id under their own session-identity binding file
52
- * instead (#729 / #731 / #645) and never write session.jsonl, so the plain
53
- * pi check always reports them unavailable after one attempt. One lookup per
54
- * registered host family — no per-host-name branch, no per-seat whitelist.
55
- */
56
- export function resolveHostAwareSessionAvailability(
57
- host: string | undefined,
58
- principalAuthority: DurablePrincipalAuthority,
59
- ): (principal: DurablePrincipal) => Promise<boolean> {
60
- if (host === undefined || host === DEFAULT_ROLE_TURN_HOST) {
61
- return (principal) => principalAuthority.isAvailable(principal);
62
- }
63
- // Unregistered names have no binding file: absence, not a guess at pi.
64
- return async (principal) =>
65
- (await readStoredHostSessionId(host, principalAuthority, principal)) !== undefined;
66
- }
67
45
 
68
46
  /**
69
47
  * Native session/thread id already stored for this principal on `host`.