@akagilnc/pi-workflow-roles 0.1.3565 → 0.1.3621
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 +4 -1
- package/README.zh-CN.md +1 -1
- package/dist/acp-host/production-host.js +18477 -8099
- package/dist/analyst-gate-cycles-read.js +387 -0
- package/dist/archivist-record-entry.js +22 -1
- package/dist/audit-escalation.js +6 -0
- package/dist/auditor-soul.js +74 -0
- package/dist/collector-config.js +86 -0
- package/dist/collector-evidence.js +316 -0
- package/dist/collector-github.js +527 -0
- package/dist/collector-ledger.js +853 -0
- package/dist/collector-tool-schemas.js +33 -0
- package/dist/compliance-transport.js +104 -52
- package/dist/diarist-contracts.js +72 -0
- package/dist/diarist-mechanical.js +411 -0
- package/dist/diarist-ticket-resolution.js +177 -0
- package/dist/diarist.js +311 -0
- package/dist/doctor-auditor.js +25 -0
- package/dist/doctor-contracts.js +2 -0
- package/dist/doctor-evidence.js +142 -0
- package/dist/gatekeeper-role.js +104 -81
- package/dist/host-transition-prior-native.js +78 -0
- package/dist/institutional-resolution.js +1 -95
- package/dist/judge-auditor.js +26 -0
- package/dist/ledger-session-read.js +227 -0
- package/dist/merger-git-state.js +78 -0
- package/dist/navigator-attendance.js +15 -7
- package/dist/navigator-public-session.js +170 -0
- package/dist/navigator-session-contracts.js +36 -40
- package/dist/notary-source-run.js +122 -0
- package/dist/package-contracts/auditor-output.js +66 -0
- package/dist/package-contracts/evidence-child-output.js +34 -0
- package/dist/package-contracts/judge-output.js +2 -0
- package/dist/package-contracts/terminating-tools.js +44 -3
- package/dist/package-resources/method-skill.js +254 -0
- package/dist/packaged-role-registry.js +56 -0
- package/dist/pi/durable-principal.js +61 -0
- package/dist/pi/in-process-session.js +25 -6
- package/dist/pi/known-failure.js +52 -0
- package/dist/pi/role-turn-host.js +430 -0
- package/dist/public-cli/auto-resume.js +414 -0
- package/dist/public-cli/cli-errors.js +8 -0
- package/dist/public-cli/cli-io.js +1 -0
- package/dist/public-cli/command-renderer.js +5 -0
- package/dist/public-cli/doctor-run.js +84 -0
- package/dist/public-cli/inspector-run.js +136 -0
- package/dist/public-cli/instruction-seat-run.js +256 -0
- package/dist/public-cli/invocation.js +2225 -0
- package/dist/public-cli/judge-run.js +118 -0
- package/dist/public-cli/load-production-acp-host.js +40 -0
- package/dist/public-cli/main.js +1511 -1124
- package/dist/public-cli/notary-run.js +161 -0
- package/dist/public-cli/option-definitions.js +1192 -0
- package/dist/public-cli/post-admission.js +667 -0
- package/dist/public-cli/public-run-credentials.js +49 -0
- package/dist/public-cli/registry.js +8 -2
- package/dist/public-cli/reviewer-dispatch-rejection.js +77 -0
- package/dist/public-cli/run-lifecycle.js +1344 -0
- package/dist/public-cli/seat-ticket-binding.js +113 -0
- package/dist/public-cli/settlement.js +3441 -0
- package/dist/public-cli/terminal.js +135 -0
- package/dist/public-cli/turn-request.js +25 -0
- package/dist/public-role-summons.js +301 -0
- package/dist/receipt-delivery-policy.js +10 -0
- package/dist/reviewer-child-executor.js +80 -8
- package/dist/reviewer-execution-ledger.js +2 -1
- package/dist/run-terminal-artifacts.js +195 -0
- package/dist/run-ticket-number.js +40 -0
- package/dist/session-assistant-usage.js +107 -0
- package/dist/shape-unreadable-failure.js +34 -0
- package/dist/submission-errors.js +3 -0
- package/dist/submission-ledger.js +388 -0
- package/dist/ticket-provenance-contracts.js +115 -0
- package/dist/ticket-provenance.js +340 -0
- package/extensions/role-runtime.ts +8 -0
- package/package.json +1 -1
- package/resources/diarist-collect.md +6 -10
- package/scripts/build-package.mjs +2 -1
- package/souls/diarist.md +9 -0
- package/souls/doctor-auditor.md +1 -0
- package/souls/evidence-child.md +1 -0
- package/src/acp-host/production-host.ts +4 -0
- package/src/acp-host/role-envelope.ts +4 -3
- package/src/acp-host/role-turn-host.ts +9 -1
- package/src/analyst-gate-cycles-read.ts +37 -2
- package/src/archivist-record-entry.ts +45 -1
- package/src/audit-escalation.ts +17 -0
- package/src/auditor-role.ts +22 -0
- package/src/auditor-soul.ts +42 -0
- package/src/compliance-transport.ts +177 -66
- package/src/diarist-contracts.ts +88 -0
- package/src/diarist-role.ts +60 -0
- package/src/diarist.ts +253 -179
- package/src/doctor-auditor.ts +10 -14
- package/src/doctor-contracts.ts +1 -0
- package/src/doctor-role.ts +2 -2
- package/src/evidence-child-role.ts +22 -0
- package/src/gatekeeper-pass-envelope.ts +109 -0
- package/src/gatekeeper-role.ts +145 -104
- package/src/host-contracts.ts +15 -2
- package/src/institutional-resolution.ts +8 -163
- package/src/judge-auditor.ts +10 -15
- package/src/judge-role.ts +8 -0
- package/src/navigator-attendance.ts +25 -9
- package/src/navigator-public-session.ts +217 -0
- package/src/navigator-session-contracts.ts +61 -54
- package/src/notary-source-run.ts +3 -1
- package/src/package-contracts/auditor-output.ts +82 -0
- package/src/package-contracts/evidence-child-output.ts +51 -0
- package/src/package-contracts/judge-output.ts +1 -0
- package/src/package-contracts/reviewer-output.ts +2 -2
- package/src/package-contracts/terminating-tools.ts +53 -3
- package/src/packaged-role-registry.ts +63 -0
- package/src/pi/adapter.ts +2 -1
- package/src/pi/in-process-session.ts +38 -12
- package/src/pi/role-turn-host.ts +42 -0
- package/src/public-cli/cli.ts +43 -6
- package/src/public-cli/countersign-run.ts +9 -129
- package/src/public-cli/diarist-run.ts +312 -0
- package/src/public-cli/instruction-seat-run.ts +289 -50
- package/src/public-cli/invocation.ts +87 -25
- package/src/public-cli/option-definitions.ts +79 -0
- package/src/public-cli/post-admission.ts +9 -3
- package/src/public-cli/registry.ts +7 -1
- package/src/public-cli/run-lifecycle.ts +53 -6
- package/src/public-cli/settlement.ts +170 -7
- package/src/public-cli/terminal.ts +4 -1
- package/src/public-role-summons.ts +431 -0
- package/src/receipt-delivery-policy.ts +10 -0
- package/src/reviewer-agent.ts +1 -1
- package/src/reviewer-child-executor.ts +114 -12
- package/src/reviewer-execution-ledger.ts +3 -2
- package/src/role-runtime.ts +275 -23
- package/src/session-assistant-usage.ts +119 -0
- package/src/session-opening-materials.ts +1 -1
- package/src/shape-unreadable-failure.ts +49 -0
- package/src/submission-errors.ts +3 -0
- package/src/ticket-provenance-contracts.ts +1 -0
- package/src/ticket-provenance.ts +0 -23
- package/dist/evidence-child-executor.js +0 -829
- package/src/diarist-llm-collector.ts +0 -324
- package/src/evidence-child-executor.ts +0 -1140
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single generic auto-resume loop for #416 (owner scope = single LLM call).
|
|
3
|
+
* Call-local retries, at most the effective autoResumeLimit times (injected once
|
|
4
|
+
* per call by the caller, #422 — never re-read from disk inside the loop),
|
|
5
|
+
* in-place (same runId/session).
|
|
6
|
+
* Unifies presentation: intermediate attempts use dummyIo, only final Terminal is presented.
|
|
7
|
+
*
|
|
8
|
+
* Owner 2026-08-23: a dispatch that exits by throwing used to bypass the entire
|
|
9
|
+
* retry mechanism (the throw escaped the while-loop before the count check ever
|
|
10
|
+
* ran). Every exception is now retained whole, in place, and the ordinary retry
|
|
11
|
+
* path continues: same budget, same call-local count semantics. No failure-type
|
|
12
|
+
* classification — every thrown value is treated identically.
|
|
13
|
+
*/
|
|
14
|
+
import { constants as fsConstants } from "node:fs";
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { lstat, mkdir, open } from "node:fs/promises";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { AUTO_RESUME_LIMIT, describeErrorIdentity, acquireRunWriterLease, markRunTerminal, RunWriterLeaseHeldError, } from "./run-lifecycle.js";
|
|
19
|
+
import { parseAutoResumeLimit } from "./config.js";
|
|
20
|
+
import { isLawfulTypedTerminalOutcome, formatTerminalResult } from "./terminal.js";
|
|
21
|
+
import { presentFailureTerminal, presentStructuralRejection, } from "./settlement.js";
|
|
22
|
+
const dummyIo = { stdout: () => { }, stderr: () => { } };
|
|
23
|
+
function presentTerminal(terminal, io) {
|
|
24
|
+
if (terminal.roleOutcome.kind === "failure" || terminal.roleOutcome.kind === "no_receipt") {
|
|
25
|
+
presentFailureTerminal(terminal, io);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
io.stdout(formatTerminalResult(terminal));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Best-effort finalization of the durable run state before an exception-path
|
|
33
|
+
* synthetic failure terminal is returned (#426 review: analyst-ledger classifies
|
|
34
|
+
* running runs as live — an exhausted invocation must not remain live
|
|
35
|
+
* indefinitely). Finalization failure must not mask the real cause.
|
|
36
|
+
*/
|
|
37
|
+
async function finalizeExceptionRunBestEffort(runDirectory, io) {
|
|
38
|
+
try {
|
|
39
|
+
await markRunTerminal(runDirectory);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
io.stderr(`run terminal-state finalization failed (best-effort continue): ${describeErrorIdentity(error)}\n`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Session custom-entry type carrying the pointer to one dispatch error file. */
|
|
46
|
+
export const DISPATCH_ERROR_RETENTION_ENTRY_TYPE = "ak_run_dispatch_error_retention";
|
|
47
|
+
/** Artifacts subdirectory of a run directory (established run-artifacts location). */
|
|
48
|
+
function runArtifactsDirectory(runDirectory) {
|
|
49
|
+
return join(runDirectory, "artifacts");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* #182-A hardened path identity, mirrored from settlement.ts's
|
|
53
|
+
* ensureAuditEvidenceDirectory: a planted symlink at the run directory or the
|
|
54
|
+
* artifacts path must not receive the dispatch error dump (O_NOFOLLOW only
|
|
55
|
+
* protects the final file name; recursive mkdir would accept a symlinked
|
|
56
|
+
* parent). Fails loudly with the true cause instead.
|
|
57
|
+
*/
|
|
58
|
+
async function ensureRealArtifactsDirectory(runDirectory) {
|
|
59
|
+
const runStat = await lstat(runDirectory);
|
|
60
|
+
if (runStat.isSymbolicLink() || !runStat.isDirectory()) {
|
|
61
|
+
throw new Error("dispatch error retention: run directory is not a real directory");
|
|
62
|
+
}
|
|
63
|
+
const artifactsDir = runArtifactsDirectory(runDirectory);
|
|
64
|
+
try {
|
|
65
|
+
const existing = await lstat(artifactsDir);
|
|
66
|
+
if (existing.isSymbolicLink() || !existing.isDirectory()) {
|
|
67
|
+
throw new Error("dispatch error retention: artifacts path is not a real directory");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (!isMissingPathError(error))
|
|
72
|
+
throw error;
|
|
73
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
74
|
+
const created = await lstat(artifactsDir);
|
|
75
|
+
if (created.isSymbolicLink() || !created.isDirectory()) {
|
|
76
|
+
throw new Error("dispatch error retention: artifacts directory is not a real directory");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return artifactsDir;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whole-object transfer of a thrown value (owner 2026-08-23: 「记录所有错误信息。
|
|
83
|
+
* 不能丢详细情况」). Every own property of the Error object — enumerable or not,
|
|
84
|
+
* which is how message/stack and any attached identity land verbatim — plus the
|
|
85
|
+
* constructor name and the full cause chain. No field list is prescribed or
|
|
86
|
+
* filtered: whatever the exception object carries goes into the file as-is.
|
|
87
|
+
*/
|
|
88
|
+
function serializeThrownValue(value, depth = 0, seen = new WeakSet()) {
|
|
89
|
+
if (value instanceof Error) {
|
|
90
|
+
if (seen.has(value))
|
|
91
|
+
return "[circular]";
|
|
92
|
+
seen.add(value);
|
|
93
|
+
const transferred = {};
|
|
94
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
95
|
+
transferred[key] = transferNestedValue(value[key], depth + 1, seen);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
errorKind: "Error",
|
|
99
|
+
constructorName: value.constructor?.name,
|
|
100
|
+
...transferred,
|
|
101
|
+
...(value.cause === undefined
|
|
102
|
+
? {}
|
|
103
|
+
: {
|
|
104
|
+
causeChain: depth >= 10
|
|
105
|
+
? "[cause-chain-depth-limit]"
|
|
106
|
+
: serializeThrownValue(value.cause, depth + 1, seen),
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
/** ENOENT identity shared with settlement.ts's hardened audit-artifact path. */
|
|
113
|
+
function isMissingPathError(error) {
|
|
114
|
+
return (error instanceof Error &&
|
|
115
|
+
"code" in error &&
|
|
116
|
+
error.code === "ENOENT");
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Recursive Error-property transfer (#426 review: nested Errors must not be
|
|
120
|
+
* passed raw to JSON.stringify — their non-enumerable message/stack would
|
|
121
|
+
* serialize as {}). Depth-limited; cycle-safe via the seen set so the recursive
|
|
122
|
+
* construction itself cannot diverge before stringify runs.
|
|
123
|
+
*/
|
|
124
|
+
function transferNestedValue(value, depth, seen) {
|
|
125
|
+
if (value instanceof Error)
|
|
126
|
+
return serializeThrownValue(value, depth, seen);
|
|
127
|
+
if (depth >= 10)
|
|
128
|
+
return "[nested-depth-limit]";
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
return value.map((item) => transferNestedValue(item, depth + 1, seen));
|
|
131
|
+
}
|
|
132
|
+
if (value !== null && typeof value === "object") {
|
|
133
|
+
if (seen.has(value))
|
|
134
|
+
return "[circular]";
|
|
135
|
+
seen.add(value);
|
|
136
|
+
const transferred = {};
|
|
137
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
138
|
+
transferred[key] = transferNestedValue(value[key], depth + 1, seen);
|
|
139
|
+
}
|
|
140
|
+
return transferred;
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
/** Cycle- and bigint-safe JSON replacer so serialization itself cannot drop data. */
|
|
145
|
+
function jsonSafeReplacer() {
|
|
146
|
+
const seen = new WeakSet();
|
|
147
|
+
return (_key, value) => {
|
|
148
|
+
if (typeof value === "bigint")
|
|
149
|
+
return `${value}n`;
|
|
150
|
+
if (typeof value === "object" && value !== null) {
|
|
151
|
+
if (seen.has(value))
|
|
152
|
+
return "[circular]";
|
|
153
|
+
seen.add(value);
|
|
154
|
+
}
|
|
155
|
+
return value;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Retain one throwing dispatch attempt's complete exception as an independent
|
|
160
|
+
* per-attempt file under the run's artifacts directory, then leave an
|
|
161
|
+
* addressable pointer in the session principal (custom entry). Exclusive-create
|
|
162
|
+
* open (O_EXCL) with a per-attempt unique name enforces 史必追加 (#419): a later
|
|
163
|
+
* attempt can never overwrite an earlier attempt's file.
|
|
164
|
+
*/
|
|
165
|
+
async function retainDispatchError(admitted, principalAuthority, sessionAppender, attempt, error) {
|
|
166
|
+
const artifactsDir = await ensureRealArtifactsDirectory(admitted.runDirectory);
|
|
167
|
+
const filePath = join(artifactsDir, `dispatch-error-attempt-${attempt}-${randomUUID()}.json`);
|
|
168
|
+
// Whole-object dump: everything the thrown value carries, nothing picked.
|
|
169
|
+
const payload = `${JSON.stringify({
|
|
170
|
+
version: 1,
|
|
171
|
+
attempt,
|
|
172
|
+
recordedAt: new Date().toISOString(),
|
|
173
|
+
error: serializeThrownValue(error),
|
|
174
|
+
}, jsonSafeReplacer(), 2)}\n`;
|
|
175
|
+
// O_EXCL: exclusive create — the retention history is append-only by
|
|
176
|
+
// construction; a colliding name fails loudly instead of overwriting.
|
|
177
|
+
// O_NOFOLLOW when the platform provides it keeps a planted symlink from
|
|
178
|
+
// being followed; on platforms without it, exclusivity still holds.
|
|
179
|
+
// AK artifact owner stays here; only the session JSONL pointer uses Pi codec.
|
|
180
|
+
const noFollowFlag = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
|
|
181
|
+
const handle = await open(filePath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollowFlag, 0o600);
|
|
182
|
+
try {
|
|
183
|
+
await handle.writeFile(payload, "utf8");
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
await handle.close();
|
|
187
|
+
}
|
|
188
|
+
// Addressable pointer in the dossier (卷宗): Pi session custom-entry codec
|
|
189
|
+
// (appendPiSessionCustomEntry). Lease still owned here with run-writer.
|
|
190
|
+
let pointerLease;
|
|
191
|
+
try {
|
|
192
|
+
pointerLease = await acquireRunWriterLease(admitted.runDirectory);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (error instanceof RunWriterLeaseHeldError)
|
|
196
|
+
return { file: filePath };
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
// Pointer-stage failure (#426 fix_now #5) is separated from the file write:
|
|
200
|
+
// once the error file is durably on disk, a failed session append must not
|
|
201
|
+
// reject through here and orphan it — the file path is still handed back.
|
|
202
|
+
let pointerError;
|
|
203
|
+
try {
|
|
204
|
+
const timestamp = new Date().toISOString();
|
|
205
|
+
await sessionAppender(principalAuthority, admitted.principal, DISPATCH_ERROR_RETENTION_ENTRY_TYPE, { version: 1, attempt, file: filePath, recordedAt: timestamp });
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
pointerError = error;
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
await pointerLease.release();
|
|
212
|
+
}
|
|
213
|
+
return pointerError === undefined ? { file: filePath } : { file: filePath, pointerError };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Typed failure terminal for a retry path that ended with only exceptions:
|
|
217
|
+
* loud, non-lawful, carrying the last true cause and the pointers to the
|
|
218
|
+
* full per-attempt error files. Never rethrows the raw exception at callers.
|
|
219
|
+
*/
|
|
220
|
+
function dispatchExceptionFailureTerminal(input) {
|
|
221
|
+
// #426 review: this terminal fires whenever the FINAL dispatch throws, not
|
|
222
|
+
// only when every attempt threw — do not misrepresent a mixed retry history.
|
|
223
|
+
const history = input.everyAttemptThrew
|
|
224
|
+
? "dispatch threw an exception on every attempt"
|
|
225
|
+
: "the final dispatch threw an exception";
|
|
226
|
+
const diagnostic = `${history} (${input.endReason}; resumes used ${input.autoResumeAttempts}); last cause: ${describeErrorIdentity(input.causeError)}`;
|
|
227
|
+
const decisiveFacts = {
|
|
228
|
+
cause: "unrecognized",
|
|
229
|
+
diagnostic,
|
|
230
|
+
resumesUsed: input.autoResumeAttempts,
|
|
231
|
+
dispatchErrorFiles: [...input.errorFiles],
|
|
232
|
+
};
|
|
233
|
+
if (input.errorFiles.length > 0) {
|
|
234
|
+
decisiveFacts.lastDispatchErrorFile = input.errorFiles[input.errorFiles.length - 1];
|
|
235
|
+
}
|
|
236
|
+
const candidate = input.causeError;
|
|
237
|
+
if (typeof candidate?.name === "string")
|
|
238
|
+
decisiveFacts.errorName = candidate.name;
|
|
239
|
+
if (typeof candidate?.code === "string" || typeof candidate?.code === "number") {
|
|
240
|
+
decisiveFacts.errorCode = candidate.code;
|
|
241
|
+
}
|
|
242
|
+
const artifacts = input.errorFiles.map((path) => ({
|
|
243
|
+
kind: "error",
|
|
244
|
+
path,
|
|
245
|
+
}));
|
|
246
|
+
return {
|
|
247
|
+
roleOutcome: {
|
|
248
|
+
kind: "failure",
|
|
249
|
+
role: input.role,
|
|
250
|
+
cause: "unrecognized",
|
|
251
|
+
diagnostic,
|
|
252
|
+
decisiveFacts,
|
|
253
|
+
},
|
|
254
|
+
navigator: { disposition: "no-advice" },
|
|
255
|
+
artifacts,
|
|
256
|
+
runId: input.runId,
|
|
257
|
+
autoResumeCount: input.autoResumeAttempts,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
export async function runWithAutoResumeLoop(options) {
|
|
261
|
+
// #422 single-point resolution + domain validation. NaN would bypass every
|
|
262
|
+
// `attempts >= limit` comparison (always false) — reject here, before any dispatch.
|
|
263
|
+
const limit = options.autoResumeLimit ?? AUTO_RESUME_LIMIT;
|
|
264
|
+
parseAutoResumeLimit(limit);
|
|
265
|
+
let autoResumeAttempts = 0;
|
|
266
|
+
let isFirst = true;
|
|
267
|
+
let currentPayload = options.buildInitialPayload();
|
|
268
|
+
let dispatchOrdinal = 0;
|
|
269
|
+
let lastThrownError;
|
|
270
|
+
let everyAttemptThrew = true;
|
|
271
|
+
const retainedErrorFiles = [];
|
|
272
|
+
while (true) {
|
|
273
|
+
let lease;
|
|
274
|
+
try {
|
|
275
|
+
lease = await acquireRunWriterLease(options.admitted.runDirectory, (diagnostic) => options.io.stderr(diagnostic));
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (error instanceof RunWriterLeaseHeldError) {
|
|
279
|
+
presentStructuralRejection(error, options.io);
|
|
280
|
+
return { exitCode: 2 };
|
|
281
|
+
}
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
let result;
|
|
285
|
+
try {
|
|
286
|
+
result = await options.dispatch(currentPayload, lease, isFirst, dummyIo);
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
// Owner 2026-08-23: 「出了异常,就原地记录错误信息,然后重试。」
|
|
290
|
+
// Retain the whole exception in place (per-attempt full file + dossier
|
|
291
|
+
// pointer); recording failure must not break the retry path (PR #418
|
|
292
|
+
// diagnostic-sink-isolation precedent). The dispatcher owns lease release
|
|
293
|
+
// in its own finally, so the retry round starts with the lock free.
|
|
294
|
+
lastThrownError = error;
|
|
295
|
+
const attempt = dispatchOrdinal;
|
|
296
|
+
try {
|
|
297
|
+
// Track the file as soon as it is durably written (#426 review):
|
|
298
|
+
// a pointer-stage failure comes back separately (pointerError) and must
|
|
299
|
+
// never orphan the retained file (#426 fix_now #5).
|
|
300
|
+
const { file, pointerError } = await retainDispatchError(options.admitted, options.principalAuthority, options.sessionAppender, attempt, error);
|
|
301
|
+
retainedErrorFiles.push(file);
|
|
302
|
+
options.io.stderr(`dispatch attempt ${attempt} threw (${describeErrorIdentity(error)}); full error retained at ${file}\n`);
|
|
303
|
+
if (pointerError !== undefined) {
|
|
304
|
+
options.io.stderr(`dispatch error retention failed (best-effort continue): ${describeErrorIdentity(pointerError)}\n`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch (retentionError) {
|
|
308
|
+
options.io.stderr(`dispatch error retention failed (best-effort continue): ${describeErrorIdentity(retentionError)}\n`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
dispatchOrdinal += 1;
|
|
312
|
+
if (result !== undefined) {
|
|
313
|
+
everyAttemptThrew = false;
|
|
314
|
+
const terminal = result.terminal;
|
|
315
|
+
if (terminal !== undefined) {
|
|
316
|
+
terminal.autoResumeCount = autoResumeAttempts;
|
|
317
|
+
}
|
|
318
|
+
const lawful = terminal !== undefined && isLawfulTypedTerminalOutcome(terminal.roleOutcome);
|
|
319
|
+
if (lawful) {
|
|
320
|
+
if (terminal !== undefined) {
|
|
321
|
+
// Present lawful terminal once to real io (dummy was used inside dispatch)
|
|
322
|
+
options.io.stdout(formatTerminalResult(terminal));
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Settlement-owned sealed disposition before any redispatch (#648 / #672):
|
|
328
|
+
// shared for non-lawful return and direct throw. Only entry presentation
|
|
329
|
+
// differs (present returned terminal vs throw-path synthetic).
|
|
330
|
+
if (options.sealedAcceptanceDisposition !== undefined) {
|
|
331
|
+
const disposition = await options.sealedAcceptanceDisposition();
|
|
332
|
+
if (disposition.kind === "block") {
|
|
333
|
+
if (disposition.reason === "sealed-accepted" && result !== undefined) {
|
|
334
|
+
const terminal = result.terminal;
|
|
335
|
+
if (terminal !== undefined)
|
|
336
|
+
presentTerminal(terminal, options.io);
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
const terminal = dispatchExceptionFailureTerminal({
|
|
340
|
+
role: options.admitted.role,
|
|
341
|
+
runId: options.admitted.runId,
|
|
342
|
+
causeError: disposition.reason === "authority-failed"
|
|
343
|
+
? disposition.cause
|
|
344
|
+
: lastThrownError,
|
|
345
|
+
errorFiles: retainedErrorFiles,
|
|
346
|
+
autoResumeAttempts,
|
|
347
|
+
endReason: disposition.reason === "authority-failed"
|
|
348
|
+
? "sealed-acceptance authority failed closed"
|
|
349
|
+
: "sealed accepted projection already present",
|
|
350
|
+
everyAttemptThrew,
|
|
351
|
+
});
|
|
352
|
+
await finalizeExceptionRunBestEffort(options.admitted.runDirectory, options.io);
|
|
353
|
+
presentTerminal(terminal, options.io);
|
|
354
|
+
return {
|
|
355
|
+
exitCode: 1,
|
|
356
|
+
terminal,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (result !== undefined) {
|
|
361
|
+
const terminal = result.terminal;
|
|
362
|
+
if (autoResumeAttempts >= limit) {
|
|
363
|
+
if (terminal !== undefined)
|
|
364
|
+
presentTerminal(terminal, options.io);
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
if (!(await options.principalAuthority.isAvailable(options.admitted.principal))) {
|
|
368
|
+
if (terminal !== undefined)
|
|
369
|
+
presentTerminal(terminal, options.io);
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
// Exception path: continue through the identical budget/session gates.
|
|
375
|
+
if (autoResumeAttempts >= limit) {
|
|
376
|
+
const terminal = dispatchExceptionFailureTerminal({
|
|
377
|
+
role: options.admitted.role,
|
|
378
|
+
runId: options.admitted.runId,
|
|
379
|
+
causeError: lastThrownError,
|
|
380
|
+
errorFiles: retainedErrorFiles,
|
|
381
|
+
autoResumeAttempts,
|
|
382
|
+
endReason: "auto-resume budget exhausted",
|
|
383
|
+
everyAttemptThrew,
|
|
384
|
+
});
|
|
385
|
+
await finalizeExceptionRunBestEffort(options.admitted.runDirectory, options.io);
|
|
386
|
+
presentTerminal(terminal, options.io);
|
|
387
|
+
return {
|
|
388
|
+
exitCode: 1,
|
|
389
|
+
terminal,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (!(await options.principalAuthority.isAvailable(options.admitted.principal))) {
|
|
393
|
+
const terminal = dispatchExceptionFailureTerminal({
|
|
394
|
+
role: options.admitted.role,
|
|
395
|
+
runId: options.admitted.runId,
|
|
396
|
+
causeError: lastThrownError,
|
|
397
|
+
errorFiles: retainedErrorFiles,
|
|
398
|
+
autoResumeAttempts,
|
|
399
|
+
endReason: "session principal unavailable before further resume",
|
|
400
|
+
everyAttemptThrew,
|
|
401
|
+
});
|
|
402
|
+
await finalizeExceptionRunBestEffort(options.admitted.runDirectory, options.io);
|
|
403
|
+
presentTerminal(terminal, options.io);
|
|
404
|
+
return {
|
|
405
|
+
exitCode: 1,
|
|
406
|
+
terminal,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
autoResumeAttempts++;
|
|
411
|
+
currentPayload = options.buildResumePayload();
|
|
412
|
+
isFirst = false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Shared public CLI usage error (structural reject before admission). */
|
|
2
|
+
export class CliUsageError extends Error {
|
|
3
|
+
code = "AK_ROLE_USAGE";
|
|
4
|
+
constructor(message, options) {
|
|
5
|
+
super(message, options?.cause === undefined ? undefined : { cause: options.cause });
|
|
6
|
+
this.name = "CliUsageError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.js";
|
|
2
|
+
import { CliUsageError } from "./cli-errors.js";
|
|
3
|
+
import { admitDoctorInvocation, buildDoctorTransportPrompt } from "./invocation.js";
|
|
4
|
+
import { runPostAdmissionOneShot, runPostAdmissionSeatResume, resumeTurnRequestProjectionOptions, } from "./post-admission.js";
|
|
5
|
+
import { loadResumableDoctorRun, markRunAdmitted, } from "./run-lifecycle.js";
|
|
6
|
+
import { presentStructuralRejection, trySettleDoctorTerminalResult, } from "./settlement.js";
|
|
7
|
+
import { isLawfulTypedTerminalOutcome, } from "./terminal.js";
|
|
8
|
+
import { projectRoleTurnRequest, } from "./turn-request.js";
|
|
9
|
+
/** Project admitted invocation onto the host-neutral turn request. */
|
|
10
|
+
export function buildDoctorTurnRequest(admitted, options) {
|
|
11
|
+
return projectRoleTurnRequest(admitted, {
|
|
12
|
+
activation: { role: "doctor", casePath: admitted.caseRunsPath },
|
|
13
|
+
}, options);
|
|
14
|
+
}
|
|
15
|
+
export async function runPublicDoctor(argv, env, io, parseDoctorArgv) {
|
|
16
|
+
let admitted;
|
|
17
|
+
try {
|
|
18
|
+
const parsed = parseDoctorArgv(argv);
|
|
19
|
+
admitted = await admitDoctorInvocation({
|
|
20
|
+
home: env.home,
|
|
21
|
+
principalAuthority: env.principalAuthority,
|
|
22
|
+
cwd: env.cwd,
|
|
23
|
+
issueNumber: parsed.issueNumber,
|
|
24
|
+
instruction: parsed.instruction,
|
|
25
|
+
attachmentPaths: parsed.attachmentPaths,
|
|
26
|
+
...(parsed.project === undefined ? {} : { project: parsed.project }),
|
|
27
|
+
...(parsed.runs === undefined ? {} : { runs: parsed.runs }),
|
|
28
|
+
...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
|
|
29
|
+
...(env.model === undefined ? {} : { model: env.model }),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof CliUsageError) {
|
|
34
|
+
presentStructuralRejection(error, io);
|
|
35
|
+
return { exitCode: 2 };
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
await markRunAdmitted(admitted, env.principalAuthority);
|
|
40
|
+
const turnRequest = buildDoctorTurnRequest(admitted, {
|
|
41
|
+
packageRoot: env.packageRoot,
|
|
42
|
+
home: env.home,
|
|
43
|
+
agentDir: env.agentDir,
|
|
44
|
+
...(env.model === undefined ? {} : { model: env.model }),
|
|
45
|
+
...(env.engine === undefined ? {} : { engine: env.engine }),
|
|
46
|
+
...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }),
|
|
47
|
+
...(env.correlationId === undefined || env.correlationId.trim() === ""
|
|
48
|
+
? {}
|
|
49
|
+
: { correlationId: env.correlationId }),
|
|
50
|
+
continuation: {
|
|
51
|
+
kind: "initial",
|
|
52
|
+
prompt: buildDoctorTransportPrompt(admitted, engineSessionMaterialFromOptions({ ...(env.engine === undefined ? {} : { engine: env.engine }), packageRoot: env.packageRoot })),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
return await runPostAdmissionOneShot({
|
|
56
|
+
admitted,
|
|
57
|
+
env,
|
|
58
|
+
io,
|
|
59
|
+
request: turnRequest,
|
|
60
|
+
adapters: doctorAdapters(),
|
|
61
|
+
...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function doctorAdapters() {
|
|
65
|
+
return {
|
|
66
|
+
trySettle: (admitted, authority) => trySettleDoctorTerminalResult(admitted, authority),
|
|
67
|
+
shouldPresentSettled: (terminal) => isLawfulTypedTerminalOutcome(terminal.roleOutcome),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resume a previously admitted Doctor run (#633). Issue + retained case
|
|
72
|
+
* identity restore from the durable admitted request; the session principal reopens.
|
|
73
|
+
*/
|
|
74
|
+
export async function runPublicDoctorResume(request, env, io) {
|
|
75
|
+
return await runPostAdmissionSeatResume({
|
|
76
|
+
request,
|
|
77
|
+
env,
|
|
78
|
+
io,
|
|
79
|
+
load: (effective) => loadResumableDoctorRun(env.home, effective.runId, env.principalAuthority),
|
|
80
|
+
buildTurnRequest: (admitted, effective) => buildDoctorTurnRequest(admitted, resumeTurnRequestProjectionOptions(admitted, effective, env)),
|
|
81
|
+
adapters: doctorAdapters(),
|
|
82
|
+
...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.js";
|
|
2
|
+
import { CliUsageError } from "./cli-errors.js";
|
|
3
|
+
import { applyInstructionTicketProbe, probeInstructionTicket, ticketNumberFromProbe, tryResumeSameTicketSeatRun, } from "./seat-ticket-binding.js";
|
|
4
|
+
import { admitInspectorInvocation, buildInspectorTransportPrompt, } from "./invocation.js";
|
|
5
|
+
import { prepareSummonsResumeMaterials, runPostAdmissionOneShot, runPostAdmissionSeatResume, resumeTurnRequestProjectionOptions, } from "./post-admission.js";
|
|
6
|
+
import { loadResumableInspectorRun, markRunAdmitted, } from "./run-lifecycle.js";
|
|
7
|
+
import { presentStructuralRejection, trySettleInspectorTerminalResult, } from "./settlement.js";
|
|
8
|
+
import { projectRoleTurnRequest, } from "./turn-request.js";
|
|
9
|
+
/** Project admitted invocation onto the host-neutral turn request. */
|
|
10
|
+
export function buildInspectorTurnRequest(admitted, options) {
|
|
11
|
+
return projectRoleTurnRequest(admitted, {
|
|
12
|
+
activation: {
|
|
13
|
+
role: "inspector",
|
|
14
|
+
},
|
|
15
|
+
}, options);
|
|
16
|
+
}
|
|
17
|
+
export async function runPublicInspector(argv, env, io, parseInspectorArgv) {
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = parseInspectorArgv(argv);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof CliUsageError) {
|
|
24
|
+
presentStructuralRejection(error, io);
|
|
25
|
+
return { exitCode: 2 };
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
// #637: same ticket → resume prior inspector run with this summons' materials.
|
|
30
|
+
// Probe captures DiaristTicketResolutionError so admit+beforeDispatch can settle
|
|
31
|
+
// controlled failure (bare pre-admit throw skips terminal settlement).
|
|
32
|
+
// No bare catch→fresh: lookup/resume failures surface; only true absence mints new.
|
|
33
|
+
const projectRoot = parsed.project ?? env.cwd;
|
|
34
|
+
const ticketProbe = await probeInstructionTicket(parsed.instruction, projectRoot, env);
|
|
35
|
+
const probedTicketNumber = ticketNumberFromProbe(ticketProbe);
|
|
36
|
+
if (probedTicketNumber !== undefined) {
|
|
37
|
+
const summons = {
|
|
38
|
+
instruction: parsed.instruction,
|
|
39
|
+
instructionEmpty: parsed.instruction.trim() === "",
|
|
40
|
+
attachmentPaths: parsed.attachmentPaths,
|
|
41
|
+
};
|
|
42
|
+
const resumed = await tryResumeSameTicketSeatRun({
|
|
43
|
+
home: env.home,
|
|
44
|
+
projectRoot,
|
|
45
|
+
role: "inspector",
|
|
46
|
+
ticketNumber: probedTicketNumber,
|
|
47
|
+
freshSummons: env.freshSummons,
|
|
48
|
+
summons,
|
|
49
|
+
resume: (runId, materials) => runPublicInspectorResume({ runId, ...(materials === undefined ? {} : { summons: materials }) }, env, io),
|
|
50
|
+
});
|
|
51
|
+
if (resumed !== undefined)
|
|
52
|
+
return resumed;
|
|
53
|
+
}
|
|
54
|
+
let admitted;
|
|
55
|
+
try {
|
|
56
|
+
admitted = await admitInspectorInvocation({
|
|
57
|
+
home: env.home,
|
|
58
|
+
principalAuthority: env.principalAuthority,
|
|
59
|
+
cwd: env.cwd,
|
|
60
|
+
instruction: parsed.instruction,
|
|
61
|
+
attachmentPaths: parsed.attachmentPaths,
|
|
62
|
+
...(parsed.project === undefined ? {} : { project: parsed.project }),
|
|
63
|
+
...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
|
|
64
|
+
...(env.model === undefined ? {} : { model: env.model }),
|
|
65
|
+
...(env.correlationId === undefined ? {} : { correlationId: env.correlationId }),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error instanceof CliUsageError) {
|
|
70
|
+
presentStructuralRejection(error, io);
|
|
71
|
+
return { exitCode: 2 };
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
await markRunAdmitted(admitted, env.principalAuthority);
|
|
76
|
+
const engineMaterial = engineSessionMaterialFromOptions({
|
|
77
|
+
...(env.engine === undefined ? {} : { engine: env.engine }),
|
|
78
|
+
packageRoot: env.packageRoot,
|
|
79
|
+
});
|
|
80
|
+
const turnRequest = buildInspectorTurnRequest(admitted, {
|
|
81
|
+
packageRoot: env.packageRoot,
|
|
82
|
+
home: env.home,
|
|
83
|
+
agentDir: env.agentDir,
|
|
84
|
+
...(env.model === undefined ? {} : { model: env.model }),
|
|
85
|
+
...(env.engine === undefined ? {} : { engine: env.engine }),
|
|
86
|
+
...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }),
|
|
87
|
+
...(env.correlationId === undefined || env.correlationId.trim() === ""
|
|
88
|
+
? {}
|
|
89
|
+
: { correlationId: env.correlationId }),
|
|
90
|
+
continuation: {
|
|
91
|
+
kind: "initial",
|
|
92
|
+
prompt: buildInspectorTransportPrompt(admitted, engineMaterial),
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
return await runPostAdmissionOneShot({
|
|
96
|
+
admitted,
|
|
97
|
+
env,
|
|
98
|
+
io,
|
|
99
|
+
request: turnRequest,
|
|
100
|
+
adapters: inspectorAdapters({
|
|
101
|
+
beforeDispatch: async (admittedSeat) => {
|
|
102
|
+
// #635/#637: apply pre-admit probe inside controlled-failure boundary.
|
|
103
|
+
await applyInstructionTicketProbe(admittedSeat, ticketProbe);
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function inspectorAdapters(options) {
|
|
110
|
+
return {
|
|
111
|
+
trySettle: (admitted, authority, scope) => trySettleInspectorTerminalResult(admitted, authority, scope),
|
|
112
|
+
shouldPresentSettled: () => true,
|
|
113
|
+
...(options?.beforeDispatch === undefined
|
|
114
|
+
? {}
|
|
115
|
+
: { beforeDispatch: options.beforeDispatch }),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Resume a previously admitted Inspector run (#633 / #637); the session principal reopens.
|
|
120
|
+
* Same-ticket summons deliver this turn's instruction + frozen attachments; manual
|
|
121
|
+
* resume keeps package-envelope / caller-message semantics and birth attachments.
|
|
122
|
+
*/
|
|
123
|
+
export async function runPublicInspectorResume(request, env, io) {
|
|
124
|
+
return await runPostAdmissionSeatResume({
|
|
125
|
+
request,
|
|
126
|
+
env,
|
|
127
|
+
io,
|
|
128
|
+
load: (effective) => loadResumableInspectorRun(env.home, effective.runId, env.principalAuthority),
|
|
129
|
+
buildTurnRequest: async (admitted, effective) => {
|
|
130
|
+
const summonsPrepared = await prepareSummonsResumeMaterials(admitted.runDirectory, effective.summons);
|
|
131
|
+
return buildInspectorTurnRequest(admitted, resumeTurnRequestProjectionOptions(admitted, effective, env, summonsPrepared));
|
|
132
|
+
},
|
|
133
|
+
adapters: inspectorAdapters(),
|
|
134
|
+
...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
|
|
135
|
+
});
|
|
136
|
+
}
|