@akagilnc/pi-workflow-roles 0.1.2157 → 0.1.2173
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/public-cli/main.js +230 -61
- package/package.json +1 -1
- package/src/public-cli/auto-resume.ts +29 -35
- package/src/public-cli/cli.ts +57 -1
- package/src/public-cli/coder-run.ts +5 -1
- package/src/public-cli/collector-run.ts +1 -1
- package/src/public-cli/config.ts +53 -3
- package/src/public-cli/doctor-run.ts +1 -1
- package/src/public-cli/fixer-run.ts +5 -1
- package/src/public-cli/judge-run.ts +5 -1
- package/src/public-cli/merger-run.ts +5 -1
- package/src/public-cli/option-definitions.ts +2 -0
- package/src/public-cli/reviewer-run.ts +5 -1
- package/src/public-cli/run-lifecycle.ts +51 -2
- package/src/public-cli/settlement.ts +170 -15
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Single generic auto-resume loop for #416 (owner scope = single LLM call).
|
|
3
|
-
* Call-local retries, at most
|
|
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).
|
|
4
6
|
* Unifies presentation: intermediate attempts use dummyIo, only final Terminal is presented.
|
|
5
7
|
*/
|
|
6
8
|
import { AUTO_RESUME_LIMIT, isSessionPrincipalAvailable, acquireRunWriterLease, RunWriterLeaseHeldError, type RunWriterLease } from "./run-lifecycle.ts";
|
|
9
|
+
import { parseAutoResumeLimit } from "./config.ts";
|
|
7
10
|
import { isLawfulTypedTerminalOutcome, formatTerminalResult, type TerminalResult } from "./terminal.ts";
|
|
8
11
|
import { presentFailureTerminal, presentStructuralRejection } from "./settlement.ts";
|
|
9
12
|
import type { CliIo } from "./cli-io.ts";
|
|
10
13
|
|
|
11
14
|
const dummyIo: CliIo = { stdout: () => {}, stderr: () => {} };
|
|
12
15
|
|
|
16
|
+
function presentTerminal(terminal: TerminalResult, io: CliIo): void {
|
|
17
|
+
if (terminal.roleOutcome.kind === "failure" || terminal.roleOutcome.kind === "no_receipt") {
|
|
18
|
+
presentFailureTerminal(terminal, io);
|
|
19
|
+
} else {
|
|
20
|
+
io.stdout(formatTerminalResult(terminal));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
export type AutoResumeDispatchResult = {
|
|
14
25
|
exitCode: number;
|
|
15
26
|
terminal?: TerminalResult;
|
|
@@ -18,10 +29,21 @@ export type AutoResumeDispatchResult = {
|
|
|
18
29
|
export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(options: {
|
|
19
30
|
admitted: { sessionFile: string; runDirectory: string };
|
|
20
31
|
io: CliIo;
|
|
32
|
+
/**
|
|
33
|
+
* Effective ceiling (#422), resolved by the caller before the loop; never re-read
|
|
34
|
+
* per round. undefined = package default (AUTO_RESUME_LIMIT). Domain-validated at
|
|
35
|
+
* this single entry point (#422): NaN/negative/fractional/Infinity reject loudly
|
|
36
|
+
* before the first dispatch instead of silently bypassing the ceiling comparison.
|
|
37
|
+
*/
|
|
38
|
+
autoResumeLimit?: number | undefined;
|
|
21
39
|
buildInitialArgs: () => string[];
|
|
22
40
|
buildResumeArgs: () => string[];
|
|
23
41
|
dispatch: (extraArgs: string[], lease: RunWriterLease, isFirst: boolean, attemptIo: CliIo) => Promise<T>;
|
|
24
42
|
}): Promise<T> {
|
|
43
|
+
// #422 single-point resolution + domain validation. NaN would bypass every
|
|
44
|
+
// `attempts >= limit` comparison (always false) — reject here, before any dispatch.
|
|
45
|
+
const limit = options.autoResumeLimit ?? AUTO_RESUME_LIMIT;
|
|
46
|
+
parseAutoResumeLimit(limit);
|
|
25
47
|
let autoResumeAttempts = 0;
|
|
26
48
|
let isFirst = true;
|
|
27
49
|
let currentExtraArgs = options.buildInitialArgs();
|
|
@@ -29,7 +51,9 @@ export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(
|
|
|
29
51
|
while (true) {
|
|
30
52
|
let lease: RunWriterLease;
|
|
31
53
|
try {
|
|
32
|
-
lease = await acquireRunWriterLease(options.admitted.runDirectory)
|
|
54
|
+
lease = await acquireRunWriterLease(options.admitted.runDirectory, (diagnostic) =>
|
|
55
|
+
options.io.stderr(diagnostic),
|
|
56
|
+
);
|
|
33
57
|
} catch (error) {
|
|
34
58
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
35
59
|
presentStructuralRejection(error, options.io);
|
|
@@ -54,42 +78,12 @@ export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(
|
|
|
54
78
|
return result;
|
|
55
79
|
}
|
|
56
80
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (terminal.roleOutcome.kind === "audit_incomplete") {
|
|
60
|
-
options.io.stdout(formatTerminalResult(terminal));
|
|
61
|
-
} else {
|
|
62
|
-
options.io.stdout(formatTerminalResult(terminal));
|
|
63
|
-
}
|
|
64
|
-
return result;
|
|
65
|
-
}
|
|
66
|
-
// Rich auditor retention detail would be lost on retry (stale child binding)
|
|
67
|
-
if (terminal !== undefined && terminal.roleOutcome.kind === "failure") {
|
|
68
|
-
const terminalJson = JSON.stringify(terminal);
|
|
69
|
-
if (terminalJson.includes("retentionFailure") || terminalJson.includes("ComplianceResponseRetentionError")) {
|
|
70
|
-
presentFailureTerminal(terminal, options.io);
|
|
71
|
-
return result;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (autoResumeAttempts >= AUTO_RESUME_LIMIT) {
|
|
76
|
-
if (terminal !== undefined) {
|
|
77
|
-
if (terminal.roleOutcome.kind === "failure" || terminal.roleOutcome.kind === "no_receipt") {
|
|
78
|
-
presentFailureTerminal(terminal, options.io);
|
|
79
|
-
} else {
|
|
80
|
-
options.io.stdout(formatTerminalResult(terminal));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
81
|
+
if (autoResumeAttempts >= limit) {
|
|
82
|
+
if (terminal !== undefined) presentTerminal(terminal, options.io);
|
|
83
83
|
return result;
|
|
84
84
|
}
|
|
85
85
|
if (!(await isSessionPrincipalAvailable(options.admitted.sessionFile))) {
|
|
86
|
-
if (terminal !== undefined)
|
|
87
|
-
if (terminal.roleOutcome.kind === "failure" || terminal.roleOutcome.kind === "no_receipt") {
|
|
88
|
-
presentFailureTerminal(terminal, options.io);
|
|
89
|
-
} else {
|
|
90
|
-
options.io.stdout(formatTerminalResult(terminal));
|
|
91
|
-
}
|
|
92
|
-
}
|
|
86
|
+
if (terminal !== undefined) presentTerminal(terminal, options.io);
|
|
93
87
|
return result;
|
|
94
88
|
}
|
|
95
89
|
|
package/src/public-cli/cli.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
parseModelSpec,
|
|
18
18
|
resolveEffectiveSeat,
|
|
19
19
|
savePublicCliConfig,
|
|
20
|
+
setAutoResumeLimit,
|
|
20
21
|
setPersistentSeatConfig,
|
|
21
22
|
setPersistentSeatEngine,
|
|
22
23
|
validatePublicCliConfigEngines,
|
|
@@ -58,7 +59,7 @@ import { runPublicJudge, runPublicResume } from "./judge-run.ts";
|
|
|
58
59
|
import { runPublicMerger, runPublicMergerResume } from "./merger-run.ts";
|
|
59
60
|
import { runPublicReviewer, runPublicReviewerResume } from "./reviewer-run.ts";
|
|
60
61
|
import { runPublicTaishi } from "./taishi-run.ts";
|
|
61
|
-
import { peekRoleRunRole } from "./run-lifecycle.ts";
|
|
62
|
+
import { AUTO_RESUME_LIMIT, peekRoleRunRole } from "./run-lifecycle.ts";
|
|
62
63
|
import {
|
|
63
64
|
AUTOMATIC_NAVIGATOR_SEAT,
|
|
64
65
|
INTERNAL_ROLE_ENTRYPOINT_RELATIVE,
|
|
@@ -527,6 +528,8 @@ function renderConfig(config: PublicCliConfig): string {
|
|
|
527
528
|
lines.push(`${seat}\t${formatModelSpec(selection)}\t${engine}`);
|
|
528
529
|
}
|
|
529
530
|
}
|
|
531
|
+
// #422: show the effective auto-resume ceiling (configured value or default).
|
|
532
|
+
lines.push(`autoResumeLimit\t${config.autoResumeLimit ?? AUTO_RESUME_LIMIT}`);
|
|
530
533
|
return `${lines.join("\n")}\n`;
|
|
531
534
|
}
|
|
532
535
|
|
|
@@ -631,6 +634,39 @@ async function runConfigCommand(
|
|
|
631
634
|
return 0;
|
|
632
635
|
}
|
|
633
636
|
|
|
637
|
+
// #422: standalone verb per the set-engine/unset-engine precedent — the
|
|
638
|
+
// existing `config set` grammar stays strictly even-position seat/spec pairs.
|
|
639
|
+
if (args[0] === "set-auto-resume-limit") {
|
|
640
|
+
if (args.length !== 2) {
|
|
641
|
+
throw new CliUsageError(
|
|
642
|
+
"usage: ak-role config set-auto-resume-limit <N>",
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
const raw = args[1]!;
|
|
646
|
+
if (!/^[0-9]+$/.test(raw)) {
|
|
647
|
+
throw new CliUsageError(
|
|
648
|
+
`auto-resume limit must be a non-negative integer, got ${raw}`,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
// #422 fidelity boundary (not an upper bound — ADR 0035 stays intact): the
|
|
652
|
+
// persisted representation is a JS number, and Number() silently rounds
|
|
653
|
+
// integers beyond 2^53-1 (e.g. 9007199254740993 → 9007199254740992). Refuse
|
|
654
|
+
// loudly instead of persisting a different N; every exactly-representable
|
|
655
|
+
// non-negative integer remains legal. The regex above guarantees pure
|
|
656
|
+
// digits, so BigInt(raw) has no leading-zero ambiguity.
|
|
657
|
+
const converted = Number(raw);
|
|
658
|
+
if (!Number.isFinite(converted) || BigInt(converted) !== BigInt(raw)) {
|
|
659
|
+
throw new CliUsageError(
|
|
660
|
+
`auto-resume limit ${raw} is not exactly representable as a number; refusing to silently round the value`,
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
let config = await loadAndValidateConfig(home, packageRoot);
|
|
664
|
+
config = setAutoResumeLimit(config, converted);
|
|
665
|
+
await savePublicCliConfig(config, home);
|
|
666
|
+
io.stdout(renderConfig(config));
|
|
667
|
+
return 0;
|
|
668
|
+
}
|
|
669
|
+
|
|
634
670
|
throw new CliUsageError(`unknown config subcommand: ${args[0]}`);
|
|
635
671
|
}
|
|
636
672
|
|
|
@@ -922,6 +958,10 @@ export async function runAkRole(
|
|
|
922
958
|
? {}
|
|
923
959
|
: { timeoutMs: env.judgeTimeoutMs }),
|
|
924
960
|
...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
|
|
961
|
+
// #422: effective auto-resume ceiling resolved once here; the loop never re-reads disk.
|
|
962
|
+
...(config.autoResumeLimit === undefined
|
|
963
|
+
? {}
|
|
964
|
+
: { autoResumeLimit: config.autoResumeLimit }),
|
|
925
965
|
},
|
|
926
966
|
io,
|
|
927
967
|
PUBLIC_ROLE_ARGV.judge.parse,
|
|
@@ -966,6 +1006,10 @@ export async function runAkRole(
|
|
|
966
1006
|
? {}
|
|
967
1007
|
: { timeoutMs: env.coderTimeoutMs }),
|
|
968
1008
|
...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
|
|
1009
|
+
// #422: effective auto-resume ceiling resolved once here; the loop never re-reads disk.
|
|
1010
|
+
...(config.autoResumeLimit === undefined
|
|
1011
|
+
? {}
|
|
1012
|
+
: { autoResumeLimit: config.autoResumeLimit }),
|
|
969
1013
|
},
|
|
970
1014
|
io,
|
|
971
1015
|
PUBLIC_ROLE_ARGV.coder.parse,
|
|
@@ -1010,6 +1054,10 @@ export async function runAkRole(
|
|
|
1010
1054
|
? {}
|
|
1011
1055
|
: { timeoutMs: env.fixerTimeoutMs }),
|
|
1012
1056
|
...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
|
|
1057
|
+
// #422: effective auto-resume ceiling resolved once here; the loop never re-reads disk.
|
|
1058
|
+
...(config.autoResumeLimit === undefined
|
|
1059
|
+
? {}
|
|
1060
|
+
: { autoResumeLimit: config.autoResumeLimit }),
|
|
1013
1061
|
},
|
|
1014
1062
|
io,
|
|
1015
1063
|
PUBLIC_ROLE_ARGV.fixer.parse,
|
|
@@ -1098,6 +1146,10 @@ export async function runAkRole(
|
|
|
1098
1146
|
? {}
|
|
1099
1147
|
: { timeoutMs: env.reviewerTimeoutMs }),
|
|
1100
1148
|
...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
|
|
1149
|
+
// #422: effective auto-resume ceiling resolved once here; the loop never re-reads disk.
|
|
1150
|
+
...(config.autoResumeLimit === undefined
|
|
1151
|
+
? {}
|
|
1152
|
+
: { autoResumeLimit: config.autoResumeLimit }),
|
|
1101
1153
|
},
|
|
1102
1154
|
io,
|
|
1103
1155
|
PUBLIC_ROLE_ARGV.reviewer.parse,
|
|
@@ -1186,6 +1238,10 @@ export async function runAkRole(
|
|
|
1186
1238
|
? {}
|
|
1187
1239
|
: { timeoutMs: env.mergerTimeoutMs }),
|
|
1188
1240
|
...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
|
|
1241
|
+
// #422: effective auto-resume ceiling resolved once here; the loop never re-reads disk.
|
|
1242
|
+
...(config.autoResumeLimit === undefined
|
|
1243
|
+
? {}
|
|
1244
|
+
: { autoResumeLimit: config.autoResumeLimit }),
|
|
1189
1245
|
},
|
|
1190
1246
|
io,
|
|
1191
1247
|
PUBLIC_ROLE_ARGV.merger.parse,
|
|
@@ -86,6 +86,8 @@ export type CoderRunEnv = {
|
|
|
86
86
|
engine?: string;
|
|
87
87
|
credentials?: CredentialProviders;
|
|
88
88
|
createRunId?: () => string;
|
|
89
|
+
/** #422: effective single-call auto-resume ceiling; undefined = package default (AUTO_RESUME_LIMIT). */
|
|
90
|
+
autoResumeLimit?: number;
|
|
89
91
|
extraPiArgs?: readonly string[];
|
|
90
92
|
timeoutMs?: number;
|
|
91
93
|
};
|
|
@@ -475,6 +477,8 @@ export async function runPublicCoder(
|
|
|
475
477
|
return runWithAutoResumeLoop({
|
|
476
478
|
admitted,
|
|
477
479
|
io,
|
|
480
|
+
// #422: pass-through only; the loop entry resolves the default and validates the domain once.
|
|
481
|
+
autoResumeLimit: env.autoResumeLimit,
|
|
478
482
|
buildInitialArgs: () =>
|
|
479
483
|
buildCoderActivationExtraArgs(admitted, {
|
|
480
484
|
packageRoot: env.packageRoot,
|
|
@@ -548,7 +552,7 @@ export async function runPublicCoderResume(
|
|
|
548
552
|
|
|
549
553
|
let lease: RunWriterLease;
|
|
550
554
|
try {
|
|
551
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
555
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
552
556
|
} catch (error) {
|
|
553
557
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
554
558
|
// Concurrent resume: reject without second writer or dispatch.
|
|
@@ -355,7 +355,7 @@ export async function runPublicCollector(
|
|
|
355
355
|
|
|
356
356
|
let lease: RunWriterLease;
|
|
357
357
|
try {
|
|
358
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
358
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
359
359
|
} catch (error) {
|
|
360
360
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
361
361
|
presentStructuralRejection(error, io);
|
package/src/public-cli/config.ts
CHANGED
|
@@ -33,6 +33,13 @@ export type PersistentSeatConfig = SeatModelConfig & {
|
|
|
33
33
|
|
|
34
34
|
export type PublicCliConfig = {
|
|
35
35
|
seats: Partial<Record<PublicConfigurableSeat, PersistentSeatConfig>>;
|
|
36
|
+
/**
|
|
37
|
+
* #422: single-call auto-resume retry ceiling, sibling of `seats`.
|
|
38
|
+
* Non-negative integer; 0 disables auto-resume (one dispatch per call).
|
|
39
|
+
* undefined = package default (AUTO_RESUME_LIMIT). No package-local upper
|
|
40
|
+
* bound (ADR 0035).
|
|
41
|
+
*/
|
|
42
|
+
autoResumeLimit?: number;
|
|
36
43
|
};
|
|
37
44
|
|
|
38
45
|
export type EffectiveSource = "persistent" | "startup" | "invocation" | "unconfigured";
|
|
@@ -108,6 +115,9 @@ export function setPersistentSeatConfig(
|
|
|
108
115
|
): PublicCliConfig {
|
|
109
116
|
const previous = config.seats[seat];
|
|
110
117
|
return {
|
|
118
|
+
// Spread preserves sibling top-level keys such as autoResumeLimit (#422):
|
|
119
|
+
// a seat write must never silently drop them.
|
|
120
|
+
...config,
|
|
111
121
|
seats: {
|
|
112
122
|
...config.seats,
|
|
113
123
|
[seat]: {
|
|
@@ -146,6 +156,7 @@ export function setPersistentSeatEngine(
|
|
|
146
156
|
if (engine === undefined) {
|
|
147
157
|
const { engine: _dropped, ...modelOnly } = previous;
|
|
148
158
|
return {
|
|
159
|
+
...config,
|
|
149
160
|
seats: {
|
|
150
161
|
...config.seats,
|
|
151
162
|
[seat]: modelOnly,
|
|
@@ -155,6 +166,7 @@ export function setPersistentSeatEngine(
|
|
|
155
166
|
// Engine-name path-safety syntax is owned solely by assertLegalEngineName
|
|
156
167
|
// (call-request + config-parse seams). Setter is pure seat mutation.
|
|
157
168
|
return {
|
|
169
|
+
...config,
|
|
158
170
|
seats: {
|
|
159
171
|
...config.seats,
|
|
160
172
|
[seat]: { ...previous, engine },
|
|
@@ -162,6 +174,32 @@ export function setPersistentSeatEngine(
|
|
|
162
174
|
};
|
|
163
175
|
}
|
|
164
176
|
|
|
177
|
+
/**
|
|
178
|
+
* #422 value domain authority for the auto-resume ceiling: non-negative integer,
|
|
179
|
+
* no package-local upper bound (ADR 0035). `0` is legal and means auto-resume is
|
|
180
|
+
* disabled (a single dispatch, no in-place retry). Negative numbers, fractions,
|
|
181
|
+
* NaN, Infinity and non-number types are rejected loudly — never silently coerced.
|
|
182
|
+
*/
|
|
183
|
+
export function parseAutoResumeLimit(value: unknown): number {
|
|
184
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`auto-resume limit must be a non-negative integer, got ${JSON.stringify(value)}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* #422: set the persistent autoResumeLimit top-level key. Pure mutation that
|
|
194
|
+
* preserves all sibling keys (seats included).
|
|
195
|
+
*/
|
|
196
|
+
export function setAutoResumeLimit(
|
|
197
|
+
config: PublicCliConfig,
|
|
198
|
+
limit: number,
|
|
199
|
+
): PublicCliConfig {
|
|
200
|
+
return { ...config, autoResumeLimit: parseAutoResumeLimit(limit) };
|
|
201
|
+
}
|
|
202
|
+
|
|
165
203
|
/** Strip optional engine so activation model argv never sees the engine axis. */
|
|
166
204
|
export function seatModelOnly(seat: PersistentSeatConfig): SeatModelConfig {
|
|
167
205
|
return seat.thinking === undefined
|
|
@@ -261,9 +299,18 @@ function parsePublicCliConfig(value: unknown): PublicCliConfig {
|
|
|
261
299
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
262
300
|
throw new Error("public CLI config must be an object");
|
|
263
301
|
}
|
|
264
|
-
const record = value as { seats?: unknown };
|
|
302
|
+
const record = value as { seats?: unknown; autoResumeLimit?: unknown };
|
|
303
|
+
// #422 round-trip preservation: the sibling top-level key must survive every
|
|
304
|
+
// parse→save cycle; an unknown-key drop would silently erase it on any write.
|
|
305
|
+
let autoResumeLimit: number | undefined;
|
|
306
|
+
if (record.autoResumeLimit !== undefined) {
|
|
307
|
+
autoResumeLimit = parseAutoResumeLimit(record.autoResumeLimit);
|
|
308
|
+
}
|
|
265
309
|
if (record.seats === undefined) {
|
|
266
|
-
return {
|
|
310
|
+
return {
|
|
311
|
+
seats: {},
|
|
312
|
+
...(autoResumeLimit === undefined ? {} : { autoResumeLimit }),
|
|
313
|
+
};
|
|
267
314
|
}
|
|
268
315
|
if (
|
|
269
316
|
record.seats === null ||
|
|
@@ -281,7 +328,10 @@ function parsePublicCliConfig(value: unknown): PublicCliConfig {
|
|
|
281
328
|
}
|
|
282
329
|
seats[key as PublicConfigurableSeat] = parseSeatModelConfig(raw, key);
|
|
283
330
|
}
|
|
284
|
-
return {
|
|
331
|
+
return {
|
|
332
|
+
seats,
|
|
333
|
+
...(autoResumeLimit === undefined ? {} : { autoResumeLimit }),
|
|
334
|
+
};
|
|
285
335
|
}
|
|
286
336
|
|
|
287
337
|
function parseSeatModelConfig(value: unknown, seat: string): PersistentSeatConfig {
|
|
@@ -334,7 +334,7 @@ export async function runPublicDoctor(
|
|
|
334
334
|
|
|
335
335
|
let lease: RunWriterLease;
|
|
336
336
|
try {
|
|
337
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
337
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
338
338
|
} catch (error) {
|
|
339
339
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
340
340
|
presentStructuralRejection(error, io);
|
|
@@ -89,6 +89,8 @@ export type FixerRunEnv = {
|
|
|
89
89
|
engine?: string;
|
|
90
90
|
credentials?: CredentialProviders;
|
|
91
91
|
createRunId?: () => string;
|
|
92
|
+
/** #422: effective single-call auto-resume ceiling; undefined = package default (AUTO_RESUME_LIMIT). */
|
|
93
|
+
autoResumeLimit?: number;
|
|
92
94
|
extraPiArgs?: readonly string[];
|
|
93
95
|
timeoutMs?: number;
|
|
94
96
|
};
|
|
@@ -494,6 +496,8 @@ export async function runPublicFixer(
|
|
|
494
496
|
return runWithAutoResumeLoop({
|
|
495
497
|
admitted,
|
|
496
498
|
io,
|
|
499
|
+
// #422: pass-through only; the loop entry resolves the default and validates the domain once.
|
|
500
|
+
autoResumeLimit: env.autoResumeLimit,
|
|
497
501
|
buildInitialArgs: () =>
|
|
498
502
|
buildFixerActivationExtraArgs(admitted, {
|
|
499
503
|
packageRoot: env.packageRoot,
|
|
@@ -567,7 +571,7 @@ export async function runPublicFixerResume(
|
|
|
567
571
|
|
|
568
572
|
let lease: RunWriterLease;
|
|
569
573
|
try {
|
|
570
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
574
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
571
575
|
} catch (error) {
|
|
572
576
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
573
577
|
io.stderr(formatCliDiagnostic(error.message));
|
|
@@ -89,6 +89,8 @@ export type JudgeRunEnv = {
|
|
|
89
89
|
*/
|
|
90
90
|
credentials?: CredentialProviders;
|
|
91
91
|
createRunId?: () => string;
|
|
92
|
+
/** #422: effective single-call auto-resume ceiling; undefined = package default (AUTO_RESUME_LIMIT). */
|
|
93
|
+
autoResumeLimit?: number;
|
|
92
94
|
/** Extra Pi args inserted before the prompt (tests: faux provider extension). */
|
|
93
95
|
extraPiArgs?: readonly string[];
|
|
94
96
|
/** Override default role-run timeout. */
|
|
@@ -463,6 +465,8 @@ export async function runPublicJudge(
|
|
|
463
465
|
return runWithAutoResumeLoop({
|
|
464
466
|
admitted,
|
|
465
467
|
io,
|
|
468
|
+
// #422: pass-through only; the loop entry resolves the default and validates the domain once.
|
|
469
|
+
autoResumeLimit: env.autoResumeLimit,
|
|
466
470
|
buildInitialArgs: () =>
|
|
467
471
|
buildJudgeActivationExtraArgs(admitted, {
|
|
468
472
|
packageRoot: env.packageRoot,
|
|
@@ -536,7 +540,7 @@ export async function runPublicResume(
|
|
|
536
540
|
|
|
537
541
|
let lease: RunWriterLease;
|
|
538
542
|
try {
|
|
539
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
543
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
540
544
|
} catch (error) {
|
|
541
545
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
542
546
|
// Concurrent resume: reject without second writer or dispatch.
|
|
@@ -90,6 +90,8 @@ export type MergerRunEnv = {
|
|
|
90
90
|
engine?: string;
|
|
91
91
|
credentials?: CredentialProviders;
|
|
92
92
|
createRunId?: () => string;
|
|
93
|
+
/** #422: effective single-call auto-resume ceiling; undefined = package default (AUTO_RESUME_LIMIT). */
|
|
94
|
+
autoResumeLimit?: number;
|
|
93
95
|
extraPiArgs?: readonly string[];
|
|
94
96
|
timeoutMs?: number;
|
|
95
97
|
};
|
|
@@ -575,6 +577,8 @@ export async function runPublicMerger(
|
|
|
575
577
|
return runWithAutoResumeLoop({
|
|
576
578
|
admitted,
|
|
577
579
|
io,
|
|
580
|
+
// #422: pass-through only; the loop entry resolves the default and validates the domain once.
|
|
581
|
+
autoResumeLimit: env.autoResumeLimit,
|
|
578
582
|
buildInitialArgs: () =>
|
|
579
583
|
buildMergerActivationExtraArgs(admitted, {
|
|
580
584
|
packageRoot: env.packageRoot,
|
|
@@ -648,7 +652,7 @@ export async function runPublicMergerResume(
|
|
|
648
652
|
|
|
649
653
|
let lease: RunWriterLease;
|
|
650
654
|
try {
|
|
651
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
655
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
652
656
|
} catch (error) {
|
|
653
657
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
654
658
|
io.stderr(formatCliDiagnostic(error.message));
|
|
@@ -1098,10 +1098,12 @@ const SUPPORT_COMMAND_HELP = {
|
|
|
1098
1098
|
"ak-role config set <seat> <provider/model[:thinking]> [<seat> <spec> ...]",
|
|
1099
1099
|
"ak-role config set-engine <seat> <name>",
|
|
1100
1100
|
"ak-role config unset-engine <seat>",
|
|
1101
|
+
"ak-role config set-auto-resume-limit <N>",
|
|
1101
1102
|
],
|
|
1102
1103
|
examples: [
|
|
1103
1104
|
"ak-role config set judge openai-codex/gpt-5.6-sol:high",
|
|
1104
1105
|
"ak-role config set-engine judge opus",
|
|
1106
|
+
"ak-role config set-auto-resume-limit 3",
|
|
1105
1107
|
],
|
|
1106
1108
|
},
|
|
1107
1109
|
help: {
|
|
@@ -91,6 +91,8 @@ export type ReviewerRunEnv = {
|
|
|
91
91
|
engine?: string;
|
|
92
92
|
credentials?: CredentialProviders;
|
|
93
93
|
createRunId?: () => string;
|
|
94
|
+
/** #422: effective single-call auto-resume ceiling; undefined = package default (AUTO_RESUME_LIMIT). */
|
|
95
|
+
autoResumeLimit?: number;
|
|
94
96
|
extraPiArgs?: readonly string[];
|
|
95
97
|
timeoutMs?: number;
|
|
96
98
|
};
|
|
@@ -514,6 +516,8 @@ export async function runPublicReviewer(
|
|
|
514
516
|
return runWithAutoResumeLoop({
|
|
515
517
|
admitted,
|
|
516
518
|
io,
|
|
519
|
+
// #422: pass-through only; the loop entry resolves the default and validates the domain once.
|
|
520
|
+
autoResumeLimit: env.autoResumeLimit,
|
|
517
521
|
buildInitialArgs: () =>
|
|
518
522
|
buildReviewerActivationExtraArgs(admitted, {
|
|
519
523
|
packageRoot: env.packageRoot,
|
|
@@ -587,7 +591,7 @@ export async function runPublicReviewerResume(
|
|
|
587
591
|
|
|
588
592
|
let lease: RunWriterLease;
|
|
589
593
|
try {
|
|
590
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
594
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
591
595
|
} catch (error) {
|
|
592
596
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
593
597
|
io.stderr(formatCliDiagnostic(error.message));
|
|
@@ -52,7 +52,10 @@ export type TypedHttp429Observation = {
|
|
|
52
52
|
readonly provider: V1ResumableProvider;
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
/**
|
|
55
|
+
/** Default for the #422 configurable single-call auto-resume ceiling
|
|
56
|
+
* (public-cli.json top-level `autoResumeLimit`). No longer the runtime truth
|
|
57
|
+
* source: runWithAutoResumeLoop receives the effective value once per call.
|
|
58
|
+
*/
|
|
56
59
|
export const AUTO_RESUME_LIMIT = 2 as const;
|
|
57
60
|
|
|
58
61
|
export type RoleRunRecord = {
|
|
@@ -340,13 +343,52 @@ export type RunWriterLease = {
|
|
|
340
343
|
release(): Promise<void>;
|
|
341
344
|
};
|
|
342
345
|
|
|
346
|
+
/**
|
|
347
|
+
* True error identity for diagnostics — name/code/message as-is, never a
|
|
348
|
+
* guessed label (failure-honesty constitution).
|
|
349
|
+
*/
|
|
350
|
+
function describeErrorIdentity(error: unknown): string {
|
|
351
|
+
const candidate = error as { name?: unknown; code?: unknown; message?: unknown };
|
|
352
|
+
const name =
|
|
353
|
+
typeof candidate?.name === "string" && candidate.name !== ""
|
|
354
|
+
? candidate.name
|
|
355
|
+
: typeof error;
|
|
356
|
+
const code =
|
|
357
|
+
typeof candidate?.code === "string" || typeof candidate?.code === "number"
|
|
358
|
+
? ` code=${String(candidate.code)}`
|
|
359
|
+
: "";
|
|
360
|
+
const message =
|
|
361
|
+
typeof candidate?.message === "string" && candidate.message !== ""
|
|
362
|
+
? `: ${candidate.message}`
|
|
363
|
+
: "";
|
|
364
|
+
return `${name}${code}${message}`;
|
|
365
|
+
}
|
|
366
|
+
|
|
343
367
|
/**
|
|
344
368
|
* Acquire the one-writer lease for a Role run. Concurrent acquire rejects
|
|
345
369
|
* without dispatch. Exclusive create — no second writer.
|
|
370
|
+
*
|
|
371
|
+
* `onCleanupFailure` receives a non-terminal diagnostic line when release-time
|
|
372
|
+
* lock cleanup fails. Release stays best-effort (a stale lock resurfaces as
|
|
373
|
+
* RunWriterLeaseHeldError on next acquire), but the true error identity must
|
|
374
|
+
* still land somewhere observable — silent swallowing is forbidden.
|
|
346
375
|
*/
|
|
347
376
|
export async function acquireRunWriterLease(
|
|
348
377
|
runDirectory: string,
|
|
378
|
+
onCleanupFailure?: (diagnostic: string) => void,
|
|
349
379
|
): Promise<RunWriterLease> {
|
|
380
|
+
const reportCleanupFailure = (error: unknown): void => {
|
|
381
|
+
// Sink isolation: a throwing onCleanupFailure must not propagate through
|
|
382
|
+
// release() — release stays best-effort by contract. The true cleanup
|
|
383
|
+
// cause has already been handed to the sink as its argument.
|
|
384
|
+
try {
|
|
385
|
+
onCleanupFailure?.(
|
|
386
|
+
`writer lease lock cleanup failed (best-effort continue; stale lock resurfaces as lease-held on next acquire) at ${join(runDirectory, WRITER_LOCK_FILE)}: ${describeErrorIdentity(error)}`,
|
|
387
|
+
);
|
|
388
|
+
} catch {
|
|
389
|
+
// diagnostic-sink failure is itself best-effort; never break release().
|
|
390
|
+
}
|
|
391
|
+
};
|
|
350
392
|
const lockPath = join(runDirectory, WRITER_LOCK_FILE);
|
|
351
393
|
try {
|
|
352
394
|
const handle = await open(lockPath, "wx");
|
|
@@ -371,7 +413,14 @@ export async function acquireRunWriterLease(
|
|
|
371
413
|
try {
|
|
372
414
|
await chmod(runDirectory, 0o755);
|
|
373
415
|
await unlink(lockPath);
|
|
374
|
-
} catch {
|
|
416
|
+
} catch (retryError) {
|
|
417
|
+
// best-effort cleanup: stale lock will surface as lease-held on next acquire (exit 2),
|
|
418
|
+
// but the true chmod/unlink cause must be recorded, not swallowed.
|
|
419
|
+
reportCleanupFailure(retryError);
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
// non-EACCES unlink failure is best-effort settlement cleanup; record true cause.
|
|
423
|
+
reportCleanupFailure(error);
|
|
375
424
|
}
|
|
376
425
|
}
|
|
377
426
|
},
|