@akagilnc/pi-workflow-roles 0.1.2175 → 0.1.2181
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 +350 -89
- package/package.json +1 -1
- package/src/activation-ledger.ts +11 -0
- package/src/public-cli/auto-resume.ts +406 -21
- package/src/public-cli/run-lifecycle.ts +1 -1
package/dist/public-cli/main.js
CHANGED
|
@@ -22627,6 +22627,10 @@ var init_settlement = __esm({
|
|
|
22627
22627
|
});
|
|
22628
22628
|
|
|
22629
22629
|
// src/public-cli/auto-resume.ts
|
|
22630
|
+
import { constants as fsConstants2 } from "node:fs";
|
|
22631
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
22632
|
+
import { appendFile as appendFile2, lstat as lstat4, mkdir as mkdir4, open as open3, readFile as readFile10 } from "node:fs/promises";
|
|
22633
|
+
import { join as join12 } from "node:path";
|
|
22630
22634
|
function presentTerminal(terminal, io) {
|
|
22631
22635
|
if (terminal.roleOutcome.kind === "failure" || terminal.roleOutcome.kind === "no_receipt") {
|
|
22632
22636
|
presentFailureTerminal(terminal, io);
|
|
@@ -22634,12 +22638,204 @@ function presentTerminal(terminal, io) {
|
|
|
22634
22638
|
io.stdout(formatTerminalResult(terminal));
|
|
22635
22639
|
}
|
|
22636
22640
|
}
|
|
22641
|
+
async function finalizeExceptionRunBestEffort(runDirectory, io) {
|
|
22642
|
+
try {
|
|
22643
|
+
await markRunTerminal(runDirectory);
|
|
22644
|
+
} catch (error) {
|
|
22645
|
+
io.stderr(
|
|
22646
|
+
`run terminal-state finalization failed (best-effort continue): ${describeErrorIdentity(error)}
|
|
22647
|
+
`
|
|
22648
|
+
);
|
|
22649
|
+
}
|
|
22650
|
+
}
|
|
22651
|
+
function runArtifactsDirectory(runDirectory) {
|
|
22652
|
+
return join12(runDirectory, "artifacts");
|
|
22653
|
+
}
|
|
22654
|
+
async function ensureRealArtifactsDirectory(runDirectory) {
|
|
22655
|
+
const runStat = await lstat4(runDirectory);
|
|
22656
|
+
if (runStat.isSymbolicLink() || !runStat.isDirectory()) {
|
|
22657
|
+
throw new Error("dispatch error retention: run directory is not a real directory");
|
|
22658
|
+
}
|
|
22659
|
+
const artifactsDir = runArtifactsDirectory(runDirectory);
|
|
22660
|
+
try {
|
|
22661
|
+
const existing = await lstat4(artifactsDir);
|
|
22662
|
+
if (existing.isSymbolicLink() || !existing.isDirectory()) {
|
|
22663
|
+
throw new Error("dispatch error retention: artifacts path is not a real directory");
|
|
22664
|
+
}
|
|
22665
|
+
} catch (error) {
|
|
22666
|
+
if (!isMissingPathError3(error)) throw error;
|
|
22667
|
+
await mkdir4(artifactsDir, { recursive: true });
|
|
22668
|
+
const created = await lstat4(artifactsDir);
|
|
22669
|
+
if (created.isSymbolicLink() || !created.isDirectory()) {
|
|
22670
|
+
throw new Error("dispatch error retention: artifacts directory is not a real directory");
|
|
22671
|
+
}
|
|
22672
|
+
}
|
|
22673
|
+
return artifactsDir;
|
|
22674
|
+
}
|
|
22675
|
+
function serializeThrownValue(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
22676
|
+
if (value instanceof Error) {
|
|
22677
|
+
if (seen.has(value)) return "[circular]";
|
|
22678
|
+
seen.add(value);
|
|
22679
|
+
const transferred = {};
|
|
22680
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
22681
|
+
transferred[key] = transferNestedValue(
|
|
22682
|
+
value[key],
|
|
22683
|
+
depth + 1,
|
|
22684
|
+
seen
|
|
22685
|
+
);
|
|
22686
|
+
}
|
|
22687
|
+
return {
|
|
22688
|
+
errorKind: "Error",
|
|
22689
|
+
constructorName: value.constructor?.name,
|
|
22690
|
+
...transferred,
|
|
22691
|
+
...value.cause === void 0 ? {} : {
|
|
22692
|
+
causeChain: depth >= 10 ? "[cause-chain-depth-limit]" : serializeThrownValue(value.cause, depth + 1, seen)
|
|
22693
|
+
}
|
|
22694
|
+
};
|
|
22695
|
+
}
|
|
22696
|
+
return value;
|
|
22697
|
+
}
|
|
22698
|
+
function isMissingPathError3(error) {
|
|
22699
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
22700
|
+
}
|
|
22701
|
+
function transferNestedValue(value, depth, seen) {
|
|
22702
|
+
if (value instanceof Error) return serializeThrownValue(value, depth, seen);
|
|
22703
|
+
if (depth >= 10) return "[nested-depth-limit]";
|
|
22704
|
+
if (Array.isArray(value)) {
|
|
22705
|
+
return value.map((item) => transferNestedValue(item, depth + 1, seen));
|
|
22706
|
+
}
|
|
22707
|
+
if (value !== null && typeof value === "object") {
|
|
22708
|
+
if (seen.has(value)) return "[circular]";
|
|
22709
|
+
seen.add(value);
|
|
22710
|
+
const transferred = {};
|
|
22711
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
22712
|
+
transferred[key] = transferNestedValue(
|
|
22713
|
+
value[key],
|
|
22714
|
+
depth + 1,
|
|
22715
|
+
seen
|
|
22716
|
+
);
|
|
22717
|
+
}
|
|
22718
|
+
return transferred;
|
|
22719
|
+
}
|
|
22720
|
+
return value;
|
|
22721
|
+
}
|
|
22722
|
+
function jsonSafeReplacer() {
|
|
22723
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
22724
|
+
return (_key, value) => {
|
|
22725
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
22726
|
+
if (typeof value === "object" && value !== null) {
|
|
22727
|
+
if (seen.has(value)) return "[circular]";
|
|
22728
|
+
seen.add(value);
|
|
22729
|
+
}
|
|
22730
|
+
return value;
|
|
22731
|
+
};
|
|
22732
|
+
}
|
|
22733
|
+
async function retainDispatchError(admitted, attempt, error) {
|
|
22734
|
+
const artifactsDir = await ensureRealArtifactsDirectory(admitted.runDirectory);
|
|
22735
|
+
const filePath = join12(
|
|
22736
|
+
artifactsDir,
|
|
22737
|
+
`dispatch-error-attempt-${attempt}-${randomUUID2()}.json`
|
|
22738
|
+
);
|
|
22739
|
+
const payload = `${JSON.stringify(
|
|
22740
|
+
{
|
|
22741
|
+
version: 1,
|
|
22742
|
+
attempt,
|
|
22743
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22744
|
+
error: serializeThrownValue(error)
|
|
22745
|
+
},
|
|
22746
|
+
jsonSafeReplacer(),
|
|
22747
|
+
2
|
|
22748
|
+
)}
|
|
22749
|
+
`;
|
|
22750
|
+
const noFollowFlag = typeof fsConstants2.O_NOFOLLOW === "number" ? fsConstants2.O_NOFOLLOW : 0;
|
|
22751
|
+
const handle = await open3(
|
|
22752
|
+
filePath,
|
|
22753
|
+
fsConstants2.O_WRONLY | fsConstants2.O_CREAT | fsConstants2.O_EXCL | noFollowFlag,
|
|
22754
|
+
384
|
|
22755
|
+
);
|
|
22756
|
+
try {
|
|
22757
|
+
await handle.writeFile(payload, "utf8");
|
|
22758
|
+
} finally {
|
|
22759
|
+
await handle.close();
|
|
22760
|
+
}
|
|
22761
|
+
let pointerLease;
|
|
22762
|
+
try {
|
|
22763
|
+
pointerLease = await acquireRunWriterLease(admitted.runDirectory);
|
|
22764
|
+
} catch (error2) {
|
|
22765
|
+
if (error2 instanceof RunWriterLeaseHeldError) return { file: filePath };
|
|
22766
|
+
throw error2;
|
|
22767
|
+
}
|
|
22768
|
+
let pointerError;
|
|
22769
|
+
try {
|
|
22770
|
+
const text = await readFile10(admitted.sessionFile, "utf8");
|
|
22771
|
+
let parentId = null;
|
|
22772
|
+
for (const line2 of text.trim().split("\n").filter(Boolean)) {
|
|
22773
|
+
const entry = JSON.parse(line2);
|
|
22774
|
+
if (typeof entry.id === "string" && entry.type !== "session") parentId = entry.id;
|
|
22775
|
+
}
|
|
22776
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
22777
|
+
const pointerLine = `${JSON.stringify({
|
|
22778
|
+
type: "custom",
|
|
22779
|
+
customType: DISPATCH_ERROR_RETENTION_ENTRY_TYPE,
|
|
22780
|
+
data: { version: 1, attempt, file: filePath, recordedAt: timestamp2 },
|
|
22781
|
+
id: randomUUID2(),
|
|
22782
|
+
parentId,
|
|
22783
|
+
timestamp: timestamp2
|
|
22784
|
+
})}
|
|
22785
|
+
`;
|
|
22786
|
+
await appendFile2(admitted.sessionFile, pointerLine, "utf8");
|
|
22787
|
+
} catch (error2) {
|
|
22788
|
+
pointerError = error2;
|
|
22789
|
+
} finally {
|
|
22790
|
+
await pointerLease.release();
|
|
22791
|
+
}
|
|
22792
|
+
return pointerError === void 0 ? { file: filePath } : { file: filePath, pointerError };
|
|
22793
|
+
}
|
|
22794
|
+
function dispatchExceptionFailureTerminal(input) {
|
|
22795
|
+
const history = input.everyAttemptThrew ? "dispatch threw an exception on every attempt" : "the final dispatch threw an exception";
|
|
22796
|
+
const diagnostic = `${history} (${input.endReason}; resumes used ${input.autoResumeAttempts}); last cause: ${describeErrorIdentity(input.causeError)}`;
|
|
22797
|
+
const decisiveFacts = {
|
|
22798
|
+
cause: "unrecognized",
|
|
22799
|
+
diagnostic,
|
|
22800
|
+
resumesUsed: input.autoResumeAttempts,
|
|
22801
|
+
dispatchErrorFiles: [...input.errorFiles]
|
|
22802
|
+
};
|
|
22803
|
+
if (input.errorFiles.length > 0) {
|
|
22804
|
+
decisiveFacts.lastDispatchErrorFile = input.errorFiles[input.errorFiles.length - 1];
|
|
22805
|
+
}
|
|
22806
|
+
const candidate = input.causeError;
|
|
22807
|
+
if (typeof candidate?.name === "string") decisiveFacts.errorName = candidate.name;
|
|
22808
|
+
if (typeof candidate?.code === "string" || typeof candidate?.code === "number") {
|
|
22809
|
+
decisiveFacts.errorCode = candidate.code;
|
|
22810
|
+
}
|
|
22811
|
+
const artifacts = input.errorFiles.map((path) => ({
|
|
22812
|
+
kind: "error",
|
|
22813
|
+
path
|
|
22814
|
+
}));
|
|
22815
|
+
return {
|
|
22816
|
+
roleOutcome: {
|
|
22817
|
+
kind: "failure",
|
|
22818
|
+
role: input.role,
|
|
22819
|
+
cause: "unrecognized",
|
|
22820
|
+
diagnostic,
|
|
22821
|
+
decisiveFacts
|
|
22822
|
+
},
|
|
22823
|
+
navigator: { disposition: "no-advice" },
|
|
22824
|
+
artifacts,
|
|
22825
|
+
runId: input.runId,
|
|
22826
|
+
autoResumeCount: input.autoResumeAttempts
|
|
22827
|
+
};
|
|
22828
|
+
}
|
|
22637
22829
|
async function runWithAutoResumeLoop(options) {
|
|
22638
22830
|
const limit = options.autoResumeLimit ?? AUTO_RESUME_LIMIT;
|
|
22639
22831
|
parseAutoResumeLimit(limit);
|
|
22640
22832
|
let autoResumeAttempts = 0;
|
|
22641
22833
|
let isFirst = true;
|
|
22642
22834
|
let currentExtraArgs = options.buildInitialArgs();
|
|
22835
|
+
let dispatchOrdinal = 0;
|
|
22836
|
+
let lastThrownError;
|
|
22837
|
+
let everyAttemptThrew = true;
|
|
22838
|
+
const retainedErrorFiles = [];
|
|
22643
22839
|
while (true) {
|
|
22644
22840
|
let lease;
|
|
22645
22841
|
try {
|
|
@@ -22654,32 +22850,96 @@ async function runWithAutoResumeLoop(options) {
|
|
|
22654
22850
|
}
|
|
22655
22851
|
throw error;
|
|
22656
22852
|
}
|
|
22657
|
-
|
|
22658
|
-
|
|
22659
|
-
|
|
22660
|
-
|
|
22853
|
+
let result2;
|
|
22854
|
+
try {
|
|
22855
|
+
result2 = await options.dispatch(currentExtraArgs, lease, isFirst, dummyIo);
|
|
22856
|
+
} catch (error) {
|
|
22857
|
+
lastThrownError = error;
|
|
22858
|
+
const attempt = dispatchOrdinal;
|
|
22859
|
+
try {
|
|
22860
|
+
const { file, pointerError } = await retainDispatchError(options.admitted, attempt, error);
|
|
22861
|
+
retainedErrorFiles.push(file);
|
|
22862
|
+
options.io.stderr(
|
|
22863
|
+
`dispatch attempt ${attempt} threw (${describeErrorIdentity(error)}); full error retained at ${file}
|
|
22864
|
+
`
|
|
22865
|
+
);
|
|
22866
|
+
if (pointerError !== void 0) {
|
|
22867
|
+
options.io.stderr(
|
|
22868
|
+
`dispatch error retention failed (best-effort continue): ${describeErrorIdentity(pointerError)}
|
|
22869
|
+
`
|
|
22870
|
+
);
|
|
22871
|
+
}
|
|
22872
|
+
} catch (retentionError) {
|
|
22873
|
+
options.io.stderr(
|
|
22874
|
+
`dispatch error retention failed (best-effort continue): ${describeErrorIdentity(retentionError)}
|
|
22875
|
+
`
|
|
22876
|
+
);
|
|
22877
|
+
}
|
|
22661
22878
|
}
|
|
22662
|
-
|
|
22663
|
-
if (
|
|
22879
|
+
dispatchOrdinal += 1;
|
|
22880
|
+
if (result2 !== void 0) {
|
|
22881
|
+
everyAttemptThrew = false;
|
|
22882
|
+
const terminal = result2.terminal;
|
|
22664
22883
|
if (terminal !== void 0) {
|
|
22665
|
-
|
|
22884
|
+
terminal.autoResumeCount = autoResumeAttempts;
|
|
22885
|
+
}
|
|
22886
|
+
const lawful = terminal !== void 0 && isLawfulTypedTerminalOutcome(terminal.roleOutcome);
|
|
22887
|
+
if (lawful) {
|
|
22888
|
+
if (terminal !== void 0) {
|
|
22889
|
+
options.io.stdout(formatTerminalResult(terminal));
|
|
22890
|
+
}
|
|
22891
|
+
return result2;
|
|
22892
|
+
}
|
|
22893
|
+
if (autoResumeAttempts >= limit) {
|
|
22894
|
+
if (terminal !== void 0) presentTerminal(terminal, options.io);
|
|
22895
|
+
return result2;
|
|
22896
|
+
}
|
|
22897
|
+
if (!await isSessionPrincipalAvailable(options.admitted.sessionFile)) {
|
|
22898
|
+
if (terminal !== void 0) presentTerminal(terminal, options.io);
|
|
22899
|
+
return result2;
|
|
22900
|
+
}
|
|
22901
|
+
} else {
|
|
22902
|
+
if (autoResumeAttempts >= limit) {
|
|
22903
|
+
const terminal = dispatchExceptionFailureTerminal({
|
|
22904
|
+
role: options.admitted.role,
|
|
22905
|
+
runId: options.admitted.runId,
|
|
22906
|
+
causeError: lastThrownError,
|
|
22907
|
+
errorFiles: retainedErrorFiles,
|
|
22908
|
+
autoResumeAttempts,
|
|
22909
|
+
endReason: "auto-resume budget exhausted",
|
|
22910
|
+
everyAttemptThrew
|
|
22911
|
+
});
|
|
22912
|
+
await finalizeExceptionRunBestEffort(options.admitted.runDirectory, options.io);
|
|
22913
|
+
presentTerminal(terminal, options.io);
|
|
22914
|
+
return {
|
|
22915
|
+
exitCode: 1,
|
|
22916
|
+
terminal
|
|
22917
|
+
};
|
|
22918
|
+
}
|
|
22919
|
+
if (!await isSessionPrincipalAvailable(options.admitted.sessionFile)) {
|
|
22920
|
+
const terminal = dispatchExceptionFailureTerminal({
|
|
22921
|
+
role: options.admitted.role,
|
|
22922
|
+
runId: options.admitted.runId,
|
|
22923
|
+
causeError: lastThrownError,
|
|
22924
|
+
errorFiles: retainedErrorFiles,
|
|
22925
|
+
autoResumeAttempts,
|
|
22926
|
+
endReason: "session principal unavailable before further resume",
|
|
22927
|
+
everyAttemptThrew
|
|
22928
|
+
});
|
|
22929
|
+
await finalizeExceptionRunBestEffort(options.admitted.runDirectory, options.io);
|
|
22930
|
+
presentTerminal(terminal, options.io);
|
|
22931
|
+
return {
|
|
22932
|
+
exitCode: 1,
|
|
22933
|
+
terminal
|
|
22934
|
+
};
|
|
22666
22935
|
}
|
|
22667
|
-
return result2;
|
|
22668
|
-
}
|
|
22669
|
-
if (autoResumeAttempts >= limit) {
|
|
22670
|
-
if (terminal !== void 0) presentTerminal(terminal, options.io);
|
|
22671
|
-
return result2;
|
|
22672
|
-
}
|
|
22673
|
-
if (!await isSessionPrincipalAvailable(options.admitted.sessionFile)) {
|
|
22674
|
-
if (terminal !== void 0) presentTerminal(terminal, options.io);
|
|
22675
|
-
return result2;
|
|
22676
22936
|
}
|
|
22677
22937
|
autoResumeAttempts++;
|
|
22678
22938
|
currentExtraArgs = options.buildResumeArgs();
|
|
22679
22939
|
isFirst = false;
|
|
22680
22940
|
}
|
|
22681
22941
|
}
|
|
22682
|
-
var dummyIo;
|
|
22942
|
+
var dummyIo, DISPATCH_ERROR_RETENTION_ENTRY_TYPE;
|
|
22683
22943
|
var init_auto_resume = __esm({
|
|
22684
22944
|
"src/public-cli/auto-resume.ts"() {
|
|
22685
22945
|
"use strict";
|
|
@@ -22690,12 +22950,13 @@ var init_auto_resume = __esm({
|
|
|
22690
22950
|
dummyIo = { stdout: () => {
|
|
22691
22951
|
}, stderr: () => {
|
|
22692
22952
|
} };
|
|
22953
|
+
DISPATCH_ERROR_RETENTION_ENTRY_TYPE = "ak_run_dispatch_error_retention";
|
|
22693
22954
|
}
|
|
22694
22955
|
});
|
|
22695
22956
|
|
|
22696
22957
|
// src/public-cli/coder-run.ts
|
|
22697
22958
|
import { writeFile as writeFile6 } from "node:fs/promises";
|
|
22698
|
-
import { join as
|
|
22959
|
+
import { join as join13 } from "node:path";
|
|
22699
22960
|
function buildCoderActivationExtraArgs(admitted, options) {
|
|
22700
22961
|
const prompt = buildCoderTransportPrompt(
|
|
22701
22962
|
admitted,
|
|
@@ -22857,7 +23118,7 @@ async function dispatchAdmittedCoder(input) {
|
|
|
22857
23118
|
}
|
|
22858
23119
|
try {
|
|
22859
23120
|
await writeFile6(
|
|
22860
|
-
|
|
23121
|
+
join13(admitted.runDirectory, "stderr.log"),
|
|
22861
23122
|
result2.stderr,
|
|
22862
23123
|
"utf8"
|
|
22863
23124
|
);
|
|
@@ -23088,7 +23349,7 @@ var init_coder_run = __esm({
|
|
|
23088
23349
|
|
|
23089
23350
|
// src/public-cli/collector-run.ts
|
|
23090
23351
|
import { writeFile as writeFile7 } from "node:fs/promises";
|
|
23091
|
-
import { join as
|
|
23352
|
+
import { join as join14 } from "node:path";
|
|
23092
23353
|
function buildCollectorActivationExtraArgs(admitted, options = {}) {
|
|
23093
23354
|
const prompt = buildCollectorTransportPrompt(
|
|
23094
23355
|
admitted,
|
|
@@ -23193,7 +23454,7 @@ async function dispatchAdmittedCollector(input) {
|
|
|
23193
23454
|
}
|
|
23194
23455
|
try {
|
|
23195
23456
|
await writeFile7(
|
|
23196
|
-
|
|
23457
|
+
join14(admitted.runDirectory, "stderr.log"),
|
|
23197
23458
|
result2.stderr,
|
|
23198
23459
|
"utf8"
|
|
23199
23460
|
);
|
|
@@ -23324,7 +23585,7 @@ var init_collector_run = __esm({
|
|
|
23324
23585
|
|
|
23325
23586
|
// src/public-cli/doctor-run.ts
|
|
23326
23587
|
import { writeFile as writeFile8 } from "node:fs/promises";
|
|
23327
|
-
import { join as
|
|
23588
|
+
import { join as join15 } from "node:path";
|
|
23328
23589
|
function buildDoctorActivationExtraArgs(admitted, options = {}) {
|
|
23329
23590
|
const prompt = buildDoctorTransportPrompt(
|
|
23330
23591
|
admitted,
|
|
@@ -23422,7 +23683,7 @@ async function dispatchAdmittedDoctor(input) {
|
|
|
23422
23683
|
}
|
|
23423
23684
|
try {
|
|
23424
23685
|
await writeFile8(
|
|
23425
|
-
|
|
23686
|
+
join15(admitted.runDirectory, "stderr.log"),
|
|
23426
23687
|
result2.stderr,
|
|
23427
23688
|
"utf8"
|
|
23428
23689
|
);
|
|
@@ -23556,7 +23817,7 @@ var init_doctor_run = __esm({
|
|
|
23556
23817
|
|
|
23557
23818
|
// src/public-cli/fixer-run.ts
|
|
23558
23819
|
import { writeFile as writeFile9 } from "node:fs/promises";
|
|
23559
|
-
import { join as
|
|
23820
|
+
import { join as join16 } from "node:path";
|
|
23560
23821
|
function buildFixerActivationExtraArgs(admitted, options) {
|
|
23561
23822
|
const prompt = buildFixerTransportPrompt(
|
|
23562
23823
|
admitted,
|
|
@@ -23727,7 +23988,7 @@ async function dispatchAdmittedFixer(input) {
|
|
|
23727
23988
|
}
|
|
23728
23989
|
try {
|
|
23729
23990
|
await writeFile9(
|
|
23730
|
-
|
|
23991
|
+
join16(admitted.runDirectory, "stderr.log"),
|
|
23731
23992
|
result2.stderr,
|
|
23732
23993
|
"utf8"
|
|
23733
23994
|
);
|
|
@@ -23965,7 +24226,7 @@ var init_fixer_run = __esm({
|
|
|
23965
24226
|
|
|
23966
24227
|
// src/public-cli/judge-run.ts
|
|
23967
24228
|
import { writeFile as writeFile10 } from "node:fs/promises";
|
|
23968
|
-
import { join as
|
|
24229
|
+
import { join as join17 } from "node:path";
|
|
23969
24230
|
function buildJudgeActivationExtraArgs(admitted, options = {}) {
|
|
23970
24231
|
const prompt = buildJudgeTransportPrompt(
|
|
23971
24232
|
admitted,
|
|
@@ -24113,7 +24374,7 @@ async function dispatchAdmittedJudge(input) {
|
|
|
24113
24374
|
}
|
|
24114
24375
|
try {
|
|
24115
24376
|
await writeFile10(
|
|
24116
|
-
|
|
24377
|
+
join17(admitted.runDirectory, "stderr.log"),
|
|
24117
24378
|
result2.stderr,
|
|
24118
24379
|
"utf8"
|
|
24119
24380
|
);
|
|
@@ -24311,8 +24572,8 @@ var init_judge_run = __esm({
|
|
|
24311
24572
|
});
|
|
24312
24573
|
|
|
24313
24574
|
// src/public-cli/merger-run.ts
|
|
24314
|
-
import { mkdir as
|
|
24315
|
-
import { join as
|
|
24575
|
+
import { mkdir as mkdir5, writeFile as writeFile11 } from "node:fs/promises";
|
|
24576
|
+
import { join as join18, resolve as resolve6 } from "node:path";
|
|
24316
24577
|
function buildMergerActivationExtraArgs(admitted, options) {
|
|
24317
24578
|
const prompt = buildMergerTransportPrompt(
|
|
24318
24579
|
admitted,
|
|
@@ -24472,7 +24733,7 @@ async function dispatchAdmittedMerger(input) {
|
|
|
24472
24733
|
}
|
|
24473
24734
|
try {
|
|
24474
24735
|
await writeFile11(
|
|
24475
|
-
|
|
24736
|
+
join18(admitted.runDirectory, "stderr.log"),
|
|
24476
24737
|
result2.stderr,
|
|
24477
24738
|
"utf8"
|
|
24478
24739
|
);
|
|
@@ -24545,7 +24806,7 @@ async function admitMergerShellForActivationFailure(options) {
|
|
|
24545
24806
|
const runId = (options.createRunId ?? uuidv7)();
|
|
24546
24807
|
const { ledgerHome, bookKey, runDirectory, sessionDirectory, sessionFile } = roleRunSessionCoordinates({ cwd: projectRoot, runId, role: "merger", home: options.home });
|
|
24547
24808
|
ensureRealDirectoryTree(ledgerHome, sessionDirectory);
|
|
24548
|
-
await
|
|
24809
|
+
await mkdir5(runDirectory, { recursive: true });
|
|
24549
24810
|
const emptyDerived = {
|
|
24550
24811
|
targetObjectId: "",
|
|
24551
24812
|
sourceObjectId: "",
|
|
@@ -24553,8 +24814,8 @@ async function admitMergerShellForActivationFailure(options) {
|
|
|
24553
24814
|
expectedConflictPaths: [],
|
|
24554
24815
|
resolutionScope: []
|
|
24555
24816
|
};
|
|
24556
|
-
const admittedRequestPath =
|
|
24557
|
-
const mergerInputPath =
|
|
24817
|
+
const admittedRequestPath = join18(runDirectory, "admitted-request.json");
|
|
24818
|
+
const mergerInputPath = join18(runDirectory, "merger-input.json");
|
|
24558
24819
|
await writeFile11(
|
|
24559
24820
|
admittedRequestPath,
|
|
24560
24821
|
`${JSON.stringify(
|
|
@@ -24784,7 +25045,7 @@ var init_merger_run = __esm({
|
|
|
24784
25045
|
|
|
24785
25046
|
// src/public-cli/reviewer-run.ts
|
|
24786
25047
|
import { writeFile as writeFile12 } from "node:fs/promises";
|
|
24787
|
-
import { join as
|
|
25048
|
+
import { join as join19 } from "node:path";
|
|
24788
25049
|
function buildReviewerTicketNumberArgs(ticketNumber) {
|
|
24789
25050
|
return ticketNumber === void 0 ? [] : ["--ak-review-ticket-number", String(ticketNumber)];
|
|
24790
25051
|
}
|
|
@@ -24953,7 +25214,7 @@ async function dispatchAdmittedReviewer(input) {
|
|
|
24953
25214
|
}
|
|
24954
25215
|
try {
|
|
24955
25216
|
await writeFile12(
|
|
24956
|
-
|
|
25217
|
+
join19(admitted.runDirectory, "stderr.log"),
|
|
24957
25218
|
result2.stderr,
|
|
24958
25219
|
"utf8"
|
|
24959
25220
|
);
|
|
@@ -25197,12 +25458,12 @@ var init_reviewer_run = __esm({
|
|
|
25197
25458
|
});
|
|
25198
25459
|
|
|
25199
25460
|
// src/atomic-write.ts
|
|
25200
|
-
import { randomUUID as
|
|
25461
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
25201
25462
|
import { rename, rm, writeFile as writeFile13 } from "node:fs/promises";
|
|
25202
|
-
import { dirname as dirname7, join as
|
|
25463
|
+
import { dirname as dirname7, join as join20 } from "node:path";
|
|
25203
25464
|
async function writeFileAtomically(destination, contents) {
|
|
25204
25465
|
const parent = dirname7(destination);
|
|
25205
|
-
const temporary =
|
|
25466
|
+
const temporary = join20(parent, `.atomic-write-${randomUUID3()}.tmp`);
|
|
25206
25467
|
try {
|
|
25207
25468
|
await writeFile13(temporary, contents);
|
|
25208
25469
|
await rename(temporary, destination);
|
|
@@ -25218,8 +25479,8 @@ var init_atomic_write = __esm({
|
|
|
25218
25479
|
});
|
|
25219
25480
|
|
|
25220
25481
|
// src/taishi-index.ts
|
|
25221
|
-
import { open as
|
|
25222
|
-
import { dirname as dirname8, join as
|
|
25482
|
+
import { open as open4, readFile as readFile11, unlink as unlink4 } from "node:fs/promises";
|
|
25483
|
+
import { dirname as dirname8, join as join21 } from "node:path";
|
|
25223
25484
|
function sleep(ms) {
|
|
25224
25485
|
return new Promise((resolve8) => {
|
|
25225
25486
|
setTimeout(resolve8, ms);
|
|
@@ -25228,12 +25489,12 @@ function sleep(ms) {
|
|
|
25228
25489
|
async function withTaishiLibraryIndexLock(ledgerHome, fn) {
|
|
25229
25490
|
const indexPath = taishiLibraryIndexPath(ledgerHome);
|
|
25230
25491
|
ensureRealDirectoryTree(ledgerHome, dirname8(indexPath));
|
|
25231
|
-
const lockPath =
|
|
25492
|
+
const lockPath = join21(dirname8(indexPath), LIBRARY_INDEX_LOCK_NAME);
|
|
25232
25493
|
assertLedgerFileInsideHome(lockPath, ledgerHome);
|
|
25233
25494
|
const startedAt = Date.now();
|
|
25234
25495
|
while (true) {
|
|
25235
25496
|
try {
|
|
25236
|
-
const handle = await
|
|
25497
|
+
const handle = await open4(lockPath, "wx");
|
|
25237
25498
|
try {
|
|
25238
25499
|
await handle.writeFile(`${process.pid}
|
|
25239
25500
|
`, "utf8");
|
|
@@ -25255,7 +25516,7 @@ async function withTaishiLibraryIndexLock(ledgerHome, fn) {
|
|
|
25255
25516
|
}
|
|
25256
25517
|
}
|
|
25257
25518
|
function taishiLibraryIndexPath(ledgerHome) {
|
|
25258
|
-
return
|
|
25519
|
+
return join21(ledgerHome, "taishi", "library-index.json");
|
|
25259
25520
|
}
|
|
25260
25521
|
function rowFromIssueMetricsPage(page) {
|
|
25261
25522
|
return {
|
|
@@ -25334,7 +25595,7 @@ async function readTaishiLibraryIndexPage(ledgerHome) {
|
|
|
25334
25595
|
const path = taishiLibraryIndexPath(ledgerHome);
|
|
25335
25596
|
let raw;
|
|
25336
25597
|
try {
|
|
25337
|
-
raw = await
|
|
25598
|
+
raw = await readFile11(path, "utf8");
|
|
25338
25599
|
} catch (error) {
|
|
25339
25600
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
25340
25601
|
return void 0;
|
|
@@ -25495,12 +25756,12 @@ var init_taishi_cohort = __esm({
|
|
|
25495
25756
|
});
|
|
25496
25757
|
|
|
25497
25758
|
// src/ledger-session-read.ts
|
|
25498
|
-
import { readFile as
|
|
25759
|
+
import { readFile as readFile12 } from "node:fs/promises";
|
|
25499
25760
|
function isRecord6(value) {
|
|
25500
25761
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25501
25762
|
}
|
|
25502
25763
|
async function readLedgerSessionJsonl(path) {
|
|
25503
|
-
const text = await
|
|
25764
|
+
const text = await readFile12(path, "utf8");
|
|
25504
25765
|
const lines = text.split("\n");
|
|
25505
25766
|
const rows = [];
|
|
25506
25767
|
for (let index = 0; index < lines.length; index += 1) {
|
|
@@ -25610,8 +25871,8 @@ function extractSessionToolIntervals(rows) {
|
|
|
25610
25871
|
if (resultTimestamp === void 0 || resultTimestamp.length === 0) {
|
|
25611
25872
|
throw new Error(`toolResult ${message.toolCallId} missing timestamp`);
|
|
25612
25873
|
}
|
|
25613
|
-
const
|
|
25614
|
-
if (
|
|
25874
|
+
const open5 = openById.get(message.toolCallId);
|
|
25875
|
+
if (open5 === void 0) {
|
|
25615
25876
|
const toolName = typeof message.toolName === "string" && message.toolName.length > 0 ? message.toolName : "unknown";
|
|
25616
25877
|
order.push({
|
|
25617
25878
|
toolCallId: message.toolCallId,
|
|
@@ -25621,10 +25882,10 @@ function extractSessionToolIntervals(rows) {
|
|
|
25621
25882
|
});
|
|
25622
25883
|
continue;
|
|
25623
25884
|
}
|
|
25624
|
-
if (
|
|
25885
|
+
if (open5.endedAt !== void 0) {
|
|
25625
25886
|
throw new Error(`duplicate toolResult for toolCallId ${message.toolCallId}`);
|
|
25626
25887
|
}
|
|
25627
|
-
|
|
25888
|
+
open5.endedAt = resultTimestamp;
|
|
25628
25889
|
}
|
|
25629
25890
|
}
|
|
25630
25891
|
return order.map((interval) => {
|
|
@@ -25657,9 +25918,9 @@ var init_ledger_session_read = __esm({
|
|
|
25657
25918
|
});
|
|
25658
25919
|
|
|
25659
25920
|
// src/run-terminal-artifacts.ts
|
|
25660
|
-
import { readdir as readdir4, readFile as
|
|
25661
|
-
import { basename as basename4, dirname as dirname9, join as
|
|
25662
|
-
function
|
|
25921
|
+
import { readdir as readdir4, readFile as readFile13 } from "node:fs/promises";
|
|
25922
|
+
import { basename as basename4, dirname as dirname9, join as join22 } from "node:path";
|
|
25923
|
+
function isMissingPathError4(error) {
|
|
25663
25924
|
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
25664
25925
|
}
|
|
25665
25926
|
function errorText2(error) {
|
|
@@ -25689,9 +25950,9 @@ function readUsableTerminalArtifactBody(body) {
|
|
|
25689
25950
|
async function readTerminalArtifactAtPath(path, file) {
|
|
25690
25951
|
let raw;
|
|
25691
25952
|
try {
|
|
25692
|
-
raw = await
|
|
25953
|
+
raw = await readFile13(path, "utf8");
|
|
25693
25954
|
} catch (error) {
|
|
25694
|
-
if (
|
|
25955
|
+
if (isMissingPathError4(error)) return void 0;
|
|
25695
25956
|
return {
|
|
25696
25957
|
status: "unreadable",
|
|
25697
25958
|
file,
|
|
@@ -25728,12 +25989,12 @@ async function listUniqueErrorFallbackPaths(directories) {
|
|
|
25728
25989
|
try {
|
|
25729
25990
|
names = await readdir4(dir);
|
|
25730
25991
|
} catch (error) {
|
|
25731
|
-
if (
|
|
25992
|
+
if (isMissingPathError4(error)) continue;
|
|
25732
25993
|
throw error;
|
|
25733
25994
|
}
|
|
25734
25995
|
for (const name of names.sort((a, b) => a.localeCompare(b))) {
|
|
25735
25996
|
if (!UNIQUE_ERROR_FALLBACK_NAME.test(name)) continue;
|
|
25736
|
-
found.push(
|
|
25997
|
+
found.push(join22(dir, name));
|
|
25737
25998
|
}
|
|
25738
25999
|
}
|
|
25739
26000
|
return found;
|
|
@@ -25749,14 +26010,14 @@ function presentUniqueFallbackBoundToRun(body, expectedRunId) {
|
|
|
25749
26010
|
return typeof body.runId === "string" && body.runId === expectedRunId;
|
|
25750
26011
|
}
|
|
25751
26012
|
async function readRunTerminalArtifact(runDirectory) {
|
|
25752
|
-
const artifactsDir =
|
|
26013
|
+
const artifactsDir = join22(runDirectory, "artifacts");
|
|
25753
26014
|
for (const file of RUN_TERMINAL_ARTIFACT_FILES) {
|
|
25754
|
-
const path =
|
|
26015
|
+
const path = join22(artifactsDir, file);
|
|
25755
26016
|
const read3 = await readTerminalArtifactAtPath(path, file);
|
|
25756
26017
|
if (read3 !== void 0) return read3;
|
|
25757
26018
|
}
|
|
25758
26019
|
for (const relative3 of RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS) {
|
|
25759
|
-
const path =
|
|
26020
|
+
const path = join22(runDirectory, relative3);
|
|
25760
26021
|
const read3 = await readTerminalArtifactAtPath(path, "error.json");
|
|
25761
26022
|
if (read3 !== void 0) return read3;
|
|
25762
26023
|
}
|
|
@@ -25793,9 +26054,9 @@ var init_run_terminal_artifacts = __esm({
|
|
|
25793
26054
|
});
|
|
25794
26055
|
|
|
25795
26056
|
// src/taishi-ledger.ts
|
|
25796
|
-
import { readdir as readdir5, readFile as
|
|
25797
|
-
import { join as
|
|
25798
|
-
function
|
|
26057
|
+
import { readdir as readdir5, readFile as readFile14 } from "node:fs/promises";
|
|
26058
|
+
import { join as join23 } from "node:path";
|
|
26059
|
+
function isMissingPathError5(error) {
|
|
25799
26060
|
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
25800
26061
|
}
|
|
25801
26062
|
function errorText3(error) {
|
|
@@ -25807,7 +26068,7 @@ function isRecord8(value) {
|
|
|
25807
26068
|
async function readExistingRunLifecycleState(runDirectory) {
|
|
25808
26069
|
try {
|
|
25809
26070
|
const raw = JSON.parse(
|
|
25810
|
-
await
|
|
26071
|
+
await readFile14(join23(runDirectory, "run-state.json"), "utf8")
|
|
25811
26072
|
);
|
|
25812
26073
|
if (!isRecord8(raw) || typeof raw.state !== "string") return void 0;
|
|
25813
26074
|
return raw.state;
|
|
@@ -25832,16 +26093,16 @@ async function listLedgerBookNames(booksRoot) {
|
|
|
25832
26093
|
const entries = await readdir5(booksRoot, { withFileTypes: true });
|
|
25833
26094
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
25834
26095
|
} catch (error) {
|
|
25835
|
-
if (
|
|
26096
|
+
if (isMissingPathError5(error)) return [];
|
|
25836
26097
|
throw error;
|
|
25837
26098
|
}
|
|
25838
26099
|
}
|
|
25839
26100
|
async function readInvocationScopeFields(runDirectory) {
|
|
25840
26101
|
let raw;
|
|
25841
26102
|
try {
|
|
25842
|
-
raw = await
|
|
26103
|
+
raw = await readFile14(join23(runDirectory, "invocation.json"), "utf8");
|
|
25843
26104
|
} catch (error) {
|
|
25844
|
-
if (
|
|
26105
|
+
if (isMissingPathError5(error)) return void 0;
|
|
25845
26106
|
throw error;
|
|
25846
26107
|
}
|
|
25847
26108
|
const parsed = JSON.parse(raw);
|
|
@@ -25871,15 +26132,15 @@ function decideIssueScope(input) {
|
|
|
25871
26132
|
}
|
|
25872
26133
|
async function resolveSessionFile(runDirectory) {
|
|
25873
26134
|
try {
|
|
25874
|
-
const raw = await
|
|
26135
|
+
const raw = await readFile14(join23(runDirectory, "invocation.json"), "utf8");
|
|
25875
26136
|
const parsed = JSON.parse(raw);
|
|
25876
26137
|
if (isRecord8(parsed) && typeof parsed.sessionFile === "string" && parsed.sessionFile.trim() !== "") {
|
|
25877
26138
|
return parsed.sessionFile;
|
|
25878
26139
|
}
|
|
25879
26140
|
} catch (error) {
|
|
25880
|
-
if (!
|
|
26141
|
+
if (!isMissingPathError5(error)) throw error;
|
|
25881
26142
|
}
|
|
25882
|
-
return
|
|
26143
|
+
return join23(runDirectory, "session", "session.jsonl");
|
|
25883
26144
|
}
|
|
25884
26145
|
async function classifyScopedRun(input) {
|
|
25885
26146
|
const missingSources = [];
|
|
@@ -26003,7 +26264,7 @@ async function classifyScopedRun(input) {
|
|
|
26003
26264
|
async function scanTaishiIssueRuns(input) {
|
|
26004
26265
|
const ledgerHome = resolveActivationLedgerHome();
|
|
26005
26266
|
const scopeTicketNumber = input.ticketNumber;
|
|
26006
|
-
const booksRoot =
|
|
26267
|
+
const booksRoot = join23(ledgerHome, "books");
|
|
26007
26268
|
let wholeBook = false;
|
|
26008
26269
|
let scopeRootIdentity;
|
|
26009
26270
|
let bookNames;
|
|
@@ -26030,19 +26291,19 @@ async function scanTaishiIssueRuns(input) {
|
|
|
26030
26291
|
const unreadable = [];
|
|
26031
26292
|
const scopeConflicts = [];
|
|
26032
26293
|
for (const book of bookNames) {
|
|
26033
|
-
const runsDir =
|
|
26294
|
+
const runsDir = join23(booksRoot, book, "runs");
|
|
26034
26295
|
let runNames;
|
|
26035
26296
|
try {
|
|
26036
26297
|
const entries = await readdir5(runsDir, { withFileTypes: true });
|
|
26037
26298
|
runNames = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
26038
26299
|
} catch (error) {
|
|
26039
|
-
if (
|
|
26300
|
+
if (isMissingPathError5(error)) continue;
|
|
26040
26301
|
throw error;
|
|
26041
26302
|
}
|
|
26042
26303
|
for (const runName of runNames) {
|
|
26043
26304
|
const parsed = parseRunDirectoryName(runName);
|
|
26044
26305
|
if (parsed === void 0) continue;
|
|
26045
|
-
const runDirectory =
|
|
26306
|
+
const runDirectory = join23(runsDir, runName);
|
|
26046
26307
|
let scopeFields;
|
|
26047
26308
|
try {
|
|
26048
26309
|
scopeFields = await readInvocationScopeFields(runDirectory);
|
|
@@ -26817,7 +27078,7 @@ var init_taishi_metric_family = __esm({
|
|
|
26817
27078
|
|
|
26818
27079
|
// src/taishi-page.ts
|
|
26819
27080
|
import { createHash as createHash4 } from "node:crypto";
|
|
26820
|
-
import { dirname as dirname10, join as
|
|
27081
|
+
import { dirname as dirname10, join as join24 } from "node:path";
|
|
26821
27082
|
function taishiIssuePageKey(address) {
|
|
26822
27083
|
const parts = ["book", address.bookKey];
|
|
26823
27084
|
if (address.issueNumber !== void 0) {
|
|
@@ -26828,7 +27089,7 @@ function taishiIssuePageKey(address) {
|
|
|
26828
27089
|
return createHash4("sha256").update(parts.join("\0")).digest("hex").slice(0, 32);
|
|
26829
27090
|
}
|
|
26830
27091
|
function taishiIssuePagePath(ledgerHome, address) {
|
|
26831
|
-
return
|
|
27092
|
+
return join24(ledgerHome, "taishi", "issues", `${taishiIssuePageKey(address)}.json`);
|
|
26832
27093
|
}
|
|
26833
27094
|
function taishiIssuePageAddressFromPage(page) {
|
|
26834
27095
|
return {
|
|
@@ -26964,8 +27225,8 @@ var init_taishi_page = __esm({
|
|
|
26964
27225
|
});
|
|
26965
27226
|
|
|
26966
27227
|
// src/taishi-entry.ts
|
|
26967
|
-
import { readFile as
|
|
26968
|
-
function
|
|
27228
|
+
import { readFile as readFile15 } from "node:fs/promises";
|
|
27229
|
+
function isMissingPathError6(error) {
|
|
26969
27230
|
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
26970
27231
|
}
|
|
26971
27232
|
function cachedPageMatchesRequestedScope(page, input) {
|
|
@@ -27003,13 +27264,13 @@ async function readOrComputeTaishiIssuePage(input) {
|
|
|
27003
27264
|
...issueNumber === void 0 && input.bookKey === void 0 ? { scopeRootIdentity: projectRoot } : {}
|
|
27004
27265
|
});
|
|
27005
27266
|
try {
|
|
27006
|
-
const raw = await
|
|
27267
|
+
const raw = await readFile15(pagePath, "utf8");
|
|
27007
27268
|
const page = JSON.parse(raw);
|
|
27008
27269
|
if (cachedPageMatchesRequestedScope(page, { bookKey, ...input })) {
|
|
27009
27270
|
return { mode: "issue", page, pagePath };
|
|
27010
27271
|
}
|
|
27011
27272
|
} catch (error) {
|
|
27012
|
-
if (!
|
|
27273
|
+
if (!isMissingPathError6(error)) {
|
|
27013
27274
|
throw new TaishiIssueComputeError({
|
|
27014
27275
|
bookKey,
|
|
27015
27276
|
projectRoot,
|
|
@@ -27096,10 +27357,10 @@ async function runTaishiModelGroupsMode(input) {
|
|
|
27096
27357
|
scopeRootIdentity: projectRoot
|
|
27097
27358
|
});
|
|
27098
27359
|
try {
|
|
27099
|
-
const raw = await
|
|
27360
|
+
const raw = await readFile15(pagePath, "utf8");
|
|
27100
27361
|
JSON.parse(raw);
|
|
27101
27362
|
} catch (error) {
|
|
27102
|
-
if (!
|
|
27363
|
+
if (!isMissingPathError6(error)) {
|
|
27103
27364
|
throw new TaishiIssueComputeError({ bookKey, projectRoot, cause: error });
|
|
27104
27365
|
}
|
|
27105
27366
|
try {
|
|
@@ -27194,7 +27455,7 @@ var init_taishi_entry = __esm({
|
|
|
27194
27455
|
});
|
|
27195
27456
|
|
|
27196
27457
|
// src/public-cli/taishi-run.ts
|
|
27197
|
-
import { readFile as
|
|
27458
|
+
import { readFile as readFile16 } from "node:fs/promises";
|
|
27198
27459
|
import { isAbsolute as isAbsolute5, resolve as resolve7 } from "node:path";
|
|
27199
27460
|
function resolveTaishiIssueBookKeyFromCwd(cwd = process.cwd()) {
|
|
27200
27461
|
try {
|
|
@@ -27247,7 +27508,7 @@ async function buildTaishiSweepModeInputFromAttachmentPaths(attachmentPaths) {
|
|
|
27247
27508
|
const absolute = isAbsolute5(sourcePath) ? sourcePath : resolve7(sourcePath);
|
|
27248
27509
|
let bytes;
|
|
27249
27510
|
try {
|
|
27250
|
-
bytes = await
|
|
27511
|
+
bytes = await readFile16(absolute);
|
|
27251
27512
|
} catch (error) {
|
|
27252
27513
|
throw new CliUsageError(
|
|
27253
27514
|
`taishi sweep attachment is not a readable regular file: ${sourcePath}`,
|
|
@@ -27349,7 +27610,7 @@ __export(cli_exports, {
|
|
|
27349
27610
|
});
|
|
27350
27611
|
import { realpath as realpath5 } from "node:fs/promises";
|
|
27351
27612
|
import { homedir as homedir3 } from "node:os";
|
|
27352
|
-
import { join as
|
|
27613
|
+
import { join as join25 } from "node:path";
|
|
27353
27614
|
function takePublicGlobalFlag(argv, index, options) {
|
|
27354
27615
|
const tokens = argv.slice(index);
|
|
27355
27616
|
const taken = options.takeDashed(tokens);
|
|
@@ -27395,7 +27656,7 @@ function resolveHome(env) {
|
|
|
27395
27656
|
return env.home ?? process.env.HOME ?? homedir3();
|
|
27396
27657
|
}
|
|
27397
27658
|
function resolveAgentDir(env, home) {
|
|
27398
|
-
return env.agentDir ?? process.env.PI_CODING_AGENT_DIR ??
|
|
27659
|
+
return env.agentDir ?? process.env.PI_CODING_AGENT_DIR ?? join25(home, ".pi", "agent");
|
|
27399
27660
|
}
|
|
27400
27661
|
function parseThinking(value) {
|
|
27401
27662
|
if (!THINKING_LEVELS2.has(value)) {
|
|
@@ -28265,7 +28526,7 @@ var init_cli = __esm({
|
|
|
28265
28526
|
|
|
28266
28527
|
// src/public-cli/main.ts
|
|
28267
28528
|
import { existsSync as existsSync3 } from "node:fs";
|
|
28268
|
-
import { dirname as dirname11, join as
|
|
28529
|
+
import { dirname as dirname11, join as join26 } from "node:path";
|
|
28269
28530
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
28270
28531
|
|
|
28271
28532
|
// src/public-cli/host-pi-runtime.ts
|
|
@@ -28354,8 +28615,8 @@ function linkPackage(packageRoot2, name, targetDir) {
|
|
|
28354
28615
|
// src/public-cli/main.ts
|
|
28355
28616
|
var here = dirname11(fileURLToPath2(import.meta.url));
|
|
28356
28617
|
function resolvePackageRoot(binDir) {
|
|
28357
|
-
const canonical =
|
|
28358
|
-
if (existsSync3(
|
|
28618
|
+
const canonical = join26(binDir, "..", "..");
|
|
28619
|
+
if (existsSync3(join26(canonical, "package.json"))) {
|
|
28359
28620
|
return canonical;
|
|
28360
28621
|
}
|
|
28361
28622
|
return binDir;
|
package/package.json
CHANGED
package/src/activation-ledger.ts
CHANGED
|
@@ -152,6 +152,17 @@ function appendActivationLedgerLine(
|
|
|
152
152
|
ensureRealDirectoryTree(resolvedHome, parent);
|
|
153
153
|
assertLedgerFileInsideHome(resolvedLedger, resolvedHome);
|
|
154
154
|
|
|
155
|
+
// Fail-closed guard (same shape as PR #418's publication seam): on platforms
|
|
156
|
+
// without O_NOFOLLOW (e.g. Windows, nodejs/node#41590) the JS bitwise-or in
|
|
157
|
+
// ACTIVATION_LEDGER_APPEND_OPEN_FLAGS would silently drop the flag and the
|
|
158
|
+
// lstat→open TOCTOU anti-symlink protection would vanish while the comment
|
|
159
|
+
// above still claims it. Refuse loudly via the existing typed ledger failure
|
|
160
|
+
// channel instead of appending unprotected; never silently degrade.
|
|
161
|
+
if (typeof constants.O_NOFOLLOW !== "number") {
|
|
162
|
+
throw new ActivationLedgerError(
|
|
163
|
+
"activation ledger append requires O_NOFOLLOW open-flag support (anti-symlink TOCTOU protection must not be silently dropped); refusing to append",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
155
166
|
const bytes = Buffer.isBuffer(line) ? line : Buffer.from(line);
|
|
156
167
|
let ledgerFd: number | undefined;
|
|
157
168
|
let primaryFailure: unknown;
|
|
@@ -4,10 +4,29 @@
|
|
|
4
4
|
* per call by the caller, #422 — never re-read from disk inside the loop),
|
|
5
5
|
* in-place (same runId/session).
|
|
6
6
|
* Unifies presentation: intermediate attempts use dummyIo, only final Terminal is presented.
|
|
7
|
+
*
|
|
8
|
+
* Owner 2026-08-23: a dispatch that exits by throwing used to bypass the entire
|
|
9
|
+
* retry mechanism (the throw escaped the while-loop before the count check ever
|
|
10
|
+
* ran). Every exception is now retained whole, in place, and the ordinary retry
|
|
11
|
+
* path continues: same budget, same call-local count semantics. No failure-type
|
|
12
|
+
* classification — every thrown value is treated identically.
|
|
7
13
|
*/
|
|
8
|
-
import {
|
|
14
|
+
import { constants as fsConstants } from "node:fs";
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { appendFile, lstat, mkdir, open, readFile } from "node:fs/promises";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
AUTO_RESUME_LIMIT,
|
|
21
|
+
describeErrorIdentity,
|
|
22
|
+
isSessionPrincipalAvailable,
|
|
23
|
+
acquireRunWriterLease,
|
|
24
|
+
markRunTerminal,
|
|
25
|
+
RunWriterLeaseHeldError,
|
|
26
|
+
type RunWriterLease,
|
|
27
|
+
} from "./run-lifecycle.ts";
|
|
9
28
|
import { parseAutoResumeLimit } from "./config.ts";
|
|
10
|
-
import { isLawfulTypedTerminalOutcome, formatTerminalResult, type TerminalResult } from "./terminal.ts";
|
|
29
|
+
import { isLawfulTypedTerminalOutcome, formatTerminalResult, type TerminalArtifactRef, type TerminalResult, type TerminalRoleName } from "./terminal.ts";
|
|
11
30
|
import { presentFailureTerminal, presentStructuralRejection } from "./settlement.ts";
|
|
12
31
|
import type { CliIo } from "./cli-io.ts";
|
|
13
32
|
|
|
@@ -21,13 +40,305 @@ function presentTerminal(terminal: TerminalResult, io: CliIo): void {
|
|
|
21
40
|
}
|
|
22
41
|
}
|
|
23
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Best-effort finalization of the durable run state before an exception-path
|
|
45
|
+
* synthetic failure terminal is returned (#426 review: taishi-ledger classifies
|
|
46
|
+
* running runs as live — an exhausted invocation must not remain live
|
|
47
|
+
* indefinitely). Finalization failure must not mask the real cause.
|
|
48
|
+
*/
|
|
49
|
+
async function finalizeExceptionRunBestEffort(runDirectory: string, io: CliIo): Promise<void> {
|
|
50
|
+
try {
|
|
51
|
+
await markRunTerminal(runDirectory);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
io.stderr(
|
|
54
|
+
`run terminal-state finalization failed (best-effort continue): ${describeErrorIdentity(error)}\n`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
24
59
|
export type AutoResumeDispatchResult = {
|
|
25
60
|
exitCode: number;
|
|
26
61
|
terminal?: TerminalResult;
|
|
27
62
|
};
|
|
28
63
|
|
|
64
|
+
/** Session custom-entry type carrying the pointer to one dispatch error file. */
|
|
65
|
+
export const DISPATCH_ERROR_RETENTION_ENTRY_TYPE = "ak_run_dispatch_error_retention" as const;
|
|
66
|
+
|
|
67
|
+
/** Artifacts subdirectory of a run directory (established run-artifacts location). */
|
|
68
|
+
function runArtifactsDirectory(runDirectory: string): string {
|
|
69
|
+
return join(runDirectory, "artifacts");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* #182-A hardened path identity, mirrored from settlement.ts's
|
|
74
|
+
* ensureAuditEvidenceDirectory: a planted symlink at the run directory or the
|
|
75
|
+
* artifacts path must not receive the dispatch error dump (O_NOFOLLOW only
|
|
76
|
+
* protects the final file name; recursive mkdir would accept a symlinked
|
|
77
|
+
* parent). Fails loudly with the true cause instead.
|
|
78
|
+
*/
|
|
79
|
+
async function ensureRealArtifactsDirectory(runDirectory: string): Promise<string> {
|
|
80
|
+
const runStat = await lstat(runDirectory);
|
|
81
|
+
if (runStat.isSymbolicLink() || !runStat.isDirectory()) {
|
|
82
|
+
throw new Error("dispatch error retention: run directory is not a real directory");
|
|
83
|
+
}
|
|
84
|
+
const artifactsDir = runArtifactsDirectory(runDirectory);
|
|
85
|
+
try {
|
|
86
|
+
const existing = await lstat(artifactsDir);
|
|
87
|
+
if (existing.isSymbolicLink() || !existing.isDirectory()) {
|
|
88
|
+
throw new Error("dispatch error retention: artifacts path is not a real directory");
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (!isMissingPathError(error)) throw error;
|
|
92
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
93
|
+
const created = await lstat(artifactsDir);
|
|
94
|
+
if (created.isSymbolicLink() || !created.isDirectory()) {
|
|
95
|
+
throw new Error("dispatch error retention: artifacts directory is not a real directory");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return artifactsDir;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Whole-object transfer of a thrown value (owner 2026-08-23: 「记录所有错误信息。
|
|
103
|
+
* 不能丢详细情况」). Every own property of the Error object — enumerable or not,
|
|
104
|
+
* which is how message/stack and any attached identity land verbatim — plus the
|
|
105
|
+
* constructor name and the full cause chain. No field list is prescribed or
|
|
106
|
+
* filtered: whatever the exception object carries goes into the file as-is.
|
|
107
|
+
*/
|
|
108
|
+
function serializeThrownValue(value: unknown, depth = 0, seen = new WeakSet<object>()): unknown {
|
|
109
|
+
if (value instanceof Error) {
|
|
110
|
+
if (seen.has(value)) return "[circular]";
|
|
111
|
+
seen.add(value);
|
|
112
|
+
const transferred: Record<string, unknown> = {};
|
|
113
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
114
|
+
transferred[key] = transferNestedValue(
|
|
115
|
+
(value as unknown as Record<string, unknown>)[key],
|
|
116
|
+
depth + 1,
|
|
117
|
+
seen,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
errorKind: "Error",
|
|
122
|
+
constructorName: value.constructor?.name,
|
|
123
|
+
...transferred,
|
|
124
|
+
...(value.cause === undefined
|
|
125
|
+
? {}
|
|
126
|
+
: {
|
|
127
|
+
causeChain:
|
|
128
|
+
depth >= 10
|
|
129
|
+
? "[cause-chain-depth-limit]"
|
|
130
|
+
: serializeThrownValue(value.cause, depth + 1, seen),
|
|
131
|
+
}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** ENOENT identity shared with settlement.ts's hardened audit-artifact path. */
|
|
138
|
+
function isMissingPathError(error: unknown): boolean {
|
|
139
|
+
return (
|
|
140
|
+
error instanceof Error &&
|
|
141
|
+
"code" in error &&
|
|
142
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Recursive Error-property transfer (#426 review: nested Errors must not be
|
|
148
|
+
* passed raw to JSON.stringify — their non-enumerable message/stack would
|
|
149
|
+
* serialize as {}). Depth-limited; cycle-safe via the seen set so the recursive
|
|
150
|
+
* construction itself cannot diverge before stringify runs.
|
|
151
|
+
*/
|
|
152
|
+
function transferNestedValue(value: unknown, depth: number, seen: WeakSet<object>): unknown {
|
|
153
|
+
if (value instanceof Error) return serializeThrownValue(value, depth, seen);
|
|
154
|
+
if (depth >= 10) return "[nested-depth-limit]";
|
|
155
|
+
if (Array.isArray(value)) {
|
|
156
|
+
return value.map((item) => transferNestedValue(item, depth + 1, seen));
|
|
157
|
+
}
|
|
158
|
+
if (value !== null && typeof value === "object") {
|
|
159
|
+
if (seen.has(value)) return "[circular]";
|
|
160
|
+
seen.add(value);
|
|
161
|
+
const transferred: Record<string, unknown> = {};
|
|
162
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
163
|
+
transferred[key] = transferNestedValue(
|
|
164
|
+
(value as unknown as Record<string, unknown>)[key],
|
|
165
|
+
depth + 1,
|
|
166
|
+
seen,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return transferred;
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Cycle- and bigint-safe JSON replacer so serialization itself cannot drop data. */
|
|
175
|
+
function jsonSafeReplacer(): (key: string, value: unknown) => unknown {
|
|
176
|
+
const seen = new WeakSet<object>();
|
|
177
|
+
return (_key: string, value: unknown): unknown => {
|
|
178
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
179
|
+
if (typeof value === "object" && value !== null) {
|
|
180
|
+
if (seen.has(value)) return "[circular]";
|
|
181
|
+
seen.add(value);
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Retain one throwing dispatch attempt's complete exception as an independent
|
|
189
|
+
* per-attempt file under the run's artifacts directory, then leave an
|
|
190
|
+
* addressable pointer in the session principal (custom entry). Exclusive-create
|
|
191
|
+
* open (O_EXCL) with a per-attempt unique name enforces 史必追加 (#419): a later
|
|
192
|
+
* attempt can never overwrite an earlier attempt's file.
|
|
193
|
+
*/
|
|
194
|
+
async function retainDispatchError(
|
|
195
|
+
admitted: { sessionFile: string; runDirectory: string },
|
|
196
|
+
attempt: number,
|
|
197
|
+
error: unknown,
|
|
198
|
+
): Promise<{ file: string; pointerError?: unknown }> {
|
|
199
|
+
const artifactsDir = await ensureRealArtifactsDirectory(admitted.runDirectory);
|
|
200
|
+
const filePath = join(
|
|
201
|
+
artifactsDir,
|
|
202
|
+
`dispatch-error-attempt-${attempt}-${randomUUID()}.json`,
|
|
203
|
+
);
|
|
204
|
+
// Whole-object dump: everything the thrown value carries, nothing picked.
|
|
205
|
+
const payload = `${JSON.stringify(
|
|
206
|
+
{
|
|
207
|
+
version: 1,
|
|
208
|
+
attempt,
|
|
209
|
+
recordedAt: new Date().toISOString(),
|
|
210
|
+
error: serializeThrownValue(error),
|
|
211
|
+
},
|
|
212
|
+
jsonSafeReplacer(),
|
|
213
|
+
2,
|
|
214
|
+
)}\n`;
|
|
215
|
+
// O_EXCL: exclusive create — the retention history is append-only by
|
|
216
|
+
// construction; a colliding name fails loudly instead of overwriting.
|
|
217
|
+
// O_NOFOLLOW when the platform provides it keeps a planted symlink from
|
|
218
|
+
// being followed; on platforms without it, exclusivity still holds.
|
|
219
|
+
const noFollowFlag =
|
|
220
|
+
typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
|
|
221
|
+
const handle = await open(
|
|
222
|
+
filePath,
|
|
223
|
+
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollowFlag,
|
|
224
|
+
0o600,
|
|
225
|
+
);
|
|
226
|
+
try {
|
|
227
|
+
await handle.writeFile(payload, "utf8");
|
|
228
|
+
} finally {
|
|
229
|
+
await handle.close();
|
|
230
|
+
}
|
|
231
|
+
// Addressable pointer in the dossier (卷宗): reuses the session principal's
|
|
232
|
+
// appended custom-entry shape (mirrors pi SessionManager.appendCustomEntry,
|
|
233
|
+
// same mechanism as #419 attempt history — no second ledger introduced).
|
|
234
|
+
// The one-writer lease is held for the append (#426 review: production
|
|
235
|
+
// dispatchers release the lease in their finally before the rejection reaches
|
|
236
|
+
// this point); if a concurrent writer already holds it, the append is skipped
|
|
237
|
+
// gracefully — the error file itself is already durably retained.
|
|
238
|
+
let pointerLease: RunWriterLease;
|
|
239
|
+
try {
|
|
240
|
+
pointerLease = await acquireRunWriterLease(admitted.runDirectory);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error instanceof RunWriterLeaseHeldError) return { file: filePath };
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
// Pointer-stage failure (#426 fix_now #5) is separated from the file write:
|
|
246
|
+
// once the error file is durably on disk, a failed readFile/JSON.parse/
|
|
247
|
+
// appendFile must not reject through here and orphan it — the file path is
|
|
248
|
+
// still handed back to the caller (retainedErrorFiles → terminal
|
|
249
|
+
// dispatchErrorFiles/artifacts), and the pointer failure is reported
|
|
250
|
+
// separately by the caller.
|
|
251
|
+
let pointerError: unknown;
|
|
252
|
+
try {
|
|
253
|
+
const text = await readFile(admitted.sessionFile, "utf8");
|
|
254
|
+
// Session headers are not branch entries (#419 precedent in
|
|
255
|
+
// appendRunAttemptHistory excludes type === "session"); leave parentId null
|
|
256
|
+
// until a non-header entry exists so the pointer stays addressable.
|
|
257
|
+
let parentId: string | null = null;
|
|
258
|
+
for (const line of text.trim().split("\n").filter(Boolean)) {
|
|
259
|
+
const entry = JSON.parse(line) as { id?: unknown; type?: unknown };
|
|
260
|
+
if (typeof entry.id === "string" && entry.type !== "session") parentId = entry.id;
|
|
261
|
+
}
|
|
262
|
+
const timestamp = new Date().toISOString();
|
|
263
|
+
const pointerLine = `${JSON.stringify({
|
|
264
|
+
type: "custom",
|
|
265
|
+
customType: DISPATCH_ERROR_RETENTION_ENTRY_TYPE,
|
|
266
|
+
data: { version: 1, attempt, file: filePath, recordedAt: timestamp },
|
|
267
|
+
id: randomUUID(),
|
|
268
|
+
parentId,
|
|
269
|
+
timestamp,
|
|
270
|
+
})}\n`;
|
|
271
|
+
await appendFile(admitted.sessionFile, pointerLine, "utf8");
|
|
272
|
+
} catch (error) {
|
|
273
|
+
pointerError = error;
|
|
274
|
+
} finally {
|
|
275
|
+
await pointerLease.release();
|
|
276
|
+
}
|
|
277
|
+
return pointerError === undefined ? { file: filePath } : { file: filePath, pointerError };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Typed failure terminal for a retry path that ended with only exceptions:
|
|
282
|
+
* loud, non-lawful, carrying the last true cause and the pointers to the
|
|
283
|
+
* full per-attempt error files. Never rethrows the raw exception at callers.
|
|
284
|
+
*/
|
|
285
|
+
function dispatchExceptionFailureTerminal(input: {
|
|
286
|
+
role: TerminalRoleName;
|
|
287
|
+
runId: string;
|
|
288
|
+
causeError: unknown;
|
|
289
|
+
errorFiles: readonly string[];
|
|
290
|
+
autoResumeAttempts: number;
|
|
291
|
+
endReason: string;
|
|
292
|
+
/** True only when every attempt threw; otherwise describe just the final attempt. */
|
|
293
|
+
everyAttemptThrew: boolean;
|
|
294
|
+
}): TerminalResult {
|
|
295
|
+
// #426 review: this terminal fires whenever the FINAL dispatch throws, not
|
|
296
|
+
// only when every attempt threw — do not misrepresent a mixed retry history.
|
|
297
|
+
const history = input.everyAttemptThrew
|
|
298
|
+
? "dispatch threw an exception on every attempt"
|
|
299
|
+
: "the final dispatch threw an exception";
|
|
300
|
+
const diagnostic = `${history} (${input.endReason}; resumes used ${input.autoResumeAttempts}); last cause: ${describeErrorIdentity(input.causeError)}`;
|
|
301
|
+
const decisiveFacts: Record<string, unknown> = {
|
|
302
|
+
cause: "unrecognized",
|
|
303
|
+
diagnostic,
|
|
304
|
+
resumesUsed: input.autoResumeAttempts,
|
|
305
|
+
dispatchErrorFiles: [...input.errorFiles],
|
|
306
|
+
};
|
|
307
|
+
if (input.errorFiles.length > 0) {
|
|
308
|
+
decisiveFacts.lastDispatchErrorFile = input.errorFiles[input.errorFiles.length - 1];
|
|
309
|
+
}
|
|
310
|
+
const candidate = input.causeError as { name?: unknown; code?: unknown };
|
|
311
|
+
if (typeof candidate?.name === "string") decisiveFacts.errorName = candidate.name;
|
|
312
|
+
if (typeof candidate?.code === "string" || typeof candidate?.code === "number") {
|
|
313
|
+
decisiveFacts.errorCode = candidate.code;
|
|
314
|
+
}
|
|
315
|
+
const artifacts: TerminalArtifactRef[] = input.errorFiles.map((path) => ({
|
|
316
|
+
kind: "error",
|
|
317
|
+
path,
|
|
318
|
+
}));
|
|
319
|
+
return {
|
|
320
|
+
roleOutcome: {
|
|
321
|
+
kind: "failure",
|
|
322
|
+
role: input.role,
|
|
323
|
+
cause: "unrecognized",
|
|
324
|
+
diagnostic,
|
|
325
|
+
decisiveFacts,
|
|
326
|
+
},
|
|
327
|
+
navigator: { disposition: "no-advice" },
|
|
328
|
+
artifacts,
|
|
329
|
+
runId: input.runId,
|
|
330
|
+
autoResumeCount: input.autoResumeAttempts,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
29
334
|
export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(options: {
|
|
30
|
-
admitted: {
|
|
335
|
+
admitted: {
|
|
336
|
+
sessionFile: string;
|
|
337
|
+
runDirectory: string;
|
|
338
|
+
/** Identity for the loop-owned typed failure terminal (dispatch-exception exhaustion). */
|
|
339
|
+
role: TerminalRoleName;
|
|
340
|
+
runId: string;
|
|
341
|
+
};
|
|
31
342
|
io: CliIo;
|
|
32
343
|
/**
|
|
33
344
|
* Effective ceiling (#422), resolved by the caller before the loop; never re-read
|
|
@@ -47,6 +358,10 @@ export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(
|
|
|
47
358
|
let autoResumeAttempts = 0;
|
|
48
359
|
let isFirst = true;
|
|
49
360
|
let currentExtraArgs = options.buildInitialArgs();
|
|
361
|
+
let dispatchOrdinal = 0;
|
|
362
|
+
let lastThrownError: unknown;
|
|
363
|
+
let everyAttemptThrew = true;
|
|
364
|
+
const retainedErrorFiles: string[] = [];
|
|
50
365
|
|
|
51
366
|
while (true) {
|
|
52
367
|
let lease: RunWriterLease;
|
|
@@ -62,29 +377,99 @@ export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(
|
|
|
62
377
|
throw error;
|
|
63
378
|
}
|
|
64
379
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
380
|
+
let result: T | undefined;
|
|
381
|
+
try {
|
|
382
|
+
result = await options.dispatch(currentExtraArgs, lease, isFirst, dummyIo);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
// Owner 2026-08-23: 「出了异常,就原地记录错误信息,然后重试。」
|
|
385
|
+
// Retain the whole exception in place (per-attempt full file + dossier
|
|
386
|
+
// pointer); recording failure must not break the retry path (PR #418
|
|
387
|
+
// diagnostic-sink-isolation precedent). The dispatcher owns lease release
|
|
388
|
+
// in its own finally, so the retry round starts with the lock free.
|
|
389
|
+
lastThrownError = error;
|
|
390
|
+
const attempt = dispatchOrdinal;
|
|
391
|
+
try {
|
|
392
|
+
// Track the file as soon as it is durably written (#426 review):
|
|
393
|
+
// a pointer-stage failure comes back separately (pointerError) and must
|
|
394
|
+
// never orphan the retained file (#426 fix_now #5).
|
|
395
|
+
const { file, pointerError } = await retainDispatchError(options.admitted, attempt, error);
|
|
396
|
+
retainedErrorFiles.push(file);
|
|
397
|
+
options.io.stderr(
|
|
398
|
+
`dispatch attempt ${attempt} threw (${describeErrorIdentity(error)}); full error retained at ${file}\n`,
|
|
399
|
+
);
|
|
400
|
+
if (pointerError !== undefined) {
|
|
401
|
+
options.io.stderr(
|
|
402
|
+
`dispatch error retention failed (best-effort continue): ${describeErrorIdentity(pointerError)}\n`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
} catch (retentionError) {
|
|
406
|
+
options.io.stderr(
|
|
407
|
+
`dispatch error retention failed (best-effort continue): ${describeErrorIdentity(retentionError)}\n`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
70
410
|
}
|
|
411
|
+
dispatchOrdinal += 1;
|
|
71
412
|
|
|
72
|
-
|
|
73
|
-
|
|
413
|
+
if (result !== undefined) {
|
|
414
|
+
everyAttemptThrew = false;
|
|
415
|
+
const terminal = (result as { terminal?: TerminalResult }).terminal;
|
|
74
416
|
if (terminal !== undefined) {
|
|
75
|
-
|
|
76
|
-
options.io.stdout(formatTerminalResult(terminal));
|
|
417
|
+
(terminal as { autoResumeCount?: number }).autoResumeCount = autoResumeAttempts;
|
|
77
418
|
}
|
|
78
|
-
return result;
|
|
79
|
-
}
|
|
80
419
|
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
420
|
+
const lawful = terminal !== undefined && isLawfulTypedTerminalOutcome(terminal.roleOutcome);
|
|
421
|
+
if (lawful) {
|
|
422
|
+
if (terminal !== undefined) {
|
|
423
|
+
// Present lawful terminal once to real io (dummy was used inside dispatch)
|
|
424
|
+
options.io.stdout(formatTerminalResult(terminal));
|
|
425
|
+
}
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (autoResumeAttempts >= limit) {
|
|
430
|
+
if (terminal !== undefined) presentTerminal(terminal, options.io);
|
|
431
|
+
return result;
|
|
432
|
+
}
|
|
433
|
+
if (!(await isSessionPrincipalAvailable(options.admitted.sessionFile))) {
|
|
434
|
+
if (terminal !== undefined) presentTerminal(terminal, options.io);
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
// Exception path: continue through the identical budget/session gates.
|
|
439
|
+
if (autoResumeAttempts >= limit) {
|
|
440
|
+
const terminal = dispatchExceptionFailureTerminal({
|
|
441
|
+
role: options.admitted.role,
|
|
442
|
+
runId: options.admitted.runId,
|
|
443
|
+
causeError: lastThrownError,
|
|
444
|
+
errorFiles: retainedErrorFiles,
|
|
445
|
+
autoResumeAttempts,
|
|
446
|
+
endReason: "auto-resume budget exhausted",
|
|
447
|
+
everyAttemptThrew,
|
|
448
|
+
});
|
|
449
|
+
await finalizeExceptionRunBestEffort(options.admitted.runDirectory, options.io);
|
|
450
|
+
presentTerminal(terminal, options.io);
|
|
451
|
+
return {
|
|
452
|
+
exitCode: 1,
|
|
453
|
+
terminal,
|
|
454
|
+
} as T;
|
|
455
|
+
}
|
|
456
|
+
if (!(await isSessionPrincipalAvailable(options.admitted.sessionFile))) {
|
|
457
|
+
const terminal = dispatchExceptionFailureTerminal({
|
|
458
|
+
role: options.admitted.role,
|
|
459
|
+
runId: options.admitted.runId,
|
|
460
|
+
causeError: lastThrownError,
|
|
461
|
+
errorFiles: retainedErrorFiles,
|
|
462
|
+
autoResumeAttempts,
|
|
463
|
+
endReason: "session principal unavailable before further resume",
|
|
464
|
+
everyAttemptThrew,
|
|
465
|
+
});
|
|
466
|
+
await finalizeExceptionRunBestEffort(options.admitted.runDirectory, options.io);
|
|
467
|
+
presentTerminal(terminal, options.io);
|
|
468
|
+
return {
|
|
469
|
+
exitCode: 1,
|
|
470
|
+
terminal,
|
|
471
|
+
} as T;
|
|
472
|
+
}
|
|
88
473
|
}
|
|
89
474
|
|
|
90
475
|
autoResumeAttempts++;
|
|
@@ -347,7 +347,7 @@ export type RunWriterLease = {
|
|
|
347
347
|
* True error identity for diagnostics — name/code/message as-is, never a
|
|
348
348
|
* guessed label (failure-honesty constitution).
|
|
349
349
|
*/
|
|
350
|
-
function describeErrorIdentity(error: unknown): string {
|
|
350
|
+
export function describeErrorIdentity(error: unknown): string {
|
|
351
351
|
const candidate = error as { name?: unknown; code?: unknown; message?: unknown };
|
|
352
352
|
const name =
|
|
353
353
|
typeof candidate?.name === "string" && candidate.name !== ""
|