@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
@@ -429,13 +429,6 @@ export async function clearCurrentCourt(runDirectory, expectedCourtAttemptId) {
429
429
  ...(current.resumable === undefined ? {} : { resumable: current.resumable }),
430
430
  });
431
431
  }
432
- /**
433
- * True when the host authority reports the durable principal available.
434
- * Resume must reopen this exact principal; directory-latest is not identity.
435
- */
436
- export async function isDurablePrincipalAvailable(principal, authority) {
437
- return authority.isAvailable(principal);
438
- }
439
432
  export class RunWriterLeaseHeldError extends Error {
440
433
  code = "AK_RUN_WRITER_LEASE_HELD";
441
434
  constructor(message = "role run writer lease is already held") {
@@ -932,7 +925,7 @@ function restoredTicketFields(fields) {
932
925
  : { courtTicketNumbers: fields.courtTicketNumbers }),
933
926
  };
934
927
  }
935
- async function loadResumableRunRecord(home, runId, authority, allowUnformedAdmitted = false) {
928
+ async function loadResumableRunRecord(home, runId, authority) {
936
929
  const runDirectory = await findRunDirectoryById(home, runId);
937
930
  if (runDirectory === undefined) {
938
931
  throw new CliUsageError(`unknown role run id: ${runId}`);
@@ -941,17 +934,14 @@ async function loadResumableRunRecord(home, runId, authority, allowUnformedAdmit
941
934
  if (disk === undefined) {
942
935
  throw new CliUsageError(`unknown role run id: ${runId}`);
943
936
  }
944
- // #416: removed terminal/resumable gates per owner decision "根本不要有限制" (2026-08-22).
945
- // Only the exact Pi session principal check remains as honest failure.
937
+ // #416 / #1091: removed terminal/resumable and local session-file gates.
938
+ // The load seam locates the run and recovers identity; host CLI answers continuation.
946
939
  // One authority.decode of the uninterpreted wire yields both principal and record.
947
940
  const materialized = materializeRoleRunFromDisk(disk, authority);
948
941
  if (materialized === undefined) {
949
942
  throw new CliUsageError(`unknown role run id: ${runId}`);
950
943
  }
951
944
  const { run, principal } = materialized;
952
- if (!(allowUnformedAdmitted && run.state === "admitted") && !(await isDurablePrincipalAvailable(principal, authority))) {
953
- throw new CliUsageError(`role run Pi session principal is unavailable: ${runId}`);
954
- }
955
945
  // Reconstruct admitted identity from durable run record + admitted-request.json.
956
946
  let instruction = "";
957
947
  let instructionEmpty = true;
@@ -1268,8 +1258,8 @@ export async function peekRoleRunRole(home, runId) {
1268
1258
  * selects the existing restore checks. The stored native host session id is
1269
1259
  * read on the public explicit resume seam, not from this load.
1270
1260
  */
1271
- export async function loadResumablePublicRole(home, runId, authority, allowUnformedAdmitted = false) {
1272
- const loaded = await loadResumableRunRecord(home, runId, authority, allowUnformedAdmitted);
1261
+ export async function loadResumablePublicRole(home, runId, authority) {
1262
+ const loaded = await loadResumableRunRecord(home, runId, authority);
1273
1263
  return seatLoadedResult(loaded, admitResumedRole(loaded));
1274
1264
  }
1275
1265
  /**
@@ -26,7 +26,7 @@ import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "../dossier-resolution.js";
26
26
  import { REVIEWER_OUTPUT_TOOL_NAME, } from "../package-contracts/reviewer-output.js";
27
27
  import { packagedAuditToolName, packagedDurableOfficerEntry, packagedRoleAcceptedOutputTool, packagedRoleMetadata, packagedSkipsGateOnInfrastructureStage, } from "../packaged-role-registry.js";
28
28
  import { SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY, SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, } from "../secretariat-contracts.js";
29
- import { classifyPackagedRoleTerminalResult, findLatestDurablePackagedRoleTerminal, hasNavigatorInfrastructureFailureBase, isAcceptedPackagedRoleTerminalResult, isReceiptSettlementBindingClear, NAVIGATOR_INVOCATION_ENTRY, NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY, parseInvocationMarkerIdentity, } from "../navigator-invocation-identity.js";
29
+ import { classifyPackagedRoleTerminalResult, findLatestDurablePackagedRoleTerminal, hasNavigatorInfrastructureFailureBase, isAcceptedPackagedRoleTerminalResult, NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY, } from "../navigator-invocation-identity.js";
30
30
  import { NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_TURN_LIMIT, noReceiptLifecycleFacts, parseNoReceiptLifecycleFacts, } from "../receipt-delivery-policy.js";
31
31
  import { roleRunArtifactsDirectory } from "../role-run-placement.js";
32
32
  import { rewriteRunDirectoryPathValue } from "../role-run-relocation.js";
@@ -1353,19 +1353,6 @@ export async function appendRunAttemptHistory(source, outcome) {
1353
1353
  }
1354
1354
  catch { }
1355
1355
  }
1356
- /**
1357
- * Minimal attendance provenance against the bound marker (ADR 0043).
1358
- * Keep only invocationId + post-terminal ordering. Runtime-self-produced
1359
- * role/phase/subject/version are not re-reconciled here (ADR 0042).
1360
- */
1361
- function navigatorAttendanceCorrelatedWithBoundMarker(details, attendanceIndex, terminalIndex, marker) {
1362
- if (attendanceIndex <= terminalIndex)
1363
- return false;
1364
- // Exact current invocation token is the bound marker principal.
1365
- if (details.invocationId !== marker.invocationId)
1366
- return false;
1367
- return true;
1368
- }
1369
1356
  function parseNavigatorAttendanceDetails(details) {
1370
1357
  const disposition = details.disposition;
1371
1358
  const advisoryDiagnostic = typeof details.routePlaybookReadFailure === "string"
@@ -1586,10 +1573,9 @@ export function extractNavigatorFact(entries) {
1586
1573
  return message === undefined ? fact : { ...fact, advisoryDiagnostic: message };
1587
1574
  }
1588
1575
  function extractNavigatorAttendanceFact(entries) {
1589
- // Affirmative attendance only. Missing / uncorrelated / unparseable is never no-advice.
1590
- // Minimal provenance: latest durable terminal + nearest preceding marker +
1591
- // invocationId + post-terminal order. Marker↔terminal cardinality belongs to
1592
- // receipt settlement (isReceiptSettlementBindingClear), not attendance extraction.
1576
+ // Affirmative attendance only. Missing / unparseable is never no-advice.
1577
+ // The shared lifecycle places the settled event on this submission's closure;
1578
+ // an unrelated attendance message cannot supply another call's result.
1593
1579
  const terminal = findLatestDurablePackagedRoleTerminal(entries);
1594
1580
  if (terminal === undefined) {
1595
1581
  return {
@@ -1598,49 +1584,14 @@ function extractNavigatorAttendanceFact(entries) {
1598
1584
  reason: "Navigator attendance has no durable packaged role terminal",
1599
1585
  };
1600
1586
  }
1601
- let markerIndex = -1;
1602
- for (let i = terminal.index - 1; i >= 0; i -= 1) {
1587
+ for (let i = terminal.index; i < entries.length; i += 1) {
1603
1588
  const entry = entries[i];
1604
- if (entry?.type === "custom" && entry.customType === NAVIGATOR_INVOCATION_ENTRY) {
1605
- markerIndex = i;
1589
+ if (entry?.type !== "custom" || entry.customType !== "ak-role-submission-closure")
1590
+ continue;
1591
+ const details = isRecord(entry.data) ? entry.data.navigator : undefined;
1592
+ if (!isRecord(details))
1606
1593
  break;
1607
- }
1608
- }
1609
- if (markerIndex < 0) {
1610
- return {
1611
- disposition: "unavailable",
1612
- source: "unknown",
1613
- reason: "Navigator attendance is uncorrelated with session invocation facts",
1614
- };
1615
- }
1616
- const marker = parseInvocationMarkerIdentity(entries[markerIndex]?.data);
1617
- if (marker === undefined) {
1618
- return {
1619
- disposition: "unavailable",
1620
- source: "unknown",
1621
- reason: "Navigator attendance is uncorrelated with session invocation facts",
1622
- };
1623
- }
1624
- for (let i = entries.length - 1; i >= 0; i -= 1) {
1625
- const entry = entries[i];
1626
- if (entry?.type === "custom_message" && entry.customType === "ak-navigator-attendance") {
1627
- const details = entry.message?.details ?? entry.details;
1628
- if (!isRecord(details)) {
1629
- return {
1630
- disposition: "unavailable",
1631
- source: "unknown",
1632
- reason: "Navigator attendance is unparseable",
1633
- };
1634
- }
1635
- if (!navigatorAttendanceCorrelatedWithBoundMarker(details, i, terminal.index, marker)) {
1636
- return {
1637
- disposition: "unavailable",
1638
- source: "unknown",
1639
- reason: "Navigator attendance is uncorrelated with session invocation facts",
1640
- };
1641
- }
1642
- return parseNavigatorAttendanceDetails(details);
1643
- }
1594
+ return parseNavigatorAttendanceDetails(details);
1644
1595
  }
1645
1596
  // Absence is not successful no-advice — require affirmative typed attendance.
1646
1597
  return {
@@ -69,8 +69,10 @@ export function formatTerminalResult(result) {
69
69
  if (result.roleOutcome.kind === "failure") {
70
70
  lines.push(`diagnostic\t${encodeTerminalField(result.roleOutcome.diagnostic)}`);
71
71
  }
72
- if (result.roleOutcome.kind === "failure" || result.roleOutcome.kind === "no_receipt" || result.roleOutcome.kind === "audit_escalation") {
73
- const facts = result.roleOutcome.decisiveFacts ?? {};
72
+ if (result.roleOutcome.kind === "failure" || result.roleOutcome.kind === "no_receipt" || result.roleOutcome.kind === "audit_escalation" || result.roleOutcome.decisiveFacts?.failedAttempts !== undefined) {
73
+ const facts = result.roleOutcome.kind === "accepted"
74
+ ? { failedAttempts: result.roleOutcome.decisiveFacts?.failedAttempts }
75
+ : result.roleOutcome.decisiveFacts ?? {};
74
76
  for (const [key, value] of Object.entries(facts)) {
75
77
  if (value === undefined)
76
78
  continue;
@@ -31,24 +31,6 @@ export function createSessionIdentityAuthority(authority, sessionBindingFile) {
31
31
  },
32
32
  };
33
33
  }
34
- /**
35
- * Host-aware resumable-session probe (#840 P1). The shared auto-resume loop
36
- * (public-cli/auto-resume.ts) asks one question — "can this principal's turn
37
- * still be resumed" — without itself knowing which concrete host executed it.
38
- * Pi's own session.jsonl transcript (DurablePrincipalAuthority#isAvailable)
39
- * answers that only for the pi host; ACP and headless hosts persist their
40
- * resumable native session id under their own session-identity binding file
41
- * instead (#729 / #731 / #645) and never write session.jsonl, so the plain
42
- * pi check always reports them unavailable after one attempt. One lookup per
43
- * registered host family — no per-host-name branch, no per-seat whitelist.
44
- */
45
- export function resolveHostAwareSessionAvailability(host, principalAuthority) {
46
- if (host === undefined || host === DEFAULT_ROLE_TURN_HOST) {
47
- return (principal) => principalAuthority.isAvailable(principal);
48
- }
49
- // Unregistered names have no binding file: absence, not a guess at pi.
50
- return async (principal) => (await readStoredHostSessionId(host, principalAuthority, principal)) !== undefined;
51
- }
52
34
  /**
53
35
  * Native session/thread id already stored for this principal on `host`.
54
36
  * Public explicit resume reads it once and hands it to the host adapter.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.5245",
3
+ "version": "0.1.5270",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -26,8 +26,6 @@ export type AcpHostDescription = Readonly<{
26
26
  * `provider:model` (hermes).
27
27
  */
28
28
  modelPassing: "argv" | "set_model";
29
- /** Which verb a bound resume uses; "session/new" hosts always mint + bind. */
30
- boundResume: "session/load" | "session/new";
31
29
  /** Durable ACP binding filename written beside the session principal. */
32
30
  sessionBindingFile: string;
33
31
  /**
@@ -61,7 +61,6 @@ export function createProductionAcpRoleTurnHost(options: ProductionAcpHostOption
61
61
  return createComposedAcpRoleTurnHost({
62
62
  hostName,
63
63
  sessionIdentity: createSessionIdentityAuthority(principalAuthority, description.sessionBindingFile),
64
- boundResume: description.boundResume,
65
64
  modelPassing: description.modelPassing,
66
65
  roleRuntimeDependencies: createRoleRuntimeDependencies(packageRoot),
67
66
  async connect(request) {
@@ -86,8 +86,6 @@ export type AcpRoleTurnHostConfig = Readonly<{
86
86
  sessionIdentity: SessionIdentityAuthority;
87
87
  /** Seat-table host key (e.g. grok-build) for sitian host field. */
88
88
  hostName: string;
89
- /** Whether a bound resume reuses the native session or mints a fresh one. */
90
- boundResume: AcpHostDescription["boundResume"];
91
89
  /**
92
90
  * How the seat model reaches the agent: "set_model" sends an ACP
93
91
  * `session/set_model` RPC with modelId `provider:model` once the session
@@ -324,7 +322,8 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
324
322
  });
325
323
  return loadedId;
326
324
  };
327
- if (request.continuation.kind === "resume" && config.boundResume === "session/load") {
325
+ let sessionReady = true;
326
+ if (request.continuation.kind === "resume") {
328
327
  const explicitHostSessionId = request.continuation.hostSessionId;
329
328
  const boundSessionId = explicitHostSessionId !== undefined && explicitHostSessionId !== ""
330
329
  ? explicitHostSessionId
@@ -333,10 +332,17 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
333
332
  sessionId = boundSessionId;
334
333
  sessionId = await loadSession(boundSessionId);
335
334
  sessionOpened = true;
335
+ } else {
336
+ outcome = failure(
337
+ "session",
338
+ "AcpSessionFailure",
339
+ "session-id-missing",
340
+ undefined,
341
+ "resume requires a bound session id",
342
+ );
343
+ sessionReady = false;
336
344
  }
337
- }
338
- let sessionReady = true;
339
- if (sessionId === undefined) {
345
+ } else {
340
346
  const session = await rpc("session/new", sessionBindParams);
341
347
  sessionId = typeof session.sessionId === "string" ? session.sessionId : undefined;
342
348
  if (sessionId === undefined || sessionId === "") {
@@ -475,10 +475,17 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
475
475
  let sessionId = explicitHostSessionId !== undefined && explicitHostSessionId !== ""
476
476
  ? explicitHostSessionId
477
477
  : await config.sessionIdentity.load(request.principal);
478
+ if (request.continuation.kind === "resume" && (sessionId === undefined || sessionId === "")) {
479
+ outcome = failure(
480
+ "session",
481
+ "HeadlessSessionFailure",
482
+ "session-id-missing",
483
+ undefined,
484
+ "resume requires a bound session id",
485
+ );
486
+ } else {
478
487
  let sessionKind: "new" | "resume" =
479
- request.continuation.kind === "resume" && sessionId !== undefined && sessionId !== ""
480
- ? "resume"
481
- : "new";
488
+ request.continuation.kind === "resume" ? "resume" : "new";
482
489
  if (sessionKind === "new" && !codex) {
483
490
  sessionId = randomUUID();
484
491
  await config.sessionIdentity.bind(request.principal, sessionId);
@@ -809,6 +816,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
809
816
  ...(request.home !== undefined ? { home: request.home } : {}),
810
817
  });
811
818
  }
819
+ }
812
820
  } finally {
813
821
  try {
814
822
  await prepared.dispose?.();
@@ -241,7 +241,6 @@ export interface DurablePrincipalAuthority {
241
241
  * object. Public layers must not forge opaque principal shapes (#636).
242
242
  */
243
243
  seal(coordinates: DurablePrincipalCoordinates): DurablePrincipal;
244
- isAvailable(principal: DurablePrincipal): Promise<boolean>;
245
244
  decode(principal: unknown): DurablePrincipalCoordinates;
246
245
  }
247
246
 
@@ -23,7 +23,6 @@ export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = O
23
23
  modelFlag: "--model",
24
24
  }),
25
25
  modelPassing: "argv",
26
- boundResume: "session/load",
27
26
  sessionBindingFile: "grok-acp-session.json",
28
27
  }),
29
28
  /**
@@ -41,7 +40,6 @@ export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = O
41
40
  thinkingFlag: "--reasoning",
42
41
  }),
43
42
  modelPassing: "set_model",
44
- boundResume: "session/load",
45
43
  sessionBindingFile: "hermes-acp-session.json",
46
44
  seatProfileSoul: Object.freeze({
47
45
  flag: "-p",
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Exact current-invocation principal for Navigator attendance correlation.
2
+ * Exact current-invocation principal for shared role lifecycle (#1087).
3
3
  * Shared role lifecycle owns one opaque uuidv7 per Role invocation and persists
4
4
  * it on the role session via Pi's guaranteed `pi.appendEntry` boundary.
5
5
  *
@@ -7,21 +7,14 @@
7
7
  * unfinished exact-session resume reuses the latest valid marker only when its
8
8
  * role/phase/subjectKey still match the expected identity; a packaged role
9
9
  * terminal completing that marker starts the next invocation (fresh mint).
10
- * Terminal settlement binds the nearest independent marker strictly before the
11
- * current durable packaged role terminal and compares equality — never attendance
12
- * self-shape, markers after the terminal, or stale older markers behind a
13
- * malformed nearest. Non-UUIDv7 principals are never accepted.
10
+ * Public settlement does not re-correlate attendance against this principal
11
+ * (ADR 0042); lifecycle dispose/grace owns call-boundary isolation (ADR 0084).
14
12
  *
15
13
  * One shared typed terminal classifier owns durable completion for lifecycle,
16
14
  * publicNavigatorSettlement, and every public CLI Receipt extractor:
17
15
  * - accepted/human: isError exactly false and no infrastructure-failure fact
18
16
  * - infrastructure: isError exactly true plus base kind/source/reasonCode identity
19
17
  * - retryable/missing/nonboolean/contradictory/malformed: nonterminal
20
- *
21
- * One truth table also owns marker↔terminal cardinality:
22
- * - a marker binds exactly one durable packaged role terminal
23
- * - multiple durable terminals after the same marker → ambiguous / fail-closed
24
- * - marker role/phase/subjectKey must match the terminal + independent expected
25
18
  */
26
19
 
27
20
  import { LEGACY_REVIEW_OUTPUT_ROLES, PACKAGED_ROLE_REGISTRY } from "./packaged-role-registry.ts";
@@ -147,7 +140,7 @@ export type InvocationMarkerIdentity = {
147
140
  readonly subjectKey: string;
148
141
  };
149
142
 
150
- /** Independently admitted / registry / cwd expected identity for resume and correlation. */
143
+ /** Independently admitted / registry / cwd expected identity for lifecycle resume. */
151
144
  export type ExpectedInvocationIdentity = {
152
145
  readonly role: string;
153
146
  readonly phase?: InvocationPhase;
@@ -342,83 +335,6 @@ export function findLatestDurablePackagedRoleTerminal(
342
335
  return undefined;
343
336
  }
344
337
 
345
- /**
346
- * Truth-table binding of the current durable packaged role terminal to its
347
- * owning invocation marker. Singleton cardinality: multiple durable terminals
348
- * after the same marker are ambiguous and fail closed.
349
- */
350
- export type DurableTerminalMarkerBinding =
351
- | {
352
- readonly kind: "bound";
353
- readonly terminal: DurablePackagedRoleTerminalRef;
354
- readonly marker: InvocationMarkerIdentity & { readonly index: number };
355
- }
356
- | {
357
- readonly kind: "unbound";
358
- readonly terminal: DurablePackagedRoleTerminalRef;
359
- }
360
- | { readonly kind: "absent" }
361
- | { readonly kind: "ambiguous" };
362
-
363
- export function bindCurrentDurableTerminalToMarker(
364
- entries: readonly NavigatorInvocationEntryLike[],
365
- ): DurableTerminalMarkerBinding {
366
- const terminal = findLatestDurablePackagedRoleTerminal(entries);
367
- if (terminal === undefined) return { kind: "absent" };
368
-
369
- let markerIndex = -1;
370
- for (let i = terminal.index - 1; i >= 0; i -= 1) {
371
- if (isInvocationMarkerEntry(entries[i])) {
372
- markerIndex = i;
373
- break;
374
- }
375
- }
376
- if (markerIndex < 0) {
377
- return { kind: "unbound", terminal };
378
- }
379
-
380
- const marker = parseInvocationMarkerIdentity(entries[markerIndex]?.data);
381
- if (marker === undefined) {
382
- // Malformed nearest marker: no stale fallback; terminal exists but is unbound.
383
- return { kind: "unbound", terminal };
384
- }
385
-
386
- let windowEnd = entries.length;
387
- for (let i = markerIndex + 1; i < entries.length; i += 1) {
388
- if (isInvocationMarkerEntry(entries[i])) {
389
- windowEnd = i;
390
- break;
391
- }
392
- }
393
-
394
- let durableCount = 0;
395
- for (let i = markerIndex + 1; i < windowEnd; i += 1) {
396
- if (durableTerminalAt(entries, i) !== undefined) durableCount += 1;
397
- }
398
- if (durableCount !== 1) return { kind: "ambiguous" };
399
- // Current terminal must be the singleton inside this marker window.
400
- if (terminal.index <= markerIndex || terminal.index >= windowEnd) {
401
- return { kind: "ambiguous" };
402
- }
403
-
404
- return {
405
- kind: "bound",
406
- terminal,
407
- marker: { ...marker, index: markerIndex },
408
- };
409
- }
410
-
411
- /**
412
- * Whether public Receipt settlement may proceed for the current durable terminal.
413
- * Only ambiguous multi-terminal bindings fail closed; attendance mismatch does not
414
- * pollute an otherwise legal single Receipt.
415
- */
416
- export function isReceiptSettlementBindingClear(
417
- entries: readonly NavigatorInvocationEntryLike[],
418
- ): boolean {
419
- return bindCurrentDurableTerminalToMarker(entries).kind !== "ambiguous";
420
- }
421
-
422
338
  export type LifecycleInvocationPrincipal = {
423
339
  readonly invocationId: string;
424
340
  /** True when the principal is already on the admitted session (resume; do not re-append). */
@@ -463,36 +379,3 @@ export function resolveLifecycleInvocationPrincipal(
463
379
 
464
380
  return { invocationId: marker.invocationId, resume: true };
465
381
  }
466
-
467
- /**
468
- * Exact current invocation principal already present on the role session.
469
- * `beforeIndex` is the current packaged role terminal index: only the nearest
470
- * marker strictly before that bound is applicable. A malformed nearest marker
471
- * fails closed (undefined) — never falls back to a stale older valid marker.
472
- * Markers at/after the terminal (future prepare/event) are never considered.
473
- * Non-UUIDv7 values are rejected.
474
- */
475
- export function currentInvocationPrincipalFromSession(
476
- entries: readonly NavigatorInvocationEntryLike[],
477
- beforeIndex: number = entries.length,
478
- ): string | undefined {
479
- return currentInvocationMarkerFromSession(entries, beforeIndex)?.invocationId;
480
- }
481
-
482
- /**
483
- * Full marker identity nearest before `beforeIndex` (current durable terminal).
484
- * Malformed nearest fails closed — no stale older fallback.
485
- */
486
- export function currentInvocationMarkerFromSession(
487
- entries: readonly NavigatorInvocationEntryLike[],
488
- beforeIndex: number = entries.length,
489
- ): InvocationMarkerIdentity | undefined {
490
- const limit = Math.min(Math.max(beforeIndex, 0), entries.length);
491
- for (let i = limit - 1; i >= 0; i -= 1) {
492
- const entry = entries[i];
493
- if (!isInvocationMarkerEntry(entry)) continue;
494
- // Nearest marker only — parse full identity or fail closed.
495
- return parseInvocationMarkerIdentity(entry?.data);
496
- }
497
- return undefined;
498
- }
@@ -214,9 +214,20 @@ export function createNativeNavigatorSessionFactory(deps?: {
214
214
  }
215
215
  if (outcome.kind === "failure") {
216
216
  providerFailure = navigatorProviderFailureFromPublicTerminal(outcome);
217
+ const failedAttempts = outcome.decisiveFacts.failedAttempts;
218
+ const attemptReasons = Array.isArray(failedAttempts)
219
+ ? failedAttempts.map((entry: unknown, index: number) => {
220
+ const fact = entry !== null && typeof entry === "object"
221
+ ? entry as { attempt?: unknown; diagnostic?: unknown }
222
+ : undefined;
223
+ return `${typeof fact?.attempt === "number" ? fact.attempt + 1 : index + 1}: ${
224
+ typeof fact?.diagnostic === "string" ? fact.diagnostic : outcome.diagnostic
225
+ }`;
226
+ })
227
+ : [];
217
228
  throw navigatorUnavailableError(
218
229
  providerFailure.source,
219
- new Error(outcome.diagnostic),
230
+ new Error(attemptReasons.length > 0 ? attemptReasons.join("\n") : outcome.diagnostic),
220
231
  providerFailure.cause,
221
232
  );
222
233
  }
@@ -1,4 +1,3 @@
1
- import { lstat } from "node:fs/promises";
2
1
  import { join } from "node:path";
3
2
 
4
3
  import type {
@@ -42,7 +41,7 @@ export function issuePiDurablePrincipalCoordinates(
42
41
  };
43
42
  }
44
43
 
45
- /** Pi's durable-principal codec and availability authority. */
44
+ /** Pi's durable-principal codec. */
46
45
  export const piDurablePrincipalAuthority: DurablePrincipalAuthority = {
47
46
  issue(request: NewDurablePrincipalRequest): DurablePrincipal {
48
47
  const coordinates = issuePiDurablePrincipalCoordinates(request);
@@ -73,13 +72,4 @@ export const piDurablePrincipalAuthority: DurablePrincipalAuthority = {
73
72
  : join(record.sessionDirectory, "session.jsonl"),
74
73
  };
75
74
  },
76
- async isAvailable(principal: DurablePrincipal): Promise<boolean> {
77
- const { sessionFile } = this.decode(principal);
78
- try {
79
- const stat = await lstat(sessionFile);
80
- return stat.isFile() && !stat.isSymbolicLink();
81
- } catch {
82
- return false;
83
- }
84
- },
85
75
  };
@@ -63,6 +63,6 @@ export function renderSystemPromptOverride(authority: {
63
63
  export type SessionIdentityAuthority = Readonly<{
64
64
  load(principal: RoleTurnRequest["principal"]): Promise<string | undefined>;
65
65
  bind(principal: RoleTurnRequest["principal"], sessionId: string): Promise<void>;
66
- /** Durable principal session path for layout ownership / isAvailable — not a rebuild source (#617 DK-4). */
66
+ /** Durable principal session path for layout ownership — not a rebuild source (#617 DK-4). */
67
67
  resolveSessionFile(principal: RoleTurnRequest["principal"]): string;
68
68
  }>;