@akagilnc/pi-workflow-roles 0.1.5382 → 0.1.5462

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +4 -2
  2. package/README.zh-CN.md +4 -2
  3. package/dist/acp-host/production-host.js +8843 -9012
  4. package/dist/archivist-record-pointer.js +38 -0
  5. package/dist/compliance-transport.js +7 -5
  6. package/dist/doctor-auditor.js +1 -0
  7. package/dist/engine-detour-usage.js +9 -36
  8. package/dist/gatekeeper-role.js +4 -18
  9. package/dist/headless-host/description.js +0 -209
  10. package/dist/headless-host/production-host.js +7828 -8247
  11. package/dist/host-contracts.js +8 -0
  12. package/dist/judge-role.js +14 -1
  13. package/dist/migrate-book-topology.js +215 -96
  14. package/dist/navigator-attendance.js +10 -9
  15. package/dist/navigator-invocation-identity.js +18 -0
  16. package/dist/packaged-role-registry.js +1 -9
  17. package/dist/pi/role-turn-host.js +46 -19
  18. package/dist/public-cli/auto-resume.js +29 -66
  19. package/dist/public-cli/cli.js +1 -0
  20. package/dist/public-cli/countersign-run.js +4 -18
  21. package/dist/public-cli/instruction-seat-run.js +258 -69
  22. package/dist/public-cli/main.js +7488 -7758
  23. package/dist/public-cli/post-admission.js +451 -349
  24. package/dist/public-cli/process-cancel.js +8 -0
  25. package/dist/public-cli/run-lifecycle.js +24 -94
  26. package/dist/public-cli/settlement.js +624 -1471
  27. package/dist/public-cli/terminal.js +34 -53
  28. package/dist/public-cli/turn-request.js +3 -0
  29. package/dist/public-role-summons.js +3 -8
  30. package/dist/receipt-delivery-policy.js +162 -28
  31. package/dist/run-terminal-artifacts.js +19 -14
  32. package/dist/serialize-thrown-value.js +7 -1
  33. package/dist/submission-errors.js +11 -1
  34. package/dist/submission-gate.js +27 -4
  35. package/dist/submission-ledger.js +21 -89
  36. package/package.json +1 -1
  37. package/scripts/build-package.mjs +0 -2
  38. package/src/acp-host/role-turn-host.ts +168 -75
  39. package/src/archivist-record-pointer.ts +48 -0
  40. package/src/compliance-transport.ts +13 -8
  41. package/src/doctor-auditor.ts +3 -0
  42. package/src/engine-detour-usage.ts +10 -39
  43. package/src/external-host-turn-loop.ts +100 -43
  44. package/src/gatekeeper-role.ts +7 -22
  45. package/src/headless-host/description.ts +4 -214
  46. package/src/headless-host/production-host.ts +2 -1
  47. package/src/headless-host/role-turn-host.ts +153 -186
  48. package/src/host-contracts.ts +20 -10
  49. package/src/judge-role.ts +24 -5
  50. package/src/navigator-attendance.ts +17 -12
  51. package/src/navigator-invocation-identity.ts +31 -0
  52. package/src/navigator-public-session.ts +1 -1
  53. package/src/navigator-session-contracts.ts +1 -1
  54. package/src/packaged-role-registry.ts +2 -10
  55. package/src/pi/adapter.ts +33 -11
  56. package/src/pi/role-turn-host.ts +49 -28
  57. package/src/prepared-role-turn.ts +2 -0
  58. package/src/public-cli/auto-resume.ts +27 -66
  59. package/src/public-cli/cli.ts +1 -0
  60. package/src/public-cli/countersign-run.ts +4 -22
  61. package/src/public-cli/instruction-seat-run.ts +307 -74
  62. package/src/public-cli/post-admission.ts +615 -459
  63. package/src/public-cli/process-cancel.ts +9 -0
  64. package/src/public-cli/run-lifecycle.ts +25 -126
  65. package/src/public-cli/settlement.ts +801 -1859
  66. package/src/public-cli/terminal.ts +42 -70
  67. package/src/public-cli/turn-request.ts +10 -1
  68. package/src/public-role-summons.ts +3 -8
  69. package/src/receipt-delivery-policy.ts +219 -28
  70. package/src/role-envelope.ts +21 -10
  71. package/src/role-runtime-dependencies.ts +1 -0
  72. package/src/role-runtime.ts +79 -102
  73. package/src/run-terminal-artifacts.ts +21 -17
  74. package/src/serialize-thrown-value.ts +7 -1
  75. package/src/submission-correctable-error.ts +5 -2
  76. package/src/submission-errors.ts +12 -1
  77. package/src/submission-gate.ts +34 -4
  78. package/src/submission-ledger.ts +30 -116
  79. package/src/worker-role.ts +19 -7
  80. package/src/worker-submission-gates.ts +29 -7
  81. package/dist/audit-escalation.js +0 -7
  82. package/dist/pi/known-failure.js +0 -51
  83. package/dist/typed-provider-http.js +0 -57
  84. package/dist/upstream-error-testimony.js +0 -19
  85. package/src/pi/known-failure.ts +0 -76
  86. package/src/typed-provider-http.ts +0 -89
  87. package/src/upstream-error-testimony.ts +0 -45
