@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.
- package/dist/acp-host/production-host.js +125 -217
- package/dist/headless-host/production-host.js +381 -470
- package/dist/host-descriptions.js +0 -2
- package/dist/navigator-invocation-identity.js +0 -57
- package/dist/navigator-public-session.js +6 -1
- package/dist/pi/durable-principal.js +1 -12
- package/dist/public-cli/auto-resume.js +59 -87
- package/dist/public-cli/instruction-seat-run.js +3 -3
- package/dist/public-cli/main.js +86 -206
- package/dist/public-cli/post-admission.js +7 -15
- package/dist/public-cli/run-lifecycle.js +5 -15
- package/dist/public-cli/settlement.js +10 -59
- package/dist/public-cli/terminal.js +4 -2
- package/dist/session-identity.js +0 -18
- package/package.json +1 -1
- package/src/acp-host/description.ts +0 -2
- package/src/acp-host/production-host.ts +0 -1
- package/src/acp-host/role-turn-host.ts +12 -6
- package/src/headless-host/role-turn-host.ts +11 -3
- package/src/host-contracts.ts +0 -1
- package/src/host-descriptions.ts +0 -2
- package/src/navigator-invocation-identity.ts +4 -121
- package/src/navigator-public-session.ts +12 -1
- package/src/pi/durable-principal.ts +1 -11
- package/src/prepared-role-turn.ts +1 -1
- package/src/public-cli/auto-resume.ts +67 -109
- package/src/public-cli/instruction-seat-run.ts +3 -3
- package/src/public-cli/post-admission.ts +7 -21
- package/src/public-cli/run-lifecycle.ts +3 -21
- package/src/public-cli/settlement.ts +8 -75
- package/src/public-cli/terminal.ts +4 -2
- package/src/role-envelope.ts +3 -3
- package/src/role-runtime.ts +23 -5
- 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
|
|
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
|
|
945
|
-
//
|
|
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
|
|
1272
|
-
const loaded = await loadResumableRunRecord(home, runId, authority
|
|
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,
|
|
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 /
|
|
1590
|
-
//
|
|
1591
|
-
//
|
|
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
|
|
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
|
|
1605
|
-
|
|
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.
|
|
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;
|
package/dist/session-identity.js
CHANGED
|
@@ -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
|
@@ -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
|
-
|
|
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"
|
|
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?.();
|
package/src/host-contracts.ts
CHANGED
|
@@ -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
|
|
package/src/host-descriptions.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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
|
|
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
|
|
66
|
+
/** Durable principal session path for layout ownership — not a rebuild source (#617 DK-4). */
|
|
67
67
|
resolveSessionFile(principal: RoleTurnRequest["principal"]): string;
|
|
68
68
|
}>;
|