@akagilnc/pi-workflow-roles 0.1.3902 → 0.1.3936
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/README.md +3 -3
- package/README.zh-CN.md +4 -4
- package/dist/acp-host/production-host.js +2540 -1994
- package/dist/compliance-transport.js +4 -1
- package/dist/headless-host/production-host.js +2274 -1728
- package/dist/prepared-role-turn.js +5 -0
- package/dist/public-cli/auto-resume.js +115 -13
- package/dist/public-cli/diarist-run.js +154 -0
- package/dist/public-cli/main.js +7082 -5854
- package/dist/public-cli/option-definitions.js +1 -1
- package/dist/public-cli/post-admission.js +455 -188
- package/dist/public-cli/role-turn-host-resolution.js +59 -0
- package/dist/public-cli/run-lifecycle.js +11 -6
- package/dist/public-cli/turn-request.js +1 -0
- package/dist/public-role-summons.js +48 -42
- package/dist/role-activation-flags.js +3 -0
- package/dist/role-runtime.js +14 -3
- package/dist/session-identity.js +58 -0
- package/package.json +1 -1
- package/src/compliance-transport.ts +6 -1
- package/src/host-contracts.ts +2 -0
- package/src/public-cli/auto-resume.ts +157 -11
- package/src/public-cli/cli.ts +25 -85
- package/src/public-cli/countersign-run.ts +26 -33
- package/src/public-cli/option-definitions.ts +1 -1
- package/src/public-cli/post-admission.ts +514 -159
- package/src/public-cli/reviewer-run.ts +0 -2
- package/src/public-cli/role-turn-host-resolution.ts +122 -0
- package/src/public-cli/run-lifecycle.ts +15 -10
- package/src/public-cli/turn-request.ts +3 -0
- package/src/public-role-summons.ts +71 -54
- package/src/role-activation-flags.ts +3 -0
- package/src/role-runtime.ts +14 -2
- package/src/session-identity.ts +34 -0
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
AUTO_RESUME_LIMIT,
|
|
26
26
|
describeErrorIdentity,
|
|
27
27
|
acquireRunWriterLease,
|
|
28
|
+
markRunResumable,
|
|
28
29
|
markRunTerminal,
|
|
29
30
|
RunWriterLeaseHeldError,
|
|
30
31
|
type RunWriterLease,
|
|
@@ -34,12 +35,42 @@ import { isLawfulTypedTerminalOutcome, formatTerminalResult, type TerminalArtifa
|
|
|
34
35
|
import {
|
|
35
36
|
presentFailureTerminal,
|
|
36
37
|
presentStructuralRejection,
|
|
38
|
+
resolveControlledFailureResumeObservation,
|
|
37
39
|
type SealedAcceptanceRedispatchDisposition,
|
|
38
40
|
} from "./settlement.ts";
|
|
39
41
|
import type { CliIo } from "./cli-io.ts";
|
|
40
42
|
|
|
41
43
|
const dummyIo: CliIo = { stdout: () => {}, stderr: () => {} };
|
|
42
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Persist run-state after a host-turn result, outside the retried dispatch try.
|
|
47
|
+
* Lawful settlement always seals terminal — a typed 429 observation must not
|
|
48
|
+
* win (#416 成功即停). 429 only marks resumable on the controlled-failure path.
|
|
49
|
+
* Write failure must escape the retried dispatch try instead of becoming a
|
|
50
|
+
* synthetic host-turn terminal.
|
|
51
|
+
*/
|
|
52
|
+
export async function persistReturnedRunState(
|
|
53
|
+
admitted: { runDirectory: string; principal?: DurablePrincipal },
|
|
54
|
+
authority: Pick<DurablePrincipalAuthority, "isAvailable">,
|
|
55
|
+
options?: { readonly lawful?: boolean },
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
if (options?.lawful === true) {
|
|
58
|
+
await markRunTerminal(admitted.runDirectory);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const resumeObservation = await resolveControlledFailureResumeObservation({
|
|
62
|
+
runDirectory: admitted.runDirectory,
|
|
63
|
+
});
|
|
64
|
+
const typedHttp429 = resumeObservation.typedHttp429;
|
|
65
|
+
if (admitted.principal !== undefined && typedHttp429 !== undefined) {
|
|
66
|
+
if (await authority.isAvailable(admitted.principal)) {
|
|
67
|
+
await markRunResumable(admitted.runDirectory, typedHttp429);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
await markRunTerminal(admitted.runDirectory);
|
|
72
|
+
}
|
|
73
|
+
|
|
43
74
|
function presentTerminal(terminal: TerminalResult, io: CliIo): void {
|
|
44
75
|
if (terminal.roleOutcome.kind === "failure" || terminal.roleOutcome.kind === "no_receipt") {
|
|
45
76
|
presentFailureTerminal(terminal, io);
|
|
@@ -67,8 +98,48 @@ async function finalizeExceptionRunBestEffort(runDirectory: string, io: CliIo):
|
|
|
67
98
|
export type AutoResumeDispatchResult = {
|
|
68
99
|
exitCode: number;
|
|
69
100
|
terminal?: TerminalResult;
|
|
101
|
+
/**
|
|
102
|
+
* Pre-turn settlement under the writer lease (station child exhausted).
|
|
103
|
+
* Loop presents this result and must not redispatch (#840 父子不层叠).
|
|
104
|
+
*/
|
|
105
|
+
skipAutoResume?: true;
|
|
106
|
+
/**
|
|
107
|
+
* Host turn actually started. Absent on beforeDispatch / pre-turn settlement
|
|
108
|
+
* so the loop retries the initial payload instead of a session resume.
|
|
109
|
+
*/
|
|
110
|
+
turnDispatched?: true;
|
|
111
|
+
/**
|
|
112
|
+
* Dispatch skipped run-state persist so this loop seam owns it.
|
|
113
|
+
* Absent when dispatch already persisted or never produced a terminal.
|
|
114
|
+
*/
|
|
115
|
+
needsPersist?: true;
|
|
70
116
|
};
|
|
71
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Thrown by dispatchPostAdmissionTurn when a failure happens inside its own
|
|
120
|
+
* settlement authority (presentControlledFailure) after the host turn
|
|
121
|
+
* genuinely started — never to fabricate a replacement terminal (ADR 0080
|
|
122
|
+
* single-settlement-disposition: settlement stays exactly presentControlledFailure
|
|
123
|
+
* / settleFailureTerminalResult, or — once the retry budget is exhausted —
|
|
124
|
+
* this loop's own dispatchExceptionFailureTerminal). Its only job is to carry
|
|
125
|
+
* the "turn already started" fact across the throw boundary so this loop
|
|
126
|
+
* selects a resume payload on the next attempt instead of replaying the
|
|
127
|
+
* initial one (#840 r9 判词 class 1 boundary — 覆盖 executeTurn 已启动后至
|
|
128
|
+
* 返回带 turnDispatched 结果前的全部异常). `cause` is the true failure;
|
|
129
|
+
* retention below serializes it whole via the standard Error.cause chain.
|
|
130
|
+
*/
|
|
131
|
+
export class TurnDispatchedFailure extends Error {
|
|
132
|
+
override readonly name = "TurnDispatchedFailure";
|
|
133
|
+
constructor(cause: unknown) {
|
|
134
|
+
super(
|
|
135
|
+
`settlement failed after the host turn genuinely started: ${
|
|
136
|
+
cause instanceof Error ? cause.message : String(cause)
|
|
137
|
+
}`,
|
|
138
|
+
{ cause },
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
72
143
|
/** Session custom-entry type carrying the pointer to one dispatch error file. */
|
|
73
144
|
export const DISPATCH_ERROR_RETENTION_ENTRY_TYPE = "ak_run_dispatch_error_retention" as const;
|
|
74
145
|
|
|
@@ -80,27 +151,30 @@ function runArtifactsDirectory(runDirectory: string): string {
|
|
|
80
151
|
/**
|
|
81
152
|
* #182-A hardened path identity, mirrored from settlement.ts's
|
|
82
153
|
* ensureAuditEvidenceDirectory: a planted symlink at the run directory or the
|
|
83
|
-
* artifacts path must not receive
|
|
154
|
+
* artifacts path must not receive a durable run artifact (O_NOFOLLOW only
|
|
84
155
|
* protects the final file name; recursive mkdir would accept a symlinked
|
|
85
|
-
* parent). Fails loudly with the true cause instead.
|
|
156
|
+
* parent). Fails loudly with the true cause instead. Shared by every caller
|
|
157
|
+
* that needs a durable run-artifacts write independent of session/dossier
|
|
158
|
+
* health — dispatch-error retention here, and post-admission's best-effort
|
|
159
|
+
* cleanup diagnostic (#840 bounce class 2).
|
|
86
160
|
*/
|
|
87
|
-
async function ensureRealArtifactsDirectory(runDirectory: string): Promise<string> {
|
|
161
|
+
export async function ensureRealArtifactsDirectory(runDirectory: string): Promise<string> {
|
|
88
162
|
const runStat = await lstat(runDirectory);
|
|
89
163
|
if (runStat.isSymbolicLink() || !runStat.isDirectory()) {
|
|
90
|
-
throw new Error("
|
|
164
|
+
throw new Error("run artifact retention: run directory is not a real directory");
|
|
91
165
|
}
|
|
92
166
|
const artifactsDir = runArtifactsDirectory(runDirectory);
|
|
93
167
|
try {
|
|
94
168
|
const existing = await lstat(artifactsDir);
|
|
95
169
|
if (existing.isSymbolicLink() || !existing.isDirectory()) {
|
|
96
|
-
throw new Error("
|
|
170
|
+
throw new Error("run artifact retention: artifacts path is not a real directory");
|
|
97
171
|
}
|
|
98
172
|
} catch (error) {
|
|
99
173
|
if (!isMissingPathError(error)) throw error;
|
|
100
174
|
await mkdir(artifactsDir, { recursive: true });
|
|
101
175
|
const created = await lstat(artifactsDir);
|
|
102
176
|
if (created.isSymbolicLink() || !created.isDirectory()) {
|
|
103
|
-
throw new Error("
|
|
177
|
+
throw new Error("run artifact retention: artifacts directory is not a real directory");
|
|
104
178
|
}
|
|
105
179
|
}
|
|
106
180
|
return artifactsDir;
|
|
@@ -268,6 +342,23 @@ async function retainDispatchError(
|
|
|
268
342
|
return pointerError === undefined ? { file: filePath } : { file: filePath, pointerError };
|
|
269
343
|
}
|
|
270
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Unwrap TurnDispatchedFailure before this loop's own final presentation
|
|
347
|
+
* (#840 r9 判词 class 1 — the auto-resume.ts final presentation boundary).
|
|
348
|
+
* The wrapper only carries the "turn already genuinely started" signal
|
|
349
|
+
* across the throw boundary so the loop above selects a resume payload; the
|
|
350
|
+
* typed decisiveFacts and diagnostic text below must name the real cause it
|
|
351
|
+
* wraps (接住可以,洗白不行 — 未识别异常不得冒用具体标签,真因必须落痕), not
|
|
352
|
+
* the internal signal's own identity.
|
|
353
|
+
*/
|
|
354
|
+
function unwrapTurnDispatchedFailure(error: unknown): unknown {
|
|
355
|
+
let current = error;
|
|
356
|
+
while (current instanceof TurnDispatchedFailure) {
|
|
357
|
+
current = current.cause;
|
|
358
|
+
}
|
|
359
|
+
return current;
|
|
360
|
+
}
|
|
361
|
+
|
|
271
362
|
/**
|
|
272
363
|
* Typed failure terminal for a retry path that ended with only exceptions:
|
|
273
364
|
* loud, non-lawful, carrying the last true cause and the pointers to the
|
|
@@ -283,12 +374,13 @@ function dispatchExceptionFailureTerminal(input: {
|
|
|
283
374
|
/** True only when every attempt threw; otherwise describe just the final attempt. */
|
|
284
375
|
everyAttemptThrew: boolean;
|
|
285
376
|
}): TerminalResult {
|
|
377
|
+
const causeError = unwrapTurnDispatchedFailure(input.causeError);
|
|
286
378
|
// #426 review: this terminal fires whenever the FINAL dispatch throws, not
|
|
287
379
|
// only when every attempt threw — do not misrepresent a mixed retry history.
|
|
288
380
|
const history = input.everyAttemptThrew
|
|
289
381
|
? "dispatch threw an exception on every attempt"
|
|
290
382
|
: "the final dispatch threw an exception";
|
|
291
|
-
const diagnostic = `${history} (${input.endReason}; resumes used ${input.autoResumeAttempts}); last cause: ${describeErrorIdentity(
|
|
383
|
+
const diagnostic = `${history} (${input.endReason}; resumes used ${input.autoResumeAttempts}); last cause: ${describeErrorIdentity(causeError)}`;
|
|
292
384
|
const decisiveFacts: Record<string, unknown> = {
|
|
293
385
|
cause: "unrecognized",
|
|
294
386
|
diagnostic,
|
|
@@ -298,7 +390,7 @@ function dispatchExceptionFailureTerminal(input: {
|
|
|
298
390
|
if (input.errorFiles.length > 0) {
|
|
299
391
|
decisiveFacts.lastDispatchErrorFile = input.errorFiles[input.errorFiles.length - 1];
|
|
300
392
|
}
|
|
301
|
-
const candidate =
|
|
393
|
+
const candidate = causeError as { name?: unknown; code?: unknown };
|
|
302
394
|
if (typeof candidate?.name === "string") decisiveFacts.errorName = candidate.name;
|
|
303
395
|
if (typeof candidate?.code === "string" || typeof candidate?.code === "number") {
|
|
304
396
|
decisiveFacts.errorCode = candidate.code;
|
|
@@ -334,6 +426,16 @@ export async function runWithAutoResumeLoop<
|
|
|
334
426
|
principal: DurablePrincipal;
|
|
335
427
|
};
|
|
336
428
|
principalAuthority: DurablePrincipalAuthority;
|
|
429
|
+
/**
|
|
430
|
+
* Host-aware "can this principal's turn still be resumed" probe (#840 P1).
|
|
431
|
+
* Callers that know the selected host pass
|
|
432
|
+
* session-identity.ts's resolveHostAwareSessionAvailability(host,
|
|
433
|
+
* principalAuthority) so an ACP/headless turn's own binding file — not
|
|
434
|
+
* pi's session.jsonl — decides continuation for that host. Defaults to
|
|
435
|
+
* principalAuthority.isAvailable (pi-only check) when omitted, so a
|
|
436
|
+
* caller that never leaves the pi host keeps identical behavior.
|
|
437
|
+
*/
|
|
438
|
+
isPrincipalAvailable?: (principal: DurablePrincipal) => Promise<boolean>;
|
|
337
439
|
io: CliIo;
|
|
338
440
|
sessionAppender: SessionCustomEntryAppender;
|
|
339
441
|
/**
|
|
@@ -358,6 +460,9 @@ export async function runWithAutoResumeLoop<
|
|
|
358
460
|
// `attempts >= limit` comparison (always false) — reject here, before any dispatch.
|
|
359
461
|
const limit = options.autoResumeLimit ?? AUTO_RESUME_LIMIT;
|
|
360
462
|
parseAutoResumeLimit(limit);
|
|
463
|
+
const isPrincipalAvailable =
|
|
464
|
+
options.isPrincipalAvailable ??
|
|
465
|
+
((principal: DurablePrincipal) => options.principalAuthority.isAvailable(principal));
|
|
361
466
|
let autoResumeAttempts = 0;
|
|
362
467
|
let isFirst = true;
|
|
363
468
|
let currentPayload = options.buildInitialPayload();
|
|
@@ -381,6 +486,11 @@ export async function runWithAutoResumeLoop<
|
|
|
381
486
|
}
|
|
382
487
|
|
|
383
488
|
let result: T | undefined;
|
|
489
|
+
// Set only when the caught throw is a TurnDispatchedFailure (#840 r9 判词
|
|
490
|
+
// class 1): the host turn genuinely started this attempt even though
|
|
491
|
+
// dispatch produced no result — the next payload must still be a resume,
|
|
492
|
+
// not a replay of the initial one.
|
|
493
|
+
let turnStartedBeforeThrow = false;
|
|
384
494
|
try {
|
|
385
495
|
result = await options.dispatch(currentPayload, lease, isFirst, dummyIo);
|
|
386
496
|
} catch (error) {
|
|
@@ -390,6 +500,7 @@ export async function runWithAutoResumeLoop<
|
|
|
390
500
|
// diagnostic-sink-isolation precedent). The dispatcher owns lease release
|
|
391
501
|
// in its own finally, so the retry round starts with the lock free.
|
|
392
502
|
lastThrownError = error;
|
|
503
|
+
turnStartedBeforeThrow = error instanceof TurnDispatchedFailure;
|
|
393
504
|
const attempt = dispatchOrdinal;
|
|
394
505
|
try {
|
|
395
506
|
// Track the file as soon as it is durably written (#426 review):
|
|
@@ -419,6 +530,7 @@ export async function runWithAutoResumeLoop<
|
|
|
419
530
|
}
|
|
420
531
|
dispatchOrdinal += 1;
|
|
421
532
|
|
|
533
|
+
let persistFailure: { readonly error: unknown } | undefined;
|
|
422
534
|
if (result !== undefined) {
|
|
423
535
|
everyAttemptThrew = false;
|
|
424
536
|
const terminal = (result as { terminal?: TerminalResult }).terminal;
|
|
@@ -427,6 +539,23 @@ export async function runWithAutoResumeLoop<
|
|
|
427
539
|
}
|
|
428
540
|
|
|
429
541
|
const lawful = terminal !== undefined && isLawfulTypedTerminalOutcome(terminal.roleOutcome);
|
|
542
|
+
if (result.needsPersist === true) {
|
|
543
|
+
// Persist on the loop seam, not inside retried dispatch.
|
|
544
|
+
try {
|
|
545
|
+
await persistReturnedRunState(
|
|
546
|
+
options.admitted,
|
|
547
|
+
{ isAvailable: isPrincipalAvailable },
|
|
548
|
+
lawful ? { lawful: true } : undefined,
|
|
549
|
+
);
|
|
550
|
+
} catch (persistError) {
|
|
551
|
+
// Lawful persist stays loud (no fake terminal). Non-lawful persist is
|
|
552
|
+
// not a host-turn failure — do not auto-resume it — but sealed stop
|
|
553
|
+
// must still see the already-settled result (#648).
|
|
554
|
+
if (lawful) throw persistError;
|
|
555
|
+
persistFailure = { error: persistError };
|
|
556
|
+
lastThrownError = persistError;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
430
559
|
if (lawful) {
|
|
431
560
|
if (terminal !== undefined) {
|
|
432
561
|
// Present lawful terminal once to real io (dummy was used inside dispatch)
|
|
@@ -434,6 +563,10 @@ export async function runWithAutoResumeLoop<
|
|
|
434
563
|
}
|
|
435
564
|
return result;
|
|
436
565
|
}
|
|
566
|
+
if (result.skipAutoResume === true) {
|
|
567
|
+
if (terminal !== undefined) presentTerminal(terminal, options.io);
|
|
568
|
+
return result;
|
|
569
|
+
}
|
|
437
570
|
}
|
|
438
571
|
|
|
439
572
|
// Settlement-owned sealed disposition before any redispatch (#648 / #672):
|
|
@@ -471,13 +604,20 @@ export async function runWithAutoResumeLoop<
|
|
|
471
604
|
}
|
|
472
605
|
}
|
|
473
606
|
|
|
607
|
+
if (persistFailure !== undefined) {
|
|
608
|
+
throw persistFailure.error;
|
|
609
|
+
}
|
|
610
|
+
|
|
474
611
|
if (result !== undefined) {
|
|
475
612
|
const terminal = (result as { terminal?: TerminalResult }).terminal;
|
|
476
613
|
if (autoResumeAttempts >= limit) {
|
|
477
614
|
if (terminal !== undefined) presentTerminal(terminal, options.io);
|
|
478
615
|
return result;
|
|
479
616
|
}
|
|
480
|
-
if (
|
|
617
|
+
if (
|
|
618
|
+
result.turnDispatched === true
|
|
619
|
+
&& !(await isPrincipalAvailable(options.admitted.principal))
|
|
620
|
+
) {
|
|
481
621
|
if (terminal !== undefined) presentTerminal(terminal, options.io);
|
|
482
622
|
return result;
|
|
483
623
|
}
|
|
@@ -500,7 +640,7 @@ export async function runWithAutoResumeLoop<
|
|
|
500
640
|
terminal,
|
|
501
641
|
} as T;
|
|
502
642
|
}
|
|
503
|
-
if (!(await
|
|
643
|
+
if (!(await isPrincipalAvailable(options.admitted.principal))) {
|
|
504
644
|
const terminal = dispatchExceptionFailureTerminal({
|
|
505
645
|
role: options.admitted.role,
|
|
506
646
|
runId: options.admitted.runId,
|
|
@@ -520,7 +660,13 @@ export async function runWithAutoResumeLoop<
|
|
|
520
660
|
}
|
|
521
661
|
|
|
522
662
|
autoResumeAttempts++;
|
|
523
|
-
|
|
663
|
+
// Resume payload only after a host turn actually started — either a
|
|
664
|
+
// returned result says so, or the attempt threw a TurnDispatchedFailure
|
|
665
|
+
// after genuinely starting the turn (#840 r9 判词 class 1). Pre-turn
|
|
666
|
+
// throws and beforeDispatch failures retry the initial payload (#840 / #416).
|
|
667
|
+
if (result?.turnDispatched === true || turnStartedBeforeThrow) {
|
|
668
|
+
currentPayload = options.buildResumePayload();
|
|
669
|
+
}
|
|
524
670
|
isFirst = false;
|
|
525
671
|
}
|
|
526
672
|
}
|
package/src/public-cli/cli.ts
CHANGED
|
@@ -44,14 +44,16 @@ import { CliUsageError } from "./cli-errors.ts";
|
|
|
44
44
|
import type { CliIo } from "./cli-io.ts";
|
|
45
45
|
import type { PostAdmissionEnv } from "./post-admission.ts";
|
|
46
46
|
import type { RoleTurnHost } from "../host-contracts.ts";
|
|
47
|
-
import {
|
|
48
|
-
import { createLazyProductionExternalHost } from "./load-production-external-host.ts";
|
|
47
|
+
import { appendPiSessionCustomEntry } from "../pi/role-turn-host.ts";
|
|
49
48
|
import {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
composeRoleTurnHostAdapters,
|
|
50
|
+
formatHostSelectionFailure,
|
|
51
|
+
HostSelectionError,
|
|
52
|
+
selectRoleTurnHost,
|
|
53
|
+
type HostSelectionFailure,
|
|
54
|
+
type NamedRoleTurnHostAdapter,
|
|
55
|
+
} from "./role-turn-host-resolution.ts";
|
|
53
56
|
import {
|
|
54
|
-
observeLaunchedRolePackageIdentity,
|
|
55
57
|
parseAnalystArgv,
|
|
56
58
|
parseCoderArgv,
|
|
57
59
|
parseCollectorArgv,
|
|
@@ -68,8 +70,6 @@ import {
|
|
|
68
70
|
parseAuditorArgv,
|
|
69
71
|
parseNotaryArgv,
|
|
70
72
|
parseReviewerArgv,
|
|
71
|
-
recordLaunchedPiIdentity,
|
|
72
|
-
recordLaunchedRolePackageIdentity,
|
|
73
73
|
} from "./invocation.ts";
|
|
74
74
|
import {
|
|
75
75
|
createTypedOptionConsumer,
|
|
@@ -237,24 +237,7 @@ function takePublicGlobalFlag(
|
|
|
237
237
|
return undefined;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
export type HostSelectionFailure
|
|
241
|
-
readonly kind: "host-unregistered" | "host-model-mismatch";
|
|
242
|
-
readonly host: string;
|
|
243
|
-
readonly seat: PublicCallableRole;
|
|
244
|
-
readonly model: string;
|
|
245
|
-
readonly registeredHosts: readonly string[];
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
export type NamedRoleTurnHostAdapter = {
|
|
249
|
-
readonly name: string;
|
|
250
|
-
readonly create: (input: { role: PublicCallableRole; model: EffectiveSeat["selection"] }) =>
|
|
251
|
-
| { readonly ok: true; readonly host: RoleTurnHost }
|
|
252
|
-
| { readonly ok: false };
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
class HostSelectionError extends Error {
|
|
256
|
-
constructor(readonly failure: HostSelectionFailure) { super(failure.kind); }
|
|
257
|
-
}
|
|
240
|
+
export type { HostSelectionFailure, NamedRoleTurnHostAdapter };
|
|
258
241
|
|
|
259
242
|
export type CliEnv = {
|
|
260
243
|
home?: string;
|
|
@@ -315,59 +298,7 @@ export type CliEnv = {
|
|
|
315
298
|
freshSummons?: true;
|
|
316
299
|
};
|
|
317
300
|
|
|
318
|
-
|
|
319
|
-
function resolveRoleTurnHost(
|
|
320
|
-
env: CliEnv,
|
|
321
|
-
options: {
|
|
322
|
-
role: PublicCallableRole;
|
|
323
|
-
seat: EffectiveSeat;
|
|
324
|
-
principalAuthority: DurablePrincipalAuthority;
|
|
325
|
-
extraPiArgs?: readonly string[];
|
|
326
|
-
timeoutMs?: number;
|
|
327
|
-
},
|
|
328
|
-
): RoleTurnHost {
|
|
329
|
-
const piHost = env.roleTurnHost ?? createPiRoleTurnHost({
|
|
330
|
-
packageRoot: env.packageRoot,
|
|
331
|
-
principalAuthority: options.principalAuthority,
|
|
332
|
-
...(options.extraPiArgs === undefined ? {} : { extraPiArgs: options.extraPiArgs }),
|
|
333
|
-
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
334
|
-
recordLaunchedPiIdentity,
|
|
335
|
-
recordLaunchedRolePackageIdentity,
|
|
336
|
-
observeLaunchedRolePackageIdentity,
|
|
337
|
-
});
|
|
338
|
-
// Composition-root adapter table: pi (in-process default) + one lazy external host per description-table key (#820).
|
|
339
|
-
const adapters: readonly NamedRoleTurnHostAdapter[] = env.hostAdapters ?? [
|
|
340
|
-
{ name: "pi", create: () => ({ ok: true as const, host: piHost }) },
|
|
341
|
-
...packagedExternalHostNames().map((name) => ({
|
|
342
|
-
name,
|
|
343
|
-
create: () => ({
|
|
344
|
-
ok: true as const,
|
|
345
|
-
// Factory loads outside the public bin static graph (ADR 0052 peer-free discovery).
|
|
346
|
-
host: createLazyProductionExternalHost({
|
|
347
|
-
packageRoot: env.packageRoot,
|
|
348
|
-
hostName: name,
|
|
349
|
-
principalAuthority: options.principalAuthority,
|
|
350
|
-
}),
|
|
351
|
-
}),
|
|
352
|
-
})),
|
|
353
|
-
];
|
|
354
|
-
const hostName = options.seat.host;
|
|
355
|
-
const adapter = adapters.find((candidate) => candidate.name === hostName);
|
|
356
|
-
const model = options.seat.selection === undefined ? "unconfigured" : `${options.seat.selection.provider}/${options.seat.selection.model}`;
|
|
357
|
-
const registeredHosts = adapters.map(({ name }) => name);
|
|
358
|
-
if (adapter === undefined) {
|
|
359
|
-
throw new HostSelectionError(
|
|
360
|
-
{ kind: "host-unregistered", host: hostName, seat: options.role, model, registeredHosts },
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
const selected = adapter.create({ role: options.role, model: options.seat.selection });
|
|
364
|
-
if (!selected.ok) {
|
|
365
|
-
throw new HostSelectionError(
|
|
366
|
-
{ kind: "host-model-mismatch", host: hostName, seat: options.role, model, registeredHosts },
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
return selected.host;
|
|
370
|
-
}
|
|
301
|
+
|
|
371
302
|
|
|
372
303
|
type RoleEnvironmentOptions = {
|
|
373
304
|
role: PublicCallableRole;
|
|
@@ -426,12 +357,21 @@ function createRoleEnvironment(
|
|
|
426
357
|
// #788 expectation 2: select a registered host first; only then project the
|
|
427
358
|
// host-facing provider (table > unique directory > fail). Bad host never
|
|
428
359
|
// reaches model resolution.
|
|
429
|
-
|
|
360
|
+
// Nested summons reuse this table and select by the child seat — never the
|
|
361
|
+
// already-selected parent adapter (#840 / ADR 0082 host-flag-two-channels).
|
|
362
|
+
const hostAdapters = composeRoleTurnHostAdapters(
|
|
363
|
+
{
|
|
364
|
+
packageRoot: env.packageRoot,
|
|
365
|
+
...(env.roleTurnHost === undefined ? {} : { roleTurnHost: env.roleTurnHost }),
|
|
366
|
+
...(env.hostAdapters === undefined ? {} : { hostAdapters: env.hostAdapters }),
|
|
367
|
+
...(extraPiArgs === undefined ? {} : { extraPiArgs }),
|
|
368
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
369
|
+
},
|
|
370
|
+
env.principalAuthority!,
|
|
371
|
+
);
|
|
372
|
+
const roleTurnHost = selectRoleTurnHost(hostAdapters, {
|
|
430
373
|
role,
|
|
431
374
|
seat: options.seat,
|
|
432
|
-
principalAuthority: env.principalAuthority!,
|
|
433
|
-
...(extraPiArgs === undefined ? {} : { extraPiArgs }),
|
|
434
|
-
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
435
375
|
});
|
|
436
376
|
const hostFacingSelection =
|
|
437
377
|
options.seat.selection === undefined
|
|
@@ -449,6 +389,7 @@ function createRoleEnvironment(
|
|
|
449
389
|
sessionAppender: appendPiSessionCustomEntry,
|
|
450
390
|
packageRoot: env.packageRoot,
|
|
451
391
|
roleTurnHost,
|
|
392
|
+
hostAdapters,
|
|
452
393
|
cwd: options.cwd,
|
|
453
394
|
...(options.credentials === undefined ? {} : { credentials: options.credentials }),
|
|
454
395
|
...(env.correlationId === undefined ? {} : { correlationId: env.correlationId }),
|
|
@@ -1606,8 +1547,7 @@ export async function runAkRole(
|
|
|
1606
1547
|
throw new CliUsageError(`unknown command: ${parsed.command}`);
|
|
1607
1548
|
} catch (error) {
|
|
1608
1549
|
if (error instanceof HostSelectionError) {
|
|
1609
|
-
|
|
1610
|
-
io.stderr(formatCliDiagnostic(`${error.failure.kind}: ${error.failure.host}; registered: ${registered}`));
|
|
1550
|
+
io.stderr(formatCliDiagnostic(formatHostSelectionFailure(error.failure)));
|
|
1611
1551
|
return { exitCode: 1, hostFailure: error.failure };
|
|
1612
1552
|
}
|
|
1613
1553
|
if (error instanceof CliUsageError) {
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
admitCountersignInvocation,
|
|
30
30
|
bindAdmittedTicketNumber,
|
|
31
31
|
buildCountersignTransportPrompt,
|
|
32
|
-
parseDiaristArgv,
|
|
33
32
|
type AdmittedCountersignInvocation,
|
|
34
33
|
type ParseCountersignArgvResult,
|
|
35
34
|
} from "./invocation.ts";
|
|
@@ -40,6 +39,7 @@ import {
|
|
|
40
39
|
type PostAdmissionEnv,
|
|
41
40
|
runPostAdmissionSeatResume,
|
|
42
41
|
resumeTurnRequestProjectionOptions,
|
|
42
|
+
StationChildExhaustedError,
|
|
43
43
|
} from "./post-admission.ts";
|
|
44
44
|
import {
|
|
45
45
|
loadResumableCountersignRun,
|
|
@@ -125,32 +125,24 @@ async function invokeCourtDiarist(input: {
|
|
|
125
125
|
},
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
-
const {
|
|
129
|
-
const result = await
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// Typed handoff only — never derived from summons prose.
|
|
147
|
-
...(input.boundTicketNumber === undefined
|
|
148
|
-
? {}
|
|
149
|
-
: { boundTicketNumber: input.boundTicketNumber }),
|
|
150
|
-
},
|
|
151
|
-
quietIo,
|
|
152
|
-
parseDiaristArgv,
|
|
153
|
-
);
|
|
128
|
+
const { summonPublicRole } = await import("../public-role-summons.ts");
|
|
129
|
+
const result = await summonPublicRole({
|
|
130
|
+
role: "diarist",
|
|
131
|
+
argv: ["--project", input.projectRoot, input.instruction],
|
|
132
|
+
cwd: env.cwd,
|
|
133
|
+
home: env.home,
|
|
134
|
+
agentDir: env.agentDir,
|
|
135
|
+
packageRoot: env.packageRoot,
|
|
136
|
+
io: quietIo,
|
|
137
|
+
...(env.credentials === undefined ? {} : { credentials: env.credentials }),
|
|
138
|
+
...(env.signal === undefined ? {} : { signal: env.signal }),
|
|
139
|
+
...(input.boundTicketNumber === undefined
|
|
140
|
+
? {}
|
|
141
|
+
: { boundTicketNumber: input.boundTicketNumber }),
|
|
142
|
+
// Child seat selects from the composition-root table. Do not pass the
|
|
143
|
+
// already-selected parent adapter (#840 / ADR 0082 host-flag-two-channels).
|
|
144
|
+
...(env.hostAdapters === undefined ? {} : { hostAdapters: env.hostAdapters }),
|
|
145
|
+
});
|
|
154
146
|
|
|
155
147
|
const roleOutcome = result.terminal?.roleOutcome;
|
|
156
148
|
// Discriminated union: only non-failure arms carry status (failure uses cause).
|
|
@@ -168,7 +160,7 @@ async function invokeCourtDiarist(input: {
|
|
|
168
160
|
const diagnostic =
|
|
169
161
|
roleOutcome?.kind === "failure"
|
|
170
162
|
? roleOutcome.diagnostic
|
|
171
|
-
: `exit ${result.exitCode}`;
|
|
163
|
+
: result.stderr?.trim() || `exit ${result.exitCode}`;
|
|
172
164
|
return {
|
|
173
165
|
identity: { kind: "unbound" },
|
|
174
166
|
failedWithoutEscalate: {
|
|
@@ -177,7 +169,7 @@ async function invokeCourtDiarist(input: {
|
|
|
177
169
|
};
|
|
178
170
|
}
|
|
179
171
|
|
|
180
|
-
const asserted = result.admitted?.ticketNumber;
|
|
172
|
+
const asserted = (result.admitted as { ticketNumber?: number } | undefined)?.ticketNumber;
|
|
181
173
|
if (
|
|
182
174
|
typeof asserted === "number" &&
|
|
183
175
|
Number.isSafeInteger(asserted) &&
|
|
@@ -225,12 +217,12 @@ export async function runCountersignCourtDiaristStation(
|
|
|
225
217
|
);
|
|
226
218
|
|
|
227
219
|
if (outcome.identity.kind === "escalate") {
|
|
228
|
-
throw new
|
|
220
|
+
throw new StationChildExhaustedError(
|
|
229
221
|
`court diarist station escalated (cannot identify court target): ${outcome.identity.reason}`,
|
|
230
222
|
);
|
|
231
223
|
}
|
|
232
224
|
if (outcome.failedWithoutEscalate !== undefined) {
|
|
233
|
-
throw new
|
|
225
|
+
throw new StationChildExhaustedError(outcome.failedWithoutEscalate.diagnostic);
|
|
234
226
|
}
|
|
235
227
|
}
|
|
236
228
|
|
|
@@ -282,8 +274,9 @@ export async function runPublicCountersign(
|
|
|
282
274
|
// (never mechanical matching of summons text). Resolve that typed key after
|
|
283
275
|
// admit so the countersign run page exists first; same-ticket re-summons
|
|
284
276
|
// resume the prior run on the typed key (unused mint is abandoned). The test
|
|
285
|
-
// seam `runCourtDiaristStation` defers identity to beforeDispatch
|
|
286
|
-
//
|
|
277
|
+
// seam `runCourtDiaristStation` defers identity to beforeDispatch; generic
|
|
278
|
+
// hook failures stay on the parent call-local budget, exhausted nested
|
|
279
|
+
// station children still skip parent auto-resume (#840 父子不层叠).
|
|
287
280
|
let typedTicket: number | undefined;
|
|
288
281
|
let identityDiaristRan = false;
|
|
289
282
|
|
|
@@ -1141,7 +1141,7 @@ export type PublicCommandHelpFacts = {
|
|
|
1141
1141
|
* the role list like every other role).
|
|
1142
1142
|
*/
|
|
1143
1143
|
export const PUBLIC_NAVIGATOR_HELP_NOTE =
|
|
1144
|
-
"Navigator also attends automatically on
|
|
1144
|
+
"Navigator also attends automatically on top-level public entry legs; that sidecar is unchanged by direct `ak-role navigator` calls." as const;
|
|
1145
1145
|
|
|
1146
1146
|
const TOP_LEVEL_HELP = {
|
|
1147
1147
|
command: "top",
|