@@ -1,3 +1,4 @@
1
+ import { serializeThrownValue } from "../serialize-thrown-value.js";
1
2
  /** Encode one free-text Terminal cell. JSON string form cannot embed raw tab/newline. */
2
3
  export function encodeTerminalField(value) {
3
4
  return JSON.stringify(value);
@@ -10,36 +11,15 @@ export function exitCodeForTerminalOutcome(outcome) {
10
11
  return isLawfulTypedTerminalOutcome(outcome) ? 0 : 1;
11
12
  }
12
13
  /**
13
- * One admitted Role run's typed Terminal aggregate.
14
- * Resumable failures carry `resume` and must not re-disclose the run ID via
15
- * top-level `runId` or public artifact path components — only `resume.command`.
16
- * #416: autoResumeCount (0..2) is a call-local observation of how many in-place
17
- * auto-resumes occurred during this single LLM call; it is not persisted to
18
- * run-state.json and does not participate in limit decisions.
14
+ * Parent-facing current reply: a court's own accepted/audit payloads only.
15
+ * Run-scoped history stays on TerminalResult.submissions (#879 / #836), and a
16
+ * failure face never answers as this court's reply (#953).
19
17
  */
20
- /**
21
- * Current-result payloads on a terminal — accepted/audit role result, or a
22
- * rare intentional failure face. Run history is TerminalResult.submissions
23
- * (#836 / #953), not this helper.
24
- */
25
- export function roleResultPayloads(outcome) {
26
- if (outcome.kind === "accepted" || outcome.kind === "audit_escalation")
27
- return outcome.payloads ?? [];
28
- if (outcome.kind === "failure")
29
- return outcome.payloads ?? [];
30
- return [];
31
- }
32
- /**
33
- * Prefer non-empty current-result payloads; an empty array must not shadow
34
- * top-level submissions (#953 / #836). Callers that need failure history must
35
- * read TerminalResult.submissions directly — do not coalesce into failure.payloads.
36
- */
37
- export function coalesceSubmissionRows(payloads, submissions) {
38
- if (payloads !== undefined && payloads.length > 0)
39
- return payloads;
40
- if (submissions !== undefined && submissions.length > 0)
41
- return submissions;
42
- return payloads ?? submissions ?? [];
18
+ export function currentReplyRows(terminal) {
19
+ const outcome = terminal?.roleOutcome;
20
+ return outcome?.kind === "accepted" || outcome?.kind === "audit_escalation"
21
+ ? outcome.payloads ?? []
22
+ : [];
43
23
  }
44
24
  /**
45
25
  * Build an advice navigator fact (#959). Prose is presented as submitted;
@@ -69,6 +49,17 @@ export function formatTerminalResult(result) {
69
49
  if (result.roleOutcome.kind === "failure") {
70
50
  lines.push(`diagnostic\t${encodeTerminalField(result.roleOutcome.diagnostic)}`);
71
51
  }
52
+ // Accepted volumes do not dump decisiveFacts unless a failed attempt is
53
+ // recorded. The unsettled-direction fact still has to be visible on its own.
54
+ // audit_escalation already dumps every decisiveFact below.
55
+ if (result.roleOutcome.kind === "accepted"
56
+ && result.roleOutcome.decisiveFacts?.directionUnsettled === true) {
57
+ lines.push("fact\tdirectionUnsettled\ttrue");
58
+ const subsequent = result.roleOutcome.decisiveFacts.subsequentAudit;
59
+ if (typeof subsequent === "string") {
60
+ lines.push(`fact\tsubsequentAudit\t${encodeTerminalField(subsequent)}`);
61
+ }
62
+ }
72
63
  if (result.roleOutcome.kind === "failure" || result.roleOutcome.kind === "no_receipt" || result.roleOutcome.kind === "audit_escalation" || result.roleOutcome.decisiveFacts?.failedAttempts !== undefined) {
73
64
  const facts = result.roleOutcome.kind === "accepted"
74
65
  ? { failedAttempts: result.roleOutcome.decisiveFacts?.failedAttempts }
@@ -76,7 +67,7 @@ export function formatTerminalResult(result) {
76
67
  for (const [key, value] of Object.entries(facts)) {
77
68
  if (value === undefined)
78
69
  continue;
79
- const rendered = typeof value === "string" ? value : JSON.stringify(value);
70
+ const rendered = typeof value === "string" ? value : serializeThrownValue(value);
80
71
  lines.push(`fact\t${encodeTerminalField(key)}\t${encodeTerminalField(rendered)}`);
81
72
  }
82
73
  }
@@ -115,11 +106,7 @@ export function formatTerminalResult(result) {
115
106
  }
116
107
  }
117
108
  }
118
- if (result.resume !== undefined) {
119
- // Resumable failure: run ID is revealed only inside the complete resume command.
120
- lines.push(`resume\t${encodeTerminalField(result.resume.command)}`);
121
- }
122
- else if (result.runId !== undefined) {
109
+ if (result.runId !== undefined) {
123
110
  lines.push(`run\t${encodeTerminalField(result.runId)}`);
124
111
  }
125
112
  if (result.autoResumeCount !== undefined) {
@@ -127,25 +114,19 @@ export function formatTerminalResult(result) {
127
114
  }
128
115
  // Role-result block: original payloads, newest first for humans (#961).
129
116
  // Typed payloads/submissions stay ledger order; only this presentation reverses.
130
- // #953: failure history is the top-level submissions carrier (#836) — present
131
- // as recorded-submission, never as this-turn submission/receipt.
132
- if (result.roleOutcome.kind === "failure") {
133
- const recorded = result.submissions ?? [];
134
- for (let i = recorded.length - 1; i >= 0; i -= 1) {
135
- const payload = recorded[i];
136
- const rendered = typeof payload === "string" ? payload : JSON.stringify(payload);
137
- lines.push(`recorded-submission\t${encodeTerminalField(rendered)}`);
138
- }
139
- }
140
- else {
141
- const payloads = result.roleOutcome.kind === "accepted" ||
142
- result.roleOutcome.kind === "audit_escalation"
143
- ? coalesceSubmissionRows(result.roleOutcome.payloads, result.submissions)
144
- : result.submissions ?? [];
145
- for (let i = payloads.length - 1; i >= 0; i -= 1) {
146
- const payload = payloads[i];
117
+ // Present the current result and the run history on distinct faces (#879 / #836).
118
+ // Neither a prior court's submission nor a failed turn's history is this reply.
119
+ const current = result.roleOutcome.kind === "failure"
120
+ ? result.roleOutcome.payloads ?? []
121
+ : currentReplyRows(result);
122
+ for (const [label, rows] of [
123
+ ["submission", current],
124
+ ["recorded-submission", result.submissions ?? []],
125
+ ]) {
126
+ for (let i = rows.length - 1; i >= 0; i -= 1) {
127
+ const payload = rows[i];
147
128
  const rendered = typeof payload === "string" ? payload : JSON.stringify(payload);
148
- lines.push(`submission\t${encodeTerminalField(rendered)}`);
129
+ lines.push(`${label}\t${encodeTerminalField(rendered)}`);
149
130
  }
150
131
  }
151
132
  return `${lines.join("\n")}\n`;
@@ -46,6 +46,9 @@ export function projectRoleTurnRequest(admitted, roleDetails, options) {
46
46
  ? {}
47
47
  : { invocationScopeId: options.invocationScopeId }),
48
48
  ...(options.stationChild === undefined ? {} : { stationChild: options.stationChild }),
49
+ ...(options.deliveryRequestLimit === undefined
50
+ ? {}
51
+ : { deliveryRequestLimit: options.deliveryRequestLimit }),
49
52
  };
50
53
  }
51
54
  /**
@@ -4,6 +4,7 @@ import { realpath } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { isRecord, errorText } from "./unknown-value.js";
7
+ import { serializeThrownValue } from "./serialize-thrown-value.js";
7
8
  const AK_ROLE_PACKAGE_ROOT_ENV = "AK_ROLE_PACKAGE_ROOT";
8
9
  const execFileAsync = promisify(execFile);
9
10
  function createCapturingIo() {
@@ -288,15 +289,9 @@ async function resolveReviewerGitToplevel(projectRoot) {
288
289
  )).stdout.trim());
289
290
  }
290
291
  async function summonParallelReviewerLenses(options) {
291
- const describeFailure = (error) => {
292
- if (error instanceof AggregateError) {
293
- return [error.message, ...error.errors.map(describeFailure)].join("\n");
294
- }
295
- return errorText(error);
296
- };
297
292
  const failedResult = (error) => ({
298
293
  exitCode: 1,
299
- stderr: describeFailure(error)
294
+ stderr: serializeThrownValue(error)
300
295
  });
301
296
  const dualFailure = (error) => {
302
297
  const failure = failedResult(error);
@@ -399,7 +394,7 @@ async function summonParallelReviewerLenses(options) {
399
394
  }
400
395
  } catch (error) {
401
396
  sealDiagnostic = `Reviewer target final seal failed:
402
- ${describeFailure(error)}`;
397
+ ${serializeThrownValue(error)}`;
403
398
  }
404
399
  if (sealDiagnostic !== void 0) {
405
400
  const withSealFailure = (result) => ({
@@ -1,46 +1,71 @@
1
+ import { AUTO_RESUME_LIMIT } from "./public-cli/run-lifecycle.js";
2
+ import { parseAutoResumeLimit } from "./public-cli/config.js";
1
3
  import { isRecord } from "./unknown-value.js";
2
- const RECEIPT_DELIVERY_TURN_LIMIT = 2;
3
4
  const NO_RECEIPT_LIFECYCLE_ENTRY_TYPE = "ak-no-receipt-lifecycle";
5
+ const RECEIPT_DELIVERY_REQUEST_ENTRY = "ak-receipt-delivery-request";
6
+ const RECEIPT_REJECTION_ENTRY = "ak-receipt-rejection";
7
+ function receiptAttemptPointer(runPointer, invocationScopeId) {
8
+ const scope = invocationScopeId?.trim() ?? "";
9
+ if (scope.length > 0) return `invocation:${scope}`;
10
+ return `current:${runPointer}`;
11
+ }
12
+ const RECEIPT_DELIVERY_LIMIT_ENV = "AK_ROLE_RECEIPT_DELIVERY_LIMIT";
13
+ function deliveryLimitFromConfig(value) {
14
+ return parseAutoResumeLimit(value ?? AUTO_RESUME_LIMIT);
15
+ }
16
+ function deliveryLimitFromEnv(env) {
17
+ const raw = env[RECEIPT_DELIVERY_LIMIT_ENV];
18
+ if (raw === void 0 || raw.trim() === "") return deliveryLimitFromConfig(void 0);
19
+ const parsed = Number(raw);
20
+ if (!Number.isInteger(parsed)) return deliveryLimitFromConfig(void 0);
21
+ return deliveryLimitFromConfig(parsed);
22
+ }
23
+ function deliveryTurnsFact(value) {
24
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
25
+ throw new TypeError("deliveryTurns must be a non-negative integer");
26
+ }
27
+ return value;
28
+ }
4
29
  function parseNoReceiptLifecycleFacts(input) {
5
- if (!isRecord(input) || typeof input.terminalToolCalled !== "boolean" || input.deliveryTurns !== RECEIPT_DELIVERY_TURN_LIMIT || input.sessionCompletion !== "settled-without-accepted-receipt" || input.acceptedReceipt !== false || typeof input.runPointer !== "string" || input.runPointer.trim() === "" || typeof input.attemptPointer !== "string" || input.attemptPointer.trim() === "" || !Array.isArray(input.rejectedReceipts) || !input.rejectedReceipts.every((item) => isRecord(item) && typeof item.reason === "string")) {
30
+ if (!isRecord(input) || typeof input.terminalToolCalled !== "boolean" || input.sessionCompletion !== "settled-without-accepted-receipt" || input.acceptedReceipt !== false || typeof input.runPointer !== "string" || input.runPointer.trim() === "" || typeof input.attemptPointer !== "string" || input.attemptPointer.trim() === "" || !Array.isArray(input.rejectedReceipts) || !input.rejectedReceipts.every((item) => isRecord(item) && typeof item.reason === "string")) {
6
31
  throw new TypeError("malformed no-receipt lifecycle facts");
7
32
  }
8
- return {
33
+ return noReceiptLifecycleFacts({
9
34
  terminalToolCalled: input.terminalToolCalled,
10
- rejectedReceipts: input.rejectedReceipts.map((item) => ({
11
- reason: item.reason,
12
- diagnosticAvailable: item.reason.trim() !== ""
13
- })),
14
- deliveryTurns: RECEIPT_DELIVERY_TURN_LIMIT,
15
- sessionCompletion: "settled-without-accepted-receipt",
35
+ rejectedReceipts: input.rejectedReceipts,
36
+ deliveryTurns: deliveryTurnsFact(input.deliveryTurns),
16
37
  runPointer: input.runPointer,
17
- attemptPointer: input.attemptPointer,
18
- acceptedReceipt: false
19
- };
38
+ attemptPointer: input.attemptPointer
39
+ });
20
40
  }
21
41
  function noReceiptLifecycleFacts(input) {
22
- if (input.deliveryTurns !== RECEIPT_DELIVERY_TURN_LIMIT) {
23
- throw new TypeError("no-receipt lifecycle requires an exhausted delivery budget");
24
- }
25
42
  return {
26
43
  terminalToolCalled: input.terminalToolCalled,
27
44
  rejectedReceipts: input.rejectedReceipts.map(({ reason }) => ({
28
45
  reason,
29
46
  diagnosticAvailable: reason.trim() !== ""
30
47
  })),
31
- deliveryTurns: RECEIPT_DELIVERY_TURN_LIMIT,
48
+ deliveryTurns: deliveryTurnsFact(input.deliveryTurns),
32
49
  sessionCompletion: "settled-without-accepted-receipt",
33
50
  runPointer: input.runPointer,
34
51
  attemptPointer: input.attemptPointer,
35
52
  acceptedReceipt: false
36
53
  };
37
54
  }
38
- function createReceiptDeliveryPolicy() {
55
+ function createReceiptDeliveryPolicy(limit) {
56
+ const deliveryLimit = deliveryLimitFromConfig(limit);
39
57
  let accepted = false;
58
+ let closed = false;
40
59
  let terminalToolCalled = false;
41
60
  let deliveryTurns = 0;
61
+ let budgetSpent = 0;
42
62
  const rejectedReceipts = [];
63
+ const spend = () => {
64
+ if (budgetSpent < deliveryLimit) budgetSpent += 1;
65
+ };
43
66
  return {
67
+ /** The effective ceiling this budget spends (#1132 — never re-read). */
68
+ limit: deliveryLimit,
44
69
  recordAccepted() {
45
70
  accepted = true;
46
71
  terminalToolCalled = true;
@@ -49,27 +74,50 @@ function createReceiptDeliveryPolicy() {
49
74
  stopForInfrastructure() {
50
75
  accepted = true;
51
76
  },
77
+ /**
78
+ * Record the rejection this turn already produced.
79
+ * The fact does not spend a budget. The seam that sends the next prompt
80
+ * owns that count.
81
+ */
52
82
  recordRejected(reason) {
53
83
  terminalToolCalled = true;
54
84
  rejectedReceipts.push({ reason, diagnosticAvailable: reason.trim() !== "" });
55
- deliveryTurns = Math.min(RECEIPT_DELIVERY_TURN_LIMIT, deliveryTurns + 1);
56
85
  },
57
86
  recordDeliveryRequest() {
58
- deliveryTurns = Math.min(RECEIPT_DELIVERY_TURN_LIMIT, deliveryTurns + 1);
87
+ deliveryTurns += 1;
88
+ spend();
59
89
  },
60
90
  /**
61
- * A nested session settled without an accepted receipt after spending its own
62
- * delivery budget. Adopt its facts and close this budget: another prompt would
63
- * open an independent session, not a delivery request on the settled one.
91
+ * Stop asking. The issued count stays whatever was actually sent (#1132 —
92
+ * do not fill the remainder up to the ceiling).
93
+ */
94
+ closeBudget() {
95
+ closed = true;
96
+ },
97
+ /**
98
+ * A nested session settled without an accepted receipt. Close so this layer
99
+ * does not open another prompt on that session. The issued count stays the
100
+ * larger of the two seams: a nested zero must not wipe prompts this layer
101
+ * already sent, and adding the two counts would bill one send twice.
64
102
  */
65
103
  recordNestedNoReceipt(facts) {
66
- terminalToolCalled = terminalToolCalled || facts.terminalToolCalled;
67
- rejectedReceipts.push(...facts.rejectedReceipts);
68
- deliveryTurns = RECEIPT_DELIVERY_TURN_LIMIT;
104
+ if (typeof facts.terminalToolCalled === "boolean") {
105
+ terminalToolCalled = terminalToolCalled || facts.terminalToolCalled;
106
+ }
107
+ if (facts.rejectedReceipts !== void 0) {
108
+ rejectedReceipts.push(...facts.rejectedReceipts);
109
+ }
110
+ if (facts.deliveryTurns !== void 0) deliveryTurns = Math.max(deliveryTurns, facts.deliveryTurns);
111
+ closed = true;
69
112
  },
70
113
  nextAction() {
71
114
  if (accepted) return "accepted";
72
- return deliveryTurns < RECEIPT_DELIVERY_TURN_LIMIT ? "request-delivery" : "no-receipt";
115
+ if (closed) return "no-receipt";
116
+ return budgetSpent < deliveryLimit ? "request-delivery" : "no-receipt";
117
+ },
118
+ /** Delivery requests actually issued so far (#1132). */
119
+ issuedDeliveryRequests() {
120
+ return deliveryTurns;
73
121
  },
74
122
  /** Current typed delivery facts. No prose. */
75
123
  deliveryState() {
@@ -82,13 +130,99 @@ function createReceiptDeliveryPolicy() {
82
130
  },
83
131
  facts(binding) {
84
132
  return noReceiptLifecycleFacts({ terminalToolCalled, rejectedReceipts: [...rejectedReceipts], deliveryTurns, ...binding });
133
+ },
134
+ /**
135
+ * Continue a public call after the in-memory policy was rebuilt.
136
+ * Sends already issued spend the same budget; copied rejections keep
137
+ * the fact and do not spend again.
138
+ */
139
+ continueIssued(prior) {
140
+ for (const receipt of prior.rejectedReceipts) {
141
+ this.recordRejected(receipt.reason);
142
+ }
143
+ if (prior.terminalToolCalled === true) terminalToolCalled = true;
144
+ for (let index = 0; index < prior.deliveryTurns; index += 1) {
145
+ this.recordDeliveryRequest();
146
+ }
147
+ }
148
+ };
149
+ }
150
+ function entryRecord(entry) {
151
+ return isRecord(entry) ? entry : void 0;
152
+ }
153
+ function customTypeOf(record, message) {
154
+ return record.customType ?? message?.customType;
155
+ }
156
+ function dataOf(record, message) {
157
+ return record.data ?? message?.details;
158
+ }
159
+ function scopeTag(record) {
160
+ const message = isRecord(record.message) ? record.message : void 0;
161
+ const customType = customTypeOf(record, message);
162
+ const data = dataOf(record, message);
163
+ if ((customType === RECEIPT_DELIVERY_REQUEST_ENTRY || customType === RECEIPT_REJECTION_ENTRY) && isRecord(data) && typeof data.invocationScopeId === "string" && data.invocationScopeId.trim() !== "") {
164
+ return data.invocationScopeId;
165
+ }
166
+ if (customType !== NO_RECEIPT_LIFECYCLE_ENTRY_TYPE) return void 0;
167
+ try {
168
+ const facts = parseNoReceiptLifecycleFacts(data);
169
+ const prefix = "invocation:";
170
+ if (!facts.attemptPointer.startsWith(prefix)) return void 0;
171
+ const scope = facts.attemptPointer.slice(prefix.length);
172
+ return scope.trim() === "" ? void 0 : scope;
173
+ } catch {
174
+ return void 0;
175
+ }
176
+ }
177
+ function priorReceiptContinuation(entries, invocationScopeId) {
178
+ const pointer = receiptAttemptPointer("", invocationScopeId);
179
+ let entryTurns = 0;
180
+ let factTurns = 0;
181
+ let rejectedReceipts = [];
182
+ let snapshotTerminal = false;
183
+ for (const entry of entries) {
184
+ const record = entryRecord(entry);
185
+ if (record === void 0) continue;
186
+ const tagged = scopeTag(record);
187
+ if (tagged !== void 0) {
188
+ const message = isRecord(record.message) ? record.message : void 0;
189
+ const customType = customTypeOf(record, message);
190
+ if (tagged !== invocationScopeId) continue;
191
+ if (customType === RECEIPT_DELIVERY_REQUEST_ENTRY) entryTurns += 1;
192
+ if (customType === RECEIPT_REJECTION_ENTRY) {
193
+ const data = dataOf(record, message);
194
+ if (isRecord(data) && typeof data.reason === "string") rejectedReceipts.push({ reason: data.reason });
195
+ }
196
+ if (customType !== NO_RECEIPT_LIFECYCLE_ENTRY_TYPE) continue;
197
+ try {
198
+ const facts = parseNoReceiptLifecycleFacts(dataOf(record, message));
199
+ if (facts.attemptPointer !== pointer) continue;
200
+ factTurns = Math.max(factTurns, facts.deliveryTurns);
201
+ if (facts.rejectedReceipts.length >= rejectedReceipts.length) {
202
+ rejectedReceipts = facts.rejectedReceipts.map(({ reason }) => ({ reason }));
203
+ }
204
+ snapshotTerminal ||= facts.terminalToolCalled;
205
+ } catch {
206
+ }
207
+ continue;
85
208
  }
209
+ }
210
+ return {
211
+ deliveryTurns: Math.max(entryTurns, factTurns),
212
+ rejectedReceipts,
213
+ terminalToolCalled: snapshotTerminal || rejectedReceipts.length > 0
86
214
  };
87
215
  }
88
216
  export {
89
217
  NO_RECEIPT_LIFECYCLE_ENTRY_TYPE,
90
- RECEIPT_DELIVERY_TURN_LIMIT,
218
+ RECEIPT_DELIVERY_LIMIT_ENV,
219
+ RECEIPT_DELIVERY_REQUEST_ENTRY,
220
+ RECEIPT_REJECTION_ENTRY,
91
221
  createReceiptDeliveryPolicy,
222
+ deliveryLimitFromConfig,
223
+ deliveryLimitFromEnv,
92
224
  noReceiptLifecycleFacts,
93
- parseNoReceiptLifecycleFacts
225
+ parseNoReceiptLifecycleFacts,
226
+ priorReceiptContinuation,
227
+ receiptAttemptPointer
94
228
  };
@@ -1,18 +1,23 @@
1
1
  /**
2
- * Canonical reader for run-directory typed terminal artifacts.
3
- * Layout owner is settlement publish*Artifacts (report.json / error.json /
4
- * audit-incomplete.json under artifacts/, plus the same publisher's durable
5
- * failure fallbacks). This module only reads presence and structural
2
+ * Canonical layout and reader for run-directory typed terminal artifacts.
3
+ * Settlement publish*Artifacts uses these names (report.json / error.json /
4
+ * audit-incomplete.json under artifacts/, plus durable failure fallbacks).
5
+ * The reader only checks presence and structural
6
6
  * readability — it does not re-derive role outcomes or invent a second
7
7
  * candidate algorithm.
8
8
  */
9
+ import { randomUUID } from "node:crypto";
9
10
  import { readdir, readFile } from "node:fs/promises";
10
11
  import { basename, dirname, join } from "node:path";
11
12
  import { roleRunArtifactsDirectory } from "./role-run-placement.js";
12
13
  import { isRecord, errorText, isMissingPathError } from "./unknown-value.js";
14
+ export const RUN_TERMINAL_REPORT_FILE = "report.json";
15
+ export const RUN_TERMINAL_ERROR_FILE = "error.json";
16
+ export const RUN_TERMINAL_EVIDENCE_FILE = "evidence.json";
17
+ export const RUN_TERMINAL_ERROR_SETTLEMENT_FILE = "error.settlement.json";
13
18
  export const RUN_TERMINAL_ARTIFACT_FILES = [
14
- "report.json",
15
- "error.json",
19
+ RUN_TERMINAL_REPORT_FILE,
20
+ RUN_TERMINAL_ERROR_FILE,
16
21
  "audit-incomplete.json",
17
22
  ];
18
23
  /**
@@ -22,11 +27,15 @@ export const RUN_TERMINAL_ARTIFACT_FILES = [
22
27
  * Relative to the run directory.
23
28
  */
24
29
  export const RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS = [
25
- "artifacts/error.settlement.json",
26
- "error.settlement.json",
30
+ `artifacts/${RUN_TERMINAL_ERROR_SETTLEMENT_FILE}`,
31
+ RUN_TERMINAL_ERROR_SETTLEMENT_FILE,
27
32
  ];
28
- /** Unique open-ended failure names: error.<uuid>.json (publisher stem + uuid). */
29
- const UNIQUE_ERROR_FALLBACK_NAME = /^error\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.json$/i;
33
+ /** Publisher and reader share the open-ended error face stem. */
34
+ export const UNIQUE_ERROR_FALLBACK_STEM = "error";
35
+ export function uniqueErrorFallbackName() {
36
+ return `${UNIQUE_ERROR_FALLBACK_STEM}.${randomUUID()}.json`;
37
+ }
38
+ const UNIQUE_ERROR_FALLBACK_NAME = new RegExp(`^${UNIQUE_ERROR_FALLBACK_STEM}\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\.json$`, "i");
30
39
  /**
31
40
  * Minimum producer-owned face shared by settlement terminal artifacts
32
41
  * (report / error / audit-incomplete). Consumer-driven: enough to identify a
@@ -219,7 +228,3 @@ export async function readRunTerminalArtifact(runDirectory) {
219
228
  }
220
229
  return { status: "absent" };
221
230
  }
222
- /** Test/helper: basename face of a unique fallback path, if any. */
223
- export function isUniqueErrorFallbackName(name) {
224
- return UNIQUE_ERROR_FALLBACK_NAME.test(basename(name));
225
- }
@@ -1,5 +1,11 @@
1
1
  import { inspect } from "node:util";
2
2
  /** Record the whole thrown value without invoking its getters or custom formatter. */
3
3
  export function serializeThrownValue(value) {
4
- return inspect(value, { depth: 10, showHidden: true, customInspect: false });
4
+ return inspect(value, {
5
+ depth: null,
6
+ maxArrayLength: null,
7
+ maxStringLength: null,
8
+ showHidden: true,
9
+ customInspect: false,
10
+ });
5
11
  }
@@ -44,10 +44,20 @@ export class WorkerPrefixReminderError extends Error {
44
44
  this.name = "WorkerPrefixReminderError";
45
45
  }
46
46
  }
47
+ /**
48
+ * ADR 0066/0070 one-shot commit and prefix reminders.
49
+ * They stay a correction the same session may make, and they do not occupy
50
+ * the configured re-ask or 催交 budget (#1132).
51
+ */
52
+ export function isOneShotWorkerReminderCode(code) {
53
+ return code === "worker_commit_reminder" || code === "worker_prefix_reminder";
54
+ }
47
55
  export class WorkerUnfinishedReasonReminderError extends Error {
48
56
  code = "worker_unfinished_reason_reminder";
49
57
  constructor() {
50
- super("本次 unfinished 回执未含 reason;本接缝缺由至多打回两次。");
58
+ // The ceiling is the configured unfinished-reason limit. This text reaches
59
+ // the model and does not restate a count.
60
+ super("本次 unfinished 回执未含 reason;请补上 reason 后再交。");
51
61
  this.name = "WorkerUnfinishedReasonReminderError";
52
62
  }
53
63
  }
@@ -1,6 +1,7 @@
1
1
  import { bookDirectOfficerRunPointer } from "./archivist-record-pointer.js";
2
2
  import { GatekeeperDecisionError, officerConclusionReask, projectGatekeeperRun, } from "./gatekeeper-role.js";
3
3
  import { receivedDiscriminator } from "./submission-errors.js";
4
+ import { deliveryLimitFromConfig } from "./receipt-delivery-policy.js";
4
5
  /** Received discriminator of the latest payload, retained verbatim for re-ask. */
5
6
  export function latestQueueStatus(terminal) {
6
7
  const outcome = terminal?.roleOutcome;
@@ -54,7 +55,7 @@ export function createDefaultGateOfficerSummon(options) {
54
55
  * Book a typed pointer under parent session/auditor-roles (archivist-owned write).
55
56
  * Offline mocks without a real session leave no nested volume (lawful zero).
56
57
  */
57
- function bookDirectOfficerPointer(context, officer, result, summoned) {
58
+ function bookDirectOfficerPointer(context, officer, result, summoned, toolCallId) {
58
59
  if (result.status !== "converged"
59
60
  && result.status !== "continue"
60
61
  && result.status !== "escalate"
@@ -77,16 +78,20 @@ function bookDirectOfficerPointer(context, officer, result, summoned) {
77
78
  ...(typeof summoned.runDirectory === "string" && summoned.runDirectory.trim() !== ""
78
79
  ? { runDirectory: summoned.runDirectory }
79
80
  : {}),
81
+ ...(toolCallId.trim() === "" ? {} : { submissionToolCallId: toolCallId }),
80
82
  });
81
83
  }
82
84
  /**
83
85
  * Shared envelope: project gate, book officer pointer, map onto host actions.
84
- * Review loop has no round cap (#753 no-round-cap).
86
+ * A real converged / continue / escalate returns immediately. An unreadable
87
+ * conclusion reasks that officer against the configured ceiling; exhaustion
88
+ * returns the receipt actually received.
85
89
  * On converged, returns the officer snapshot (receipt + nested runId) for seat projection.
86
90
  */
87
91
  export async function requireSubmissionGate(options) {
88
92
  let reask;
89
- // No round cap — end only on converged, continue/escalate, or real failure.
93
+ let reasksSpent = 0;
94
+ const reaskLimit = deliveryLimitFromConfig(options.autoResumeLimit);
90
95
  const summonOfficer = options.summonOfficer ??
91
96
  createDefaultGateOfficerSummon({
92
97
  cwd: options.context.cwd ?? process.cwd(),
@@ -104,7 +109,7 @@ export async function requireSubmissionGate(options) {
104
109
  // Envelope-owned pointer book. Failure is host infrastructure — single face.
105
110
  if (projected.summoned !== undefined) {
106
111
  try {
107
- bookDirectOfficerPointer(options.context, projected.officer, gatekeeper, projected.summoned);
112
+ bookDirectOfficerPointer(options.context, projected.officer, gatekeeper, projected.summoned, options.toolCallId);
108
113
  }
109
114
  catch (error) {
110
115
  options.hostActions.failInfrastructure(error, options.context, options.toolCallId);
@@ -131,6 +136,24 @@ export async function requireSubmissionGate(options) {
131
136
  };
132
137
  }
133
138
  if (gatekeeper.status === "needs_reask") {
139
+ if (reasksSpent >= reaskLimit) {
140
+ return {
141
+ status: "needs_reask",
142
+ officer: projected.officer,
143
+ receipt: gatekeeper.receipt,
144
+ ...(typeof gatekeeper.runId === "string" && gatekeeper.runId.trim() !== ""
145
+ ? { runId: gatekeeper.runId }
146
+ : {}),
147
+ ...(typeof projected.summoned?.runDirectory === "string"
148
+ && projected.summoned.runDirectory.trim() !== ""
149
+ ? { runDirectory: projected.summoned.runDirectory }
150
+ : {}),
151
+ ...(projected.summoned?.terminal === undefined
152
+ ? {}
153
+ : { terminal: projected.summoned.terminal }),
154
+ };
155
+ }
156
+ reasksSpent += 1;
134
157
  reask = officerConclusionReask(gatekeeper.receivedStatus);
135
158
  continue;
136
159
  }