@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.
- package/README.md +4 -2
- package/README.zh-CN.md +4 -2
- package/dist/acp-host/production-host.js +8843 -9012
- package/dist/archivist-record-pointer.js +38 -0
- package/dist/compliance-transport.js +7 -5
- package/dist/doctor-auditor.js +1 -0
- package/dist/engine-detour-usage.js +9 -36
- package/dist/gatekeeper-role.js +4 -18
- package/dist/headless-host/description.js +0 -209
- package/dist/headless-host/production-host.js +7828 -8247
- package/dist/host-contracts.js +8 -0
- package/dist/judge-role.js +14 -1
- package/dist/migrate-book-topology.js +215 -96
- package/dist/navigator-attendance.js +10 -9
- package/dist/navigator-invocation-identity.js +18 -0
- package/dist/packaged-role-registry.js +1 -9
- package/dist/pi/role-turn-host.js +46 -19
- package/dist/public-cli/auto-resume.js +29 -66
- package/dist/public-cli/cli.js +1 -0
- package/dist/public-cli/countersign-run.js +4 -18
- package/dist/public-cli/instruction-seat-run.js +258 -69
- package/dist/public-cli/main.js +7488 -7758
- package/dist/public-cli/post-admission.js +451 -349
- package/dist/public-cli/process-cancel.js +8 -0
- package/dist/public-cli/run-lifecycle.js +24 -94
- package/dist/public-cli/settlement.js +624 -1471
- package/dist/public-cli/terminal.js +34 -53
- package/dist/public-cli/turn-request.js +3 -0
- package/dist/public-role-summons.js +3 -8
- package/dist/receipt-delivery-policy.js +162 -28
- package/dist/run-terminal-artifacts.js +19 -14
- package/dist/serialize-thrown-value.js +7 -1
- package/dist/submission-errors.js +11 -1
- package/dist/submission-gate.js +27 -4
- package/dist/submission-ledger.js +21 -89
- package/package.json +1 -1
- package/scripts/build-package.mjs +0 -2
- package/src/acp-host/role-turn-host.ts +168 -75
- package/src/archivist-record-pointer.ts +48 -0
- package/src/compliance-transport.ts +13 -8
- package/src/doctor-auditor.ts +3 -0
- package/src/engine-detour-usage.ts +10 -39
- package/src/external-host-turn-loop.ts +100 -43
- package/src/gatekeeper-role.ts +7 -22
- package/src/headless-host/description.ts +4 -214
- package/src/headless-host/production-host.ts +2 -1
- package/src/headless-host/role-turn-host.ts +153 -186
- package/src/host-contracts.ts +20 -10
- package/src/judge-role.ts +24 -5
- package/src/navigator-attendance.ts +17 -12
- package/src/navigator-invocation-identity.ts +31 -0
- package/src/navigator-public-session.ts +1 -1
- package/src/navigator-session-contracts.ts +1 -1
- package/src/packaged-role-registry.ts +2 -10
- package/src/pi/adapter.ts +33 -11
- package/src/pi/role-turn-host.ts +49 -28
- package/src/prepared-role-turn.ts +2 -0
- package/src/public-cli/auto-resume.ts +27 -66
- package/src/public-cli/cli.ts +1 -0
- package/src/public-cli/countersign-run.ts +4 -22
- package/src/public-cli/instruction-seat-run.ts +307 -74
- package/src/public-cli/post-admission.ts +615 -459
- package/src/public-cli/process-cancel.ts +9 -0
- package/src/public-cli/run-lifecycle.ts +25 -126
- package/src/public-cli/settlement.ts +801 -1859
- package/src/public-cli/terminal.ts +42 -70
- package/src/public-cli/turn-request.ts +10 -1
- package/src/public-role-summons.ts +3 -8
- package/src/receipt-delivery-policy.ts +219 -28
- package/src/role-envelope.ts +21 -10
- package/src/role-runtime-dependencies.ts +1 -0
- package/src/role-runtime.ts +79 -102
- package/src/run-terminal-artifacts.ts +21 -17
- package/src/serialize-thrown-value.ts +7 -1
- package/src/submission-correctable-error.ts +5 -2
- package/src/submission-errors.ts +12 -1
- package/src/submission-gate.ts +34 -4
- package/src/submission-ledger.ts +30 -116
- package/src/worker-role.ts +19 -7
- package/src/worker-submission-gates.ts +29 -7
- package/dist/audit-escalation.js +0 -7
- package/dist/pi/known-failure.js +0 -51
- package/dist/typed-provider-http.js +0 -57
- package/dist/upstream-error-testimony.js +0 -19
- package/src/pi/known-failure.ts +0 -76
- package/src/typed-provider-http.ts +0 -89
- package/src/upstream-error-testimony.ts +0 -45
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { NoReceiptLifecycleFacts } from "../receipt-delivery-policy.ts";
|
|
9
9
|
import type { ControlledFailureCause } from "../host-contracts.ts";
|
|
10
10
|
import type { PackagedRole } from "../packaged-role-registry.ts";
|
|
11
|
+
import { serializeThrownValue } from "../serialize-thrown-value.ts";
|
|
11
12
|
|
|
12
13
|
export type { ControlledFailureCause } from "../host-contracts.ts";
|
|
13
14
|
|
|
@@ -25,11 +26,11 @@ export type TerminalArtifactRef = {
|
|
|
25
26
|
/** Public callable roles that currently produce Terminal outcomes. */
|
|
26
27
|
export type TerminalRoleName = PackagedRole;
|
|
27
28
|
|
|
28
|
-
export type NoReceiptTerminalOutcome = NoReceiptLifecycleFacts & {
|
|
29
|
+
export type NoReceiptTerminalOutcome = Partial<NoReceiptLifecycleFacts> & {
|
|
29
30
|
kind: "no_receipt";
|
|
30
31
|
role: TerminalRoleName;
|
|
31
32
|
status: "no-accepted-receipt";
|
|
32
|
-
decisiveFacts: NoReceiptLifecycleFacts & Readonly<Record<string, unknown>>;
|
|
33
|
+
decisiveFacts: Partial<NoReceiptLifecycleFacts> & Readonly<Record<string, unknown>>;
|
|
33
34
|
};
|
|
34
35
|
|
|
35
36
|
export type TerminalRoleOutcome =
|
|
@@ -56,7 +57,7 @@ export type TerminalRoleOutcome =
|
|
|
56
57
|
role: TerminalRoleName;
|
|
57
58
|
/**
|
|
58
59
|
* Typed cause class when a typed fact confirms it.
|
|
59
|
-
* Omitted when unknown — original diagnostic
|
|
60
|
+
* Omitted when unknown — the original diagnostic retains the fact (#881).
|
|
60
61
|
* Never a fabricated "unrecognized" label.
|
|
61
62
|
*/
|
|
62
63
|
cause?: ControlledFailureCause;
|
|
@@ -102,12 +103,6 @@ export type TerminalNavigatorFact =
|
|
|
102
103
|
advisoryDiagnostic?: string;
|
|
103
104
|
};
|
|
104
105
|
|
|
105
|
-
/** Present only when a controlled failure is v1-resumable (typed HTTP 429). */
|
|
106
|
-
export type TerminalResume = {
|
|
107
|
-
/** Complete public command; run ID is revealed only here. */
|
|
108
|
-
readonly command: string;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
106
|
/** Current English gate seat faces projected on Terminal (#478). */
|
|
112
107
|
export type TerminalGateSeat = "gatekeeper" | "inspector" | "notary";
|
|
113
108
|
|
|
@@ -147,38 +142,21 @@ export type TerminalGateFact = {
|
|
|
147
142
|
};
|
|
148
143
|
|
|
149
144
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* #416: autoResumeCount (0..2) is a call-local observation of how many in-place
|
|
154
|
-
* auto-resumes occurred during this single LLM call; it is not persisted to
|
|
155
|
-
* run-state.json and does not participate in limit decisions.
|
|
156
|
-
*/
|
|
157
|
-
/**
|
|
158
|
-
* Current-result payloads on a terminal — accepted/audit role result, or a
|
|
159
|
-
* rare intentional failure face. Run history is TerminalResult.submissions
|
|
160
|
-
* (#836 / #953), not this helper.
|
|
145
|
+
* Parent-facing current reply: a court's own accepted/audit payloads only.
|
|
146
|
+
* Run-scoped history stays on TerminalResult.submissions (#879 / #836), and a
|
|
147
|
+
* failure face never answers as this court's reply (#953).
|
|
161
148
|
*/
|
|
162
|
-
export function
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
149
|
+
export function currentReplyRows(terminal: TerminalResult | undefined): readonly unknown[] {
|
|
150
|
+
const outcome = terminal?.roleOutcome;
|
|
151
|
+
return outcome?.kind === "accepted" || outcome?.kind === "audit_escalation"
|
|
152
|
+
? outcome.payloads ?? []
|
|
153
|
+
: [];
|
|
166
154
|
}
|
|
167
155
|
|
|
168
156
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* read TerminalResult.submissions directly — do not coalesce into failure.payloads.
|
|
157
|
+
* One admitted Role run's typed Terminal aggregate.
|
|
158
|
+
* autoResumeCount is call-local (#416).
|
|
172
159
|
*/
|
|
173
|
-
export function coalesceSubmissionRows(
|
|
174
|
-
payloads: readonly unknown[] | undefined,
|
|
175
|
-
submissions: readonly unknown[] | undefined,
|
|
176
|
-
): readonly unknown[] {
|
|
177
|
-
if (payloads !== undefined && payloads.length > 0) return payloads;
|
|
178
|
-
if (submissions !== undefined && submissions.length > 0) return submissions;
|
|
179
|
-
return payloads ?? submissions ?? [];
|
|
180
|
-
}
|
|
181
|
-
|
|
182
160
|
export type TerminalResult = {
|
|
183
161
|
roleOutcome: TerminalRoleOutcome;
|
|
184
162
|
/** Default Reviewer parent: original child Terminals, keyed by frozen axis. */
|
|
@@ -207,22 +185,14 @@ export type TerminalResult = {
|
|
|
207
185
|
/** Call-local auto-resume observation (0..2) for this single LLM call; read-only, not persisted. */
|
|
208
186
|
autoResumeCount?: number;
|
|
209
187
|
} & (
|
|
210
|
-
| {
|
|
211
|
-
/** Resumable failure: run ID appears only inside resume.command. */
|
|
212
|
-
resume: TerminalResume;
|
|
213
|
-
runId?: undefined;
|
|
214
|
-
batch?: undefined;
|
|
215
|
-
}
|
|
216
188
|
| {
|
|
217
189
|
runId: string;
|
|
218
|
-
resume?: undefined;
|
|
219
190
|
batch?: undefined;
|
|
220
191
|
}
|
|
221
192
|
| {
|
|
222
193
|
/** Deterministic public batch projection; no parent Role run exists. */
|
|
223
194
|
batch: "reviewer";
|
|
224
195
|
runId?: undefined;
|
|
225
|
-
resume?: undefined;
|
|
226
196
|
}
|
|
227
197
|
);
|
|
228
198
|
|
|
@@ -263,6 +233,19 @@ export function formatTerminalResult(result: TerminalResult): string {
|
|
|
263
233
|
`diagnostic\t${encodeTerminalField(result.roleOutcome.diagnostic)}`,
|
|
264
234
|
);
|
|
265
235
|
}
|
|
236
|
+
// Accepted volumes do not dump decisiveFacts unless a failed attempt is
|
|
237
|
+
// recorded. The unsettled-direction fact still has to be visible on its own.
|
|
238
|
+
// audit_escalation already dumps every decisiveFact below.
|
|
239
|
+
if (
|
|
240
|
+
result.roleOutcome.kind === "accepted"
|
|
241
|
+
&& result.roleOutcome.decisiveFacts?.directionUnsettled === true
|
|
242
|
+
) {
|
|
243
|
+
lines.push("fact\tdirectionUnsettled\ttrue");
|
|
244
|
+
const subsequent = result.roleOutcome.decisiveFacts.subsequentAudit;
|
|
245
|
+
if (typeof subsequent === "string") {
|
|
246
|
+
lines.push(`fact\tsubsequentAudit\t${encodeTerminalField(subsequent)}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
266
249
|
if (result.roleOutcome.kind === "failure" || result.roleOutcome.kind === "no_receipt" || result.roleOutcome.kind === "audit_escalation" || result.roleOutcome.decisiveFacts?.failedAttempts !== undefined) {
|
|
267
250
|
const facts = result.roleOutcome.kind === "accepted"
|
|
268
251
|
? { failedAttempts: result.roleOutcome.decisiveFacts?.failedAttempts }
|
|
@@ -270,7 +253,7 @@ export function formatTerminalResult(result: TerminalResult): string {
|
|
|
270
253
|
for (const [key, value] of Object.entries(facts)) {
|
|
271
254
|
if (value === undefined) continue;
|
|
272
255
|
const rendered =
|
|
273
|
-
typeof value === "string" ? value :
|
|
256
|
+
typeof value === "string" ? value : serializeThrownValue(value);
|
|
274
257
|
lines.push(`fact\t${encodeTerminalField(key)}\t${encodeTerminalField(rendered)}`);
|
|
275
258
|
}
|
|
276
259
|
}
|
|
@@ -315,10 +298,7 @@ export function formatTerminalResult(result: TerminalResult): string {
|
|
|
315
298
|
}
|
|
316
299
|
}
|
|
317
300
|
}
|
|
318
|
-
if (result.
|
|
319
|
-
// Resumable failure: run ID is revealed only inside the complete resume command.
|
|
320
|
-
lines.push(`resume\t${encodeTerminalField(result.resume.command)}`);
|
|
321
|
-
} else if (result.runId !== undefined) {
|
|
301
|
+
if (result.runId !== undefined) {
|
|
322
302
|
lines.push(`run\t${encodeTerminalField(result.runId)}`);
|
|
323
303
|
}
|
|
324
304
|
if (result.autoResumeCount !== undefined) {
|
|
@@ -326,27 +306,19 @@ export function formatTerminalResult(result: TerminalResult): string {
|
|
|
326
306
|
}
|
|
327
307
|
// Role-result block: original payloads, newest first for humans (#961).
|
|
328
308
|
// Typed payloads/submissions stay ledger order; only this presentation reverses.
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
result.roleOutcome.kind === "audit_escalation"
|
|
343
|
-
? coalesceSubmissionRows(result.roleOutcome.payloads, result.submissions)
|
|
344
|
-
: result.submissions ?? [];
|
|
345
|
-
for (let i = payloads.length - 1; i >= 0; i -= 1) {
|
|
346
|
-
const payload = payloads[i]!;
|
|
347
|
-
const rendered =
|
|
348
|
-
typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
349
|
-
lines.push(`submission\t${encodeTerminalField(rendered)}`);
|
|
309
|
+
// Present the current result and the run history on distinct faces (#879 / #836).
|
|
310
|
+
// Neither a prior court's submission nor a failed turn's history is this reply.
|
|
311
|
+
const current = result.roleOutcome.kind === "failure"
|
|
312
|
+
? result.roleOutcome.payloads ?? []
|
|
313
|
+
: currentReplyRows(result);
|
|
314
|
+
for (const [label, rows] of [
|
|
315
|
+
["submission", current],
|
|
316
|
+
["recorded-submission", result.submissions ?? []],
|
|
317
|
+
] as const) {
|
|
318
|
+
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
319
|
+
const payload = rows[i]!;
|
|
320
|
+
const rendered = typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
321
|
+
lines.push(`${label}\t${encodeTerminalField(rendered)}`);
|
|
350
322
|
}
|
|
351
323
|
}
|
|
352
324
|
return `${lines.join("\n")}\n`;
|
|
@@ -70,7 +70,13 @@ export type RoleTurnRequestProjectionOptions = {
|
|
|
70
70
|
/** #537 public-invocation scope (one ak-role call). */
|
|
71
71
|
invocationScopeId?: string;
|
|
72
72
|
/** Station child role run (#840): omit automatic navigator attendance. */
|
|
73
|
-
stationChild?: boolean;
|
|
73
|
+
readonly stationChild?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* #1132: the one effective delivery-request ceiling (#422 single resolve by
|
|
76
|
+
* the caller). Projected so the host adapter's own re-ask loop and the AK
|
|
77
|
+
* execution seam count with the same configured number.
|
|
78
|
+
*/
|
|
79
|
+
readonly deliveryRequestLimit?: number;
|
|
74
80
|
};
|
|
75
81
|
|
|
76
82
|
export type AdmittedTurnInvocation = {
|
|
@@ -118,6 +124,9 @@ export function projectRoleTurnRequest(
|
|
|
118
124
|
? {}
|
|
119
125
|
: { invocationScopeId: options.invocationScopeId }),
|
|
120
126
|
...(options.stationChild === undefined ? {} : { stationChild: options.stationChild }),
|
|
127
|
+
...(options.deliveryRequestLimit === undefined
|
|
128
|
+
? {}
|
|
129
|
+
: { deliveryRequestLimit: options.deliveryRequestLimit }),
|
|
121
130
|
};
|
|
122
131
|
}
|
|
123
132
|
|
|
@@ -27,6 +27,7 @@ import type { TerminalResult } from "./public-cli/terminal.ts";
|
|
|
27
27
|
import type { HostSelectionFailure, NamedRoleTurnHostAdapter } from "./public-cli/role-turn-host-resolution.ts";
|
|
28
28
|
|
|
29
29
|
import { isRecord, errorText } from "./unknown-value.ts";
|
|
30
|
+
import { serializeThrownValue } from "./serialize-thrown-value.ts";
|
|
30
31
|
|
|
31
32
|
/** Env published by the parent activation so nested summons never re-derive root. */
|
|
32
33
|
export const AK_ROLE_PACKAGE_ROOT_ENV = "AK_ROLE_PACKAGE_ROOT" as const;
|
|
@@ -588,15 +589,9 @@ export async function summonParallelReviewerLenses(options: {
|
|
|
588
589
|
readonly completeness: PublicSummonResult;
|
|
589
590
|
readonly correctness: PublicSummonResult;
|
|
590
591
|
}> {
|
|
591
|
-
const describeFailure = (error: unknown): string => {
|
|
592
|
-
if (error instanceof AggregateError) {
|
|
593
|
-
return [error.message, ...error.errors.map(describeFailure)].join("\n");
|
|
594
|
-
}
|
|
595
|
-
return errorText(error);
|
|
596
|
-
};
|
|
597
592
|
const failedResult = (error: unknown): PublicSummonResult => ({
|
|
598
593
|
exitCode: 1,
|
|
599
|
-
stderr:
|
|
594
|
+
stderr: serializeThrownValue(error),
|
|
600
595
|
});
|
|
601
596
|
const dualFailure = (error: unknown) => {
|
|
602
597
|
const failure = failedResult(error);
|
|
@@ -720,7 +715,7 @@ export async function summonParallelReviewerLenses(options: {
|
|
|
720
715
|
].join("\n");
|
|
721
716
|
}
|
|
722
717
|
} catch (error) {
|
|
723
|
-
sealDiagnostic = `Reviewer target final seal failed:\n${
|
|
718
|
+
sealDiagnostic = `Reviewer target final seal failed:\n${serializeThrownValue(error)}`;
|
|
724
719
|
}
|
|
725
720
|
if (sealDiagnostic !== undefined) {
|
|
726
721
|
const withSealFailure = (result: PublicSummonResult): PublicSummonResult => ({
|
|
@@ -1,14 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared accepted-receipt delivery budget for role, auditor, and Navigator sessions (#288).
|
|
3
|
+
*
|
|
4
|
+
* #1132: the ceiling is the one configured auto-resume value (public-cli.json
|
|
5
|
+
* top-level `autoResumeLimit`). This module owns no number of its own — the one
|
|
6
|
+
* package default stays with AUTO_RESUME_LIMIT (#422), and `deliveryTurns`
|
|
7
|
+
* records the delivery requests actually issued, never the budget
|
|
8
|
+
* (零次即记零,不把额度或其他续跑次数冒充催交次数).
|
|
9
|
+
*/
|
|
10
|
+
import { AUTO_RESUME_LIMIT } from "./public-cli/run-lifecycle.ts";
|
|
11
|
+
import { parseAutoResumeLimit } from "./public-cli/config.ts";
|
|
1
12
|
import { isRecord } from "./unknown-value.ts";
|
|
2
|
-
/** Shared accepted-receipt delivery budget for role, auditor, and Navigator sessions (#288). */
|
|
3
|
-
export const RECEIPT_DELIVERY_TURN_LIMIT = 2 as const;
|
|
4
13
|
|
|
5
14
|
export const NO_RECEIPT_LIFECYCLE_ENTRY_TYPE = "ak-no-receipt-lifecycle" as const;
|
|
6
15
|
|
|
16
|
+
/** One in-process催交 send, tagged with the public call that issued it. */
|
|
17
|
+
export const RECEIPT_DELIVERY_REQUEST_ENTRY = "ak-receipt-delivery-request" as const;
|
|
18
|
+
|
|
19
|
+
/** A rejection observed by the output-tool hook, bound before any delivery turn. */
|
|
20
|
+
export const RECEIPT_REJECTION_ENTRY = "ak-receipt-rejection" as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Facts of one public call. A call with an invocation scope does not share
|
|
24
|
+
* `current:<runDirectory>` with the previous call on the same run.
|
|
25
|
+
*/
|
|
26
|
+
export function receiptAttemptPointer(runPointer: string, invocationScopeId?: string): string {
|
|
27
|
+
const scope = invocationScopeId?.trim() ?? "";
|
|
28
|
+
if (scope.length > 0) return `invocation:${scope}`;
|
|
29
|
+
return `current:${runPointer}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Child-process transport for the effective ceiling (#1132). */
|
|
33
|
+
export const RECEIPT_DELIVERY_LIMIT_ENV = "AK_ROLE_RECEIPT_DELIVERY_LIMIT" as const;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The effective receipt-delivery ceiling: the caller's already-resolved
|
|
37
|
+
* configured value, else the single package default. Domain-validated at this
|
|
38
|
+
* one seam (#422) so NaN/negative/fractional/Infinity never silently disable
|
|
39
|
+
* or bypass the comparison downstream.
|
|
40
|
+
*/
|
|
41
|
+
export function deliveryLimitFromConfig(value: number | undefined): number {
|
|
42
|
+
return parseAutoResumeLimit(value ?? AUTO_RESUME_LIMIT);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Effective ceiling for an in-child role runtime reading its own env (#1132). */
|
|
46
|
+
export function deliveryLimitFromEnv(env: NodeJS.ProcessEnv): number {
|
|
47
|
+
const raw = env[RECEIPT_DELIVERY_LIMIT_ENV];
|
|
48
|
+
if (raw === undefined || raw.trim() === "") return deliveryLimitFromConfig(undefined);
|
|
49
|
+
const parsed = Number(raw);
|
|
50
|
+
if (!Number.isInteger(parsed)) return deliveryLimitFromConfig(undefined);
|
|
51
|
+
return deliveryLimitFromConfig(parsed);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function deliveryTurnsFact(value: unknown): number {
|
|
55
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
56
|
+
throw new TypeError("deliveryTurns must be a non-negative integer");
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
7
61
|
/** The sole schema shared by lifecycle owners and Terminal projections. */
|
|
8
62
|
export type NoReceiptLifecycleFacts = {
|
|
9
63
|
terminalToolCalled: boolean;
|
|
10
64
|
rejectedReceipts: readonly { reason: string; diagnosticAvailable: boolean }[];
|
|
11
|
-
|
|
65
|
+
/** Delivery requests actually issued on this run (#1132 — not a budget). */
|
|
66
|
+
deliveryTurns: number;
|
|
12
67
|
sessionCompletion: "settled-without-accepted-receipt";
|
|
13
68
|
runPointer: string;
|
|
14
69
|
attemptPointer: string;
|
|
@@ -19,7 +74,6 @@ export type NoReceiptLifecycleFacts = {
|
|
|
19
74
|
export function parseNoReceiptLifecycleFacts(input: unknown): NoReceiptLifecycleFacts {
|
|
20
75
|
if (!isRecord(input)
|
|
21
76
|
|| typeof input.terminalToolCalled !== "boolean"
|
|
22
|
-
|| input.deliveryTurns !== RECEIPT_DELIVERY_TURN_LIMIT
|
|
23
77
|
|| input.sessionCompletion !== "settled-without-accepted-receipt"
|
|
24
78
|
|| input.acceptedReceipt !== false
|
|
25
79
|
|| typeof input.runPointer !== "string" || input.runPointer.trim() === ""
|
|
@@ -29,36 +83,33 @@ export function parseNoReceiptLifecycleFacts(input: unknown): NoReceiptLifecycle
|
|
|
29
83
|
&& typeof item.reason === "string")) {
|
|
30
84
|
throw new TypeError("malformed no-receipt lifecycle facts");
|
|
31
85
|
}
|
|
32
|
-
return {
|
|
86
|
+
return noReceiptLifecycleFacts({
|
|
33
87
|
terminalToolCalled: input.terminalToolCalled,
|
|
34
|
-
rejectedReceipts: input.rejectedReceipts
|
|
35
|
-
|
|
36
|
-
diagnosticAvailable: (item.reason as string).trim() !== "",
|
|
37
|
-
})),
|
|
38
|
-
deliveryTurns: RECEIPT_DELIVERY_TURN_LIMIT,
|
|
39
|
-
sessionCompletion: "settled-without-accepted-receipt",
|
|
88
|
+
rejectedReceipts: input.rejectedReceipts as { reason: string }[],
|
|
89
|
+
deliveryTurns: deliveryTurnsFact(input.deliveryTurns),
|
|
40
90
|
runPointer: input.runPointer,
|
|
41
91
|
attemptPointer: input.attemptPointer,
|
|
42
|
-
|
|
43
|
-
};
|
|
92
|
+
});
|
|
44
93
|
}
|
|
45
94
|
|
|
95
|
+
/**
|
|
96
|
+
* #1132: exhaustion is the caller's decision — the receipt-delivery loop only
|
|
97
|
+
* settles after its budget is actually spent — so this records the issued
|
|
98
|
+
* count verbatim instead of demanding it equal a package constant.
|
|
99
|
+
*/
|
|
46
100
|
export function noReceiptLifecycleFacts(
|
|
47
101
|
input: Omit<NoReceiptLifecycleFacts, "rejectedReceipts" | "deliveryTurns" | "sessionCompletion" | "acceptedReceipt"> & {
|
|
48
102
|
rejectedReceipts: readonly { reason: string }[];
|
|
49
103
|
deliveryTurns: number;
|
|
50
104
|
},
|
|
51
105
|
): NoReceiptLifecycleFacts {
|
|
52
|
-
if (input.deliveryTurns !== RECEIPT_DELIVERY_TURN_LIMIT) {
|
|
53
|
-
throw new TypeError("no-receipt lifecycle requires an exhausted delivery budget");
|
|
54
|
-
}
|
|
55
106
|
return {
|
|
56
107
|
terminalToolCalled: input.terminalToolCalled,
|
|
57
108
|
rejectedReceipts: input.rejectedReceipts.map(({ reason }) => ({
|
|
58
109
|
reason,
|
|
59
110
|
diagnosticAvailable: reason.trim() !== "",
|
|
60
111
|
})),
|
|
61
|
-
deliveryTurns:
|
|
112
|
+
deliveryTurns: deliveryTurnsFact(input.deliveryTurns),
|
|
62
113
|
sessionCompletion: "settled-without-accepted-receipt",
|
|
63
114
|
runPointer: input.runPointer,
|
|
64
115
|
attemptPointer: input.attemptPointer,
|
|
@@ -66,39 +117,70 @@ export function noReceiptLifecycleFacts(
|
|
|
66
117
|
};
|
|
67
118
|
}
|
|
68
119
|
|
|
69
|
-
export function createReceiptDeliveryPolicy() {
|
|
120
|
+
export function createReceiptDeliveryPolicy(limit?: number) {
|
|
121
|
+
const deliveryLimit = deliveryLimitFromConfig(limit);
|
|
70
122
|
let accepted = false;
|
|
123
|
+
let closed = false;
|
|
71
124
|
let terminalToolCalled = false;
|
|
125
|
+
/** Requests actually sent. Rejections and a nested close do not invent sends. */
|
|
72
126
|
let deliveryTurns = 0;
|
|
127
|
+
/** Stop counter. Separate from the issued-count fact (#1132). */
|
|
128
|
+
let budgetSpent = 0;
|
|
73
129
|
const rejectedReceipts: { reason: string; diagnosticAvailable: boolean }[] = [];
|
|
130
|
+
const spend = (): void => {
|
|
131
|
+
if (budgetSpent < deliveryLimit) budgetSpent += 1;
|
|
132
|
+
};
|
|
74
133
|
return {
|
|
134
|
+
/** The effective ceiling this budget spends (#1132 — never re-read). */
|
|
135
|
+
limit: deliveryLimit,
|
|
75
136
|
recordAccepted() {
|
|
76
137
|
accepted = true;
|
|
77
138
|
terminalToolCalled = true;
|
|
78
139
|
},
|
|
79
140
|
/** Infrastructure owns terminality and must never trigger receipt催交. */
|
|
80
141
|
stopForInfrastructure() { accepted = true; },
|
|
142
|
+
/**
|
|
143
|
+
* Record the rejection this turn already produced.
|
|
144
|
+
* The fact does not spend a budget. The seam that sends the next prompt
|
|
145
|
+
* owns that count.
|
|
146
|
+
*/
|
|
81
147
|
recordRejected(reason: string) {
|
|
82
148
|
terminalToolCalled = true;
|
|
83
149
|
rejectedReceipts.push({ reason, diagnosticAvailable: reason.trim() !== "" });
|
|
84
|
-
deliveryTurns = Math.min(RECEIPT_DELIVERY_TURN_LIMIT, deliveryTurns + 1);
|
|
85
150
|
},
|
|
86
151
|
recordDeliveryRequest() {
|
|
87
|
-
deliveryTurns
|
|
152
|
+
deliveryTurns += 1;
|
|
153
|
+
spend();
|
|
88
154
|
},
|
|
89
155
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
|
|
156
|
+
* Stop asking. The issued count stays whatever was actually sent (#1132 —
|
|
157
|
+
* do not fill the remainder up to the ceiling).
|
|
158
|
+
*/
|
|
159
|
+
closeBudget() { closed = true; },
|
|
160
|
+
/**
|
|
161
|
+
* A nested session settled without an accepted receipt. Close so this layer
|
|
162
|
+
* does not open another prompt on that session. The issued count stays the
|
|
163
|
+
* larger of the two seams: a nested zero must not wipe prompts this layer
|
|
164
|
+
* already sent, and adding the two counts would bill one send twice.
|
|
93
165
|
*/
|
|
94
|
-
recordNestedNoReceipt(facts: NoReceiptLifecycleFacts) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
166
|
+
recordNestedNoReceipt(facts: Partial<NoReceiptLifecycleFacts>) {
|
|
167
|
+
if (typeof facts.terminalToolCalled === "boolean") {
|
|
168
|
+
terminalToolCalled = terminalToolCalled || facts.terminalToolCalled;
|
|
169
|
+
}
|
|
170
|
+
if (facts.rejectedReceipts !== undefined) {
|
|
171
|
+
rejectedReceipts.push(...facts.rejectedReceipts);
|
|
172
|
+
}
|
|
173
|
+
if (facts.deliveryTurns !== undefined) deliveryTurns = Math.max(deliveryTurns, facts.deliveryTurns);
|
|
174
|
+
closed = true;
|
|
98
175
|
},
|
|
99
176
|
nextAction(): "accepted" | "request-delivery" | "no-receipt" {
|
|
100
177
|
if (accepted) return "accepted";
|
|
101
|
-
|
|
178
|
+
if (closed) return "no-receipt";
|
|
179
|
+
return budgetSpent < deliveryLimit ? "request-delivery" : "no-receipt";
|
|
180
|
+
},
|
|
181
|
+
/** Delivery requests actually issued so far (#1132). */
|
|
182
|
+
issuedDeliveryRequests(): number {
|
|
183
|
+
return deliveryTurns;
|
|
102
184
|
},
|
|
103
185
|
/** Current typed delivery facts. No prose. */
|
|
104
186
|
deliveryState() {
|
|
@@ -112,5 +194,114 @@ export function createReceiptDeliveryPolicy() {
|
|
|
112
194
|
facts(binding: { runPointer: string; attemptPointer: string }): NoReceiptLifecycleFacts {
|
|
113
195
|
return noReceiptLifecycleFacts({ terminalToolCalled, rejectedReceipts: [...rejectedReceipts], deliveryTurns, ...binding });
|
|
114
196
|
},
|
|
197
|
+
/**
|
|
198
|
+
* Continue a public call after the in-memory policy was rebuilt.
|
|
199
|
+
* Sends already issued spend the same budget; copied rejections keep
|
|
200
|
+
* the fact and do not spend again.
|
|
201
|
+
*/
|
|
202
|
+
continueIssued(prior: {
|
|
203
|
+
readonly deliveryTurns: number;
|
|
204
|
+
readonly rejectedReceipts: readonly { reason: string }[];
|
|
205
|
+
readonly terminalToolCalled?: boolean;
|
|
206
|
+
}) {
|
|
207
|
+
for (const receipt of prior.rejectedReceipts) {
|
|
208
|
+
this.recordRejected(receipt.reason);
|
|
209
|
+
}
|
|
210
|
+
if (prior.terminalToolCalled === true) terminalToolCalled = true;
|
|
211
|
+
for (let index = 0; index < prior.deliveryTurns; index += 1) {
|
|
212
|
+
this.recordDeliveryRequest();
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function entryRecord(entry: unknown): Record<string, unknown> | undefined {
|
|
219
|
+
return isRecord(entry) ? entry : undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Sends, observed rejections, and the latest lifecycle fact for this public call. */
|
|
223
|
+
export type ReceiptContinuation = {
|
|
224
|
+
readonly deliveryTurns: number;
|
|
225
|
+
readonly rejectedReceipts: readonly { reason: string }[];
|
|
226
|
+
readonly terminalToolCalled: boolean;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
function customTypeOf(record: Record<string, unknown>, message: Record<string, unknown> | undefined): unknown {
|
|
230
|
+
return record.customType ?? message?.customType;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function dataOf(record: Record<string, unknown>, message: Record<string, unknown> | undefined): unknown {
|
|
234
|
+
return record.data ?? message?.details;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Scope id carried by a delivery request or an invocation-scoped lifecycle fact. */
|
|
238
|
+
function scopeTag(record: Record<string, unknown>): string | undefined {
|
|
239
|
+
const message = isRecord(record.message) ? record.message : undefined;
|
|
240
|
+
const customType = customTypeOf(record, message);
|
|
241
|
+
const data = dataOf(record, message);
|
|
242
|
+
if (
|
|
243
|
+
(customType === RECEIPT_DELIVERY_REQUEST_ENTRY || customType === RECEIPT_REJECTION_ENTRY)
|
|
244
|
+
&& isRecord(data)
|
|
245
|
+
&& typeof data.invocationScopeId === "string"
|
|
246
|
+
&& data.invocationScopeId.trim() !== ""
|
|
247
|
+
) {
|
|
248
|
+
return data.invocationScopeId;
|
|
249
|
+
}
|
|
250
|
+
if (customType !== NO_RECEIPT_LIFECYCLE_ENTRY_TYPE) return undefined;
|
|
251
|
+
try {
|
|
252
|
+
const facts = parseNoReceiptLifecycleFacts(data);
|
|
253
|
+
const prefix = "invocation:";
|
|
254
|
+
if (!facts.attemptPointer.startsWith(prefix)) return undefined;
|
|
255
|
+
const scope = facts.attemptPointer.slice(prefix.length);
|
|
256
|
+
return scope.trim() === "" ? undefined : scope;
|
|
257
|
+
} catch {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Restore only facts explicitly bound to this public call, never inferred from dialogue. */
|
|
263
|
+
export function priorReceiptContinuation(
|
|
264
|
+
entries: Iterable<unknown>,
|
|
265
|
+
invocationScopeId: string,
|
|
266
|
+
): ReceiptContinuation {
|
|
267
|
+
const pointer = receiptAttemptPointer("", invocationScopeId);
|
|
268
|
+
let entryTurns = 0;
|
|
269
|
+
let factTurns = 0;
|
|
270
|
+
let rejectedReceipts: { reason: string }[] = [];
|
|
271
|
+
let snapshotTerminal = false;
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
const record = entryRecord(entry);
|
|
274
|
+
if (record === undefined) continue;
|
|
275
|
+
const tagged = scopeTag(record);
|
|
276
|
+
if (tagged !== undefined) {
|
|
277
|
+
const message = isRecord(record.message) ? record.message : undefined;
|
|
278
|
+
const customType = customTypeOf(record, message);
|
|
279
|
+
if (tagged !== invocationScopeId) continue;
|
|
280
|
+
if (customType === RECEIPT_DELIVERY_REQUEST_ENTRY) entryTurns += 1;
|
|
281
|
+
if (customType === RECEIPT_REJECTION_ENTRY) {
|
|
282
|
+
const data = dataOf(record, message);
|
|
283
|
+
if (isRecord(data) && typeof data.reason === "string") rejectedReceipts.push({ reason: data.reason });
|
|
284
|
+
}
|
|
285
|
+
if (customType !== NO_RECEIPT_LIFECYCLE_ENTRY_TYPE) continue;
|
|
286
|
+
try {
|
|
287
|
+
const facts = parseNoReceiptLifecycleFacts(dataOf(record, message));
|
|
288
|
+
if (facts.attemptPointer !== pointer) continue;
|
|
289
|
+
factTurns = Math.max(factTurns, facts.deliveryTurns);
|
|
290
|
+
// Snapshots include all preceding observations. Replay later events,
|
|
291
|
+
// rather than treating their free-text reasons as deduplication keys.
|
|
292
|
+
if (facts.rejectedReceipts.length >= rejectedReceipts.length) {
|
|
293
|
+
rejectedReceipts = facts.rejectedReceipts.map(({ reason }) => ({ reason }));
|
|
294
|
+
}
|
|
295
|
+
snapshotTerminal ||= facts.terminalToolCalled;
|
|
296
|
+
} catch {
|
|
297
|
+
// A malformed historical entry is not this call's count.
|
|
298
|
+
}
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
deliveryTurns: Math.max(entryTurns, factTurns),
|
|
304
|
+
rejectedReceipts,
|
|
305
|
+
terminalToolCalled: snapshotTerminal || rejectedReceipts.length > 0,
|
|
115
306
|
};
|
|
116
307
|
}
|
package/src/role-envelope.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
RoleTurnRequest,
|
|
20
20
|
} from "./host-contracts.ts";
|
|
21
21
|
import { packagedRoleOutputTool } from "./packaged-role-registry.ts";
|
|
22
|
+
import { deliveryLimitFromConfig } from "./receipt-delivery-policy.ts";
|
|
22
23
|
import {
|
|
23
24
|
createRoleRuntimeExtension,
|
|
24
25
|
type RoleRuntimeDependencies,
|
|
@@ -34,6 +35,8 @@ import {
|
|
|
34
35
|
extractInfrastructureFailureEvidence,
|
|
35
36
|
} from "./navigator-invocation-identity.ts";
|
|
36
37
|
import { serializeThrownValue } from "./serialize-thrown-value.ts";
|
|
38
|
+
import { retainPackageFault } from "./public-cli/settlement.ts";
|
|
39
|
+
import { describeErrorIdentity } from "./public-cli/run-lifecycle.ts";
|
|
37
40
|
|
|
38
41
|
import { isRecord, errorText } from "./unknown-value.ts";
|
|
39
42
|
|
|
@@ -250,7 +253,10 @@ export async function prepareRoleEnvelope(options: {
|
|
|
250
253
|
startKeepalive() {},
|
|
251
254
|
stopKeepalive() {},
|
|
252
255
|
};
|
|
253
|
-
createRoleRuntimeExtension(
|
|
256
|
+
createRoleRuntimeExtension(
|
|
257
|
+
options.dependencies,
|
|
258
|
+
deliveryLimitFromConfig(options.request.deliveryRequestLimit),
|
|
259
|
+
)(envelope);
|
|
254
260
|
|
|
255
261
|
const token = randomUUID();
|
|
256
262
|
const server = createServer((socket) => serveSocket(socket));
|
|
@@ -553,14 +559,14 @@ export async function prepareRoleEnvelope(options: {
|
|
|
553
559
|
const dispose = async (): Promise<void> => {
|
|
554
560
|
if (disposed) return;
|
|
555
561
|
disposed = true;
|
|
556
|
-
const
|
|
562
|
+
const requiredFailures: unknown[] = [];
|
|
557
563
|
// session_shutdown may still append package durable entries (navigator
|
|
558
564
|
// attendance when agent_settled was skipped). Drain only after those
|
|
559
565
|
// handlers — single terminal judgment via settleDurableWrites (#959).
|
|
560
566
|
try {
|
|
561
567
|
await emit("session_shutdown", {});
|
|
562
568
|
} catch (error) {
|
|
563
|
-
|
|
569
|
+
requiredFailures.push(error);
|
|
564
570
|
}
|
|
565
571
|
try {
|
|
566
572
|
// Always drain post-shutdown writes. Only throw when the host has not
|
|
@@ -568,7 +574,7 @@ export async function prepareRoleEnvelope(options: {
|
|
|
568
574
|
const alreadyHanded = durableFailureHandedToCloseRound;
|
|
569
575
|
const durableFailure = await settleDurableWrites();
|
|
570
576
|
if (durableFailure !== undefined && !alreadyHanded) {
|
|
571
|
-
|
|
577
|
+
requiredFailures.push(
|
|
572
578
|
Object.assign(
|
|
573
579
|
new Error(durableFailure.failure.diagnostic ?? "durable session entry flush failed"),
|
|
574
580
|
{ code: durableFailure.failure.identity?.code ?? "durable-session-write-failed" },
|
|
@@ -576,7 +582,7 @@ export async function prepareRoleEnvelope(options: {
|
|
|
576
582
|
);
|
|
577
583
|
}
|
|
578
584
|
} catch (error) {
|
|
579
|
-
|
|
585
|
+
requiredFailures.push(error);
|
|
580
586
|
}
|
|
581
587
|
try {
|
|
582
588
|
const closeAll = (server as unknown as { closeAllConnections?: () => void }).closeAllConnections;
|
|
@@ -585,12 +591,17 @@ export async function prepareRoleEnvelope(options: {
|
|
|
585
591
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
586
592
|
});
|
|
587
593
|
} catch (error) {
|
|
588
|
-
|
|
594
|
+
// Closing the relay is ordinary cleanup, not required envelope work.
|
|
595
|
+
await retainPackageFault({
|
|
596
|
+
runDirectory: request.runDirectory,
|
|
597
|
+
diagnostic: `relay close failed beside envelope shutdown: ${describeErrorIdentity(error)}`,
|
|
598
|
+
error,
|
|
599
|
+
});
|
|
589
600
|
}
|
|
590
|
-
if (
|
|
591
|
-
if (
|
|
592
|
-
throw new AggregateError(
|
|
593
|
-
cause:
|
|
601
|
+
if (requiredFailures.length === 1) throw requiredFailures[0];
|
|
602
|
+
if (requiredFailures.length > 1) {
|
|
603
|
+
throw new AggregateError(requiredFailures, "Required envelope shutdown failures", {
|
|
604
|
+
cause: requiredFailures[0],
|
|
594
605
|
});
|
|
595
606
|
}
|
|
596
607
|
};
|
|
@@ -64,6 +64,7 @@ export function createRoleRuntimeDependencies(packageRoot: string): RoleRuntimeD
|
|
|
64
64
|
authority: options.authority,
|
|
65
65
|
invocationId: options.invocationId,
|
|
66
66
|
createSession: navigatorSessionFactory,
|
|
67
|
+
...(options.deliveryRequestLimit === undefined ? {} : { deliveryRequestLimit: options.deliveryRequestLimit }),
|
|
67
68
|
...(options.contextError === undefined ? {} : { contextError: options.contextError }),
|
|
68
69
|
onEvent: options.onEvent,
|
|
69
70
|
}),
|