@akagilnc/pi-workflow-roles 0.1.2157 → 0.1.2169
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 +144 -51
- package/package.json +1 -1
- package/src/public-cli/auto-resume.ts +13 -33
- package/src/public-cli/coder-run.ts +1 -1
- package/src/public-cli/collector-run.ts +1 -1
- package/src/public-cli/doctor-run.ts +1 -1
- package/src/public-cli/fixer-run.ts +1 -1
- package/src/public-cli/judge-run.ts +1 -1
- package/src/public-cli/merger-run.ts +1 -1
- package/src/public-cli/reviewer-run.ts +1 -1
- package/src/public-cli/run-lifecycle.ts +47 -1
- package/src/public-cli/settlement.ts +170 -15
package/dist/public-cli/main.js
CHANGED
|
@@ -18866,7 +18866,22 @@ async function isSessionPrincipalAvailable(sessionFile) {
|
|
|
18866
18866
|
return false;
|
|
18867
18867
|
}
|
|
18868
18868
|
}
|
|
18869
|
-
|
|
18869
|
+
function describeErrorIdentity(error) {
|
|
18870
|
+
const candidate = error;
|
|
18871
|
+
const name = typeof candidate?.name === "string" && candidate.name !== "" ? candidate.name : typeof error;
|
|
18872
|
+
const code = typeof candidate?.code === "string" || typeof candidate?.code === "number" ? ` code=${String(candidate.code)}` : "";
|
|
18873
|
+
const message = typeof candidate?.message === "string" && candidate.message !== "" ? `: ${candidate.message}` : "";
|
|
18874
|
+
return `${name}${code}${message}`;
|
|
18875
|
+
}
|
|
18876
|
+
async function acquireRunWriterLease(runDirectory, onCleanupFailure) {
|
|
18877
|
+
const reportCleanupFailure = (error) => {
|
|
18878
|
+
try {
|
|
18879
|
+
onCleanupFailure?.(
|
|
18880
|
+
`writer lease lock cleanup failed (best-effort continue; stale lock resurfaces as lease-held on next acquire) at ${join10(runDirectory, WRITER_LOCK_FILE)}: ${describeErrorIdentity(error)}`
|
|
18881
|
+
);
|
|
18882
|
+
} catch {
|
|
18883
|
+
}
|
|
18884
|
+
};
|
|
18870
18885
|
const lockPath = join10(runDirectory, WRITER_LOCK_FILE);
|
|
18871
18886
|
try {
|
|
18872
18887
|
const handle = await open(lockPath, "wx");
|
|
@@ -18892,8 +18907,11 @@ async function acquireRunWriterLease(runDirectory) {
|
|
|
18892
18907
|
try {
|
|
18893
18908
|
await chmod(runDirectory, 493);
|
|
18894
18909
|
await unlink3(lockPath);
|
|
18895
|
-
} catch {
|
|
18910
|
+
} catch (retryError) {
|
|
18911
|
+
reportCleanupFailure(retryError);
|
|
18896
18912
|
}
|
|
18913
|
+
} else {
|
|
18914
|
+
reportCleanupFailure(error);
|
|
18897
18915
|
}
|
|
18898
18916
|
}
|
|
18899
18917
|
}
|
|
@@ -19886,7 +19904,8 @@ var init_navigator_invocation_identity = __esm({
|
|
|
19886
19904
|
|
|
19887
19905
|
// src/public-cli/settlement.ts
|
|
19888
19906
|
import { randomUUID } from "node:crypto";
|
|
19889
|
-
import {
|
|
19907
|
+
import { constants as fsConstants } from "node:fs";
|
|
19908
|
+
import { appendFile, lstat as lstat3, mkdir as mkdir3, open as open2, readFile as readFile9, readdir as readdir3, writeFile as writeFile5 } from "node:fs/promises";
|
|
19890
19909
|
import { dirname as dirname6, join as join11 } from "node:path";
|
|
19891
19910
|
function isChildDiagnosticFloodLine(line2) {
|
|
19892
19911
|
if (/^at\s+/.test(line2)) return true;
|
|
@@ -20200,12 +20219,24 @@ async function readBoundAuditorKnownFailure(sessionFile) {
|
|
|
20200
20219
|
}
|
|
20201
20220
|
const parentId = parentEntries.find((entry) => entry.type === "session")?.id;
|
|
20202
20221
|
if (parentId === void 0) return void 0;
|
|
20222
|
+
const RESUME_ENVELOPE = RESUME_TRANSPORT_ENVELOPE;
|
|
20223
|
+
const isResumeEnvelope = (msg) => {
|
|
20224
|
+
if (!isRecord5(msg) || msg.role !== "user") return false;
|
|
20225
|
+
const text = typeof msg.text === "string" ? msg.text : typeof msg.content === "string" ? msg.content : void 0;
|
|
20226
|
+
if (text === RESUME_ENVELOPE) return true;
|
|
20227
|
+
const content = msg.content;
|
|
20228
|
+
if (Array.isArray(content)) {
|
|
20229
|
+
return content.some((p) => isRecord5(p) && (p.text === RESUME_ENVELOPE || p.content === RESUME_ENVELOPE));
|
|
20230
|
+
}
|
|
20231
|
+
return false;
|
|
20232
|
+
};
|
|
20203
20233
|
let latestParentUserIndex = -1;
|
|
20204
20234
|
for (let i = parentEntries.length - 1; i >= 0; i -= 1) {
|
|
20205
|
-
|
|
20206
|
-
|
|
20207
|
-
|
|
20208
|
-
|
|
20235
|
+
const entry = parentEntries[i];
|
|
20236
|
+
if (entry?.type !== "message" || entry.message?.role !== "user") continue;
|
|
20237
|
+
if (isResumeEnvelope(entry.message)) continue;
|
|
20238
|
+
latestParentUserIndex = i;
|
|
20239
|
+
break;
|
|
20209
20240
|
}
|
|
20210
20241
|
const childDirectory = join11(dirname6(sessionFile), "auditor-roles");
|
|
20211
20242
|
let names;
|
|
@@ -20215,6 +20246,7 @@ async function readBoundAuditorKnownFailure(sessionFile) {
|
|
|
20215
20246
|
if (isMissingPathError2(error)) return void 0;
|
|
20216
20247
|
throw sessionReadFailure(error, "failed to read bound auditor session directory");
|
|
20217
20248
|
}
|
|
20249
|
+
const validAuditorFiles = [];
|
|
20218
20250
|
for (const file of names.filter((name) => name.endsWith(".jsonl")).sort().reverse()) {
|
|
20219
20251
|
let entries;
|
|
20220
20252
|
try {
|
|
@@ -20229,6 +20261,9 @@ async function readBoundAuditorKnownFailure(sessionFile) {
|
|
|
20229
20261
|
const attemptEntryId = typeof bindingParent?.attemptEntryId === "string" ? bindingParent.attemptEntryId : void 0;
|
|
20230
20262
|
const attemptEntryIndex = attemptEntryId === void 0 ? -1 : parentEntries.findIndex((entry) => entry.id === attemptEntryId);
|
|
20231
20263
|
if (bindingParent?.sessionId !== parentId || bindingParent.sessionFile !== sessionFile || attemptEntryIndex < latestParentUserIndex) continue;
|
|
20264
|
+
validAuditorFiles.push({ file, entries, ...attemptEntryId === void 0 ? {} : { attemptEntryId } });
|
|
20265
|
+
}
|
|
20266
|
+
for (const { entries, attemptEntryId } of validAuditorFiles) {
|
|
20232
20267
|
const stop = extractSessionProviderStop(entries);
|
|
20233
20268
|
if (stop === void 0) continue;
|
|
20234
20269
|
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
@@ -20248,6 +20283,10 @@ async function readBoundAuditorKnownFailure(sessionFile) {
|
|
|
20248
20283
|
...isRecord5(failure.details) ? { details: failure.details } : {}
|
|
20249
20284
|
};
|
|
20250
20285
|
}
|
|
20286
|
+
}
|
|
20287
|
+
for (const { entries } of validAuditorFiles) {
|
|
20288
|
+
const stop = extractSessionProviderStop(entries);
|
|
20289
|
+
if (stop === void 0) continue;
|
|
20251
20290
|
const primary = knownFailureFromProviderStop(stop);
|
|
20252
20291
|
return {
|
|
20253
20292
|
...primary,
|
|
@@ -20981,20 +21020,74 @@ async function ensureAuditEvidenceDirectory(runDirectory) {
|
|
|
20981
21020
|
}
|
|
20982
21021
|
return artifactsDir;
|
|
20983
21022
|
}
|
|
21023
|
+
async function appendRunAttemptHistory(admitted, outcome) {
|
|
21024
|
+
const entries = await readBoundSessionEntries(admitted.sessionFile);
|
|
21025
|
+
let parentId = null;
|
|
21026
|
+
let priorEntries = 0;
|
|
21027
|
+
for (const entry of entries) {
|
|
21028
|
+
if (typeof entry.id === "string" && entry.type !== "session") parentId = entry.id;
|
|
21029
|
+
if (entry.type === "custom" && entry.customType === ATTEMPT_HISTORY_ENTRY_TYPE) {
|
|
21030
|
+
priorEntries += 1;
|
|
21031
|
+
}
|
|
21032
|
+
}
|
|
21033
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
21034
|
+
const line2 = `${JSON.stringify({
|
|
21035
|
+
type: "custom",
|
|
21036
|
+
customType: ATTEMPT_HISTORY_ENTRY_TYPE,
|
|
21037
|
+
data: {
|
|
21038
|
+
sequence: priorEntries + 1,
|
|
21039
|
+
role: admitted.role,
|
|
21040
|
+
runId: admitted.runId,
|
|
21041
|
+
recordedAt: timestamp2,
|
|
21042
|
+
outcome
|
|
21043
|
+
},
|
|
21044
|
+
id: randomUUID(),
|
|
21045
|
+
parentId,
|
|
21046
|
+
timestamp: timestamp2
|
|
21047
|
+
})}
|
|
21048
|
+
`;
|
|
21049
|
+
await appendFile(admitted.sessionFile, line2, "utf8");
|
|
21050
|
+
}
|
|
20984
21051
|
async function publishComplianceAuditIncompleteEvidence(admitted, outcome) {
|
|
21052
|
+
await appendRunAttemptHistory(admitted, outcome);
|
|
21053
|
+
if (typeof fsConstants.O_NOFOLLOW !== "number" || typeof fsConstants.O_NONBLOCK !== "number") {
|
|
21054
|
+
throw auditArtifactPublicationError(
|
|
21055
|
+
"audit evidence publication requires O_NOFOLLOW|O_NONBLOCK open-flag support (anti-symlink/anti-planted protection must not be silently dropped); refusing to publish",
|
|
21056
|
+
"ENOSYS"
|
|
21057
|
+
);
|
|
21058
|
+
}
|
|
20985
21059
|
const artifactsDir = await ensureAuditEvidenceDirectory(admitted.runDirectory);
|
|
20986
21060
|
const evidencePath = join11(artifactsDir, "audit-incomplete.json");
|
|
21061
|
+
let existing;
|
|
20987
21062
|
try {
|
|
20988
|
-
|
|
20989
|
-
throw auditArtifactPublicationError(
|
|
20990
|
-
existing.isSymbolicLink() ? "audit evidence destination is a symlink" : "audit evidence destination collision",
|
|
20991
|
-
existing.isSymbolicLink() ? "ELOOP" : "EEXIST"
|
|
20992
|
-
);
|
|
21063
|
+
existing = await lstat3(evidencePath);
|
|
20993
21064
|
} catch (error) {
|
|
20994
21065
|
if (!isMissingPathError2(error)) throw error;
|
|
20995
21066
|
}
|
|
20996
|
-
|
|
21067
|
+
if (existing?.isSymbolicLink()) {
|
|
21068
|
+
throw auditArtifactPublicationError(
|
|
21069
|
+
"audit evidence destination is a symlink",
|
|
21070
|
+
"ELOOP"
|
|
21071
|
+
);
|
|
21072
|
+
}
|
|
21073
|
+
if (existing && !existing.isFile()) {
|
|
21074
|
+
throw auditArtifactPublicationError(
|
|
21075
|
+
"audit evidence destination is not a regular file",
|
|
21076
|
+
"EEXIST"
|
|
21077
|
+
);
|
|
21078
|
+
}
|
|
21079
|
+
const handle = await open2(
|
|
21080
|
+
evidencePath,
|
|
21081
|
+
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_TRUNC | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK,
|
|
21082
|
+
384
|
|
21083
|
+
);
|
|
20997
21084
|
try {
|
|
21085
|
+
if (!(await handle.stat()).isFile()) {
|
|
21086
|
+
throw auditArtifactPublicationError(
|
|
21087
|
+
"audit evidence destination is not a regular file",
|
|
21088
|
+
"EEXIST"
|
|
21089
|
+
);
|
|
21090
|
+
}
|
|
20998
21091
|
await handle.writeFile(`${JSON.stringify(outcome, null, 2)}
|
|
20999
21092
|
`, "utf8");
|
|
21000
21093
|
await handle.sync();
|
|
@@ -21247,6 +21340,7 @@ async function extractNavigatorFactFromAdmittedSession(admitted) {
|
|
|
21247
21340
|
}
|
|
21248
21341
|
}
|
|
21249
21342
|
async function publishJudgeArtifacts(admitted, roleOutcome, sessionDirectory) {
|
|
21343
|
+
await appendRunAttemptHistory(admitted, roleOutcome);
|
|
21250
21344
|
const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
|
|
21251
21345
|
const reportPath = join11(artifactsDir, "report.json");
|
|
21252
21346
|
const evidencePath = join11(artifactsDir, "evidence.json");
|
|
@@ -21291,6 +21385,7 @@ async function publishJudgeArtifacts(admitted, roleOutcome, sessionDirectory) {
|
|
|
21291
21385
|
];
|
|
21292
21386
|
}
|
|
21293
21387
|
async function publishCoderArtifacts(admitted, roleOutcome, sessionDirectory, options = {}) {
|
|
21388
|
+
await appendRunAttemptHistory(admitted, roleOutcome);
|
|
21294
21389
|
const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
|
|
21295
21390
|
const reportPath = join11(artifactsDir, "report.json");
|
|
21296
21391
|
const evidencePath = join11(artifactsDir, "evidence.json");
|
|
@@ -21460,6 +21555,7 @@ function extractFixerMethodInvocations(entries, options) {
|
|
|
21460
21555
|
return Object.freeze(observed);
|
|
21461
21556
|
}
|
|
21462
21557
|
async function publishFixerArtifacts(admitted, roleOutcome, sessionDirectory, options) {
|
|
21558
|
+
await appendRunAttemptHistory(admitted, roleOutcome);
|
|
21463
21559
|
const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
|
|
21464
21560
|
const reportPath = join11(artifactsDir, "report.json");
|
|
21465
21561
|
const evidencePath = join11(artifactsDir, "evidence.json");
|
|
@@ -21571,6 +21667,7 @@ async function settleLawfulFixerTerminalResult(admitted, options) {
|
|
|
21571
21667
|
};
|
|
21572
21668
|
}
|
|
21573
21669
|
async function publishCollectorArtifacts(admitted, roleOutcome, sessionDirectory, options = {}) {
|
|
21670
|
+
await appendRunAttemptHistory(admitted, roleOutcome);
|
|
21574
21671
|
const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
|
|
21575
21672
|
const reportPath = join11(artifactsDir, "report.json");
|
|
21576
21673
|
const evidencePath = join11(artifactsDir, "evidence.json");
|
|
@@ -21690,6 +21787,7 @@ async function trySettleCollectorTerminalResult(admitted) {
|
|
|
21690
21787
|
return settleLawfulCollectorTerminalResult(admitted);
|
|
21691
21788
|
}
|
|
21692
21789
|
async function publishDoctorArtifacts(admitted, roleOutcome, sessionDirectory, options = {}) {
|
|
21790
|
+
await appendRunAttemptHistory(admitted, roleOutcome);
|
|
21693
21791
|
const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
|
|
21694
21792
|
const reportPath = join11(artifactsDir, "report.json");
|
|
21695
21793
|
const evidencePath = join11(artifactsDir, "evidence.json");
|
|
@@ -21857,6 +21955,7 @@ function extractReviewerMethodInvocations(entries, options) {
|
|
|
21857
21955
|
return Object.freeze(observed);
|
|
21858
21956
|
}
|
|
21859
21957
|
async function publishReviewerArtifacts(admitted, roleOutcome, sessionDirectory, options) {
|
|
21958
|
+
await appendRunAttemptHistory(admitted, roleOutcome);
|
|
21860
21959
|
const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
|
|
21861
21960
|
const reportPath = join11(artifactsDir, "report.json");
|
|
21862
21961
|
const evidencePath = join11(artifactsDir, "evidence.json");
|
|
@@ -22025,6 +22124,7 @@ function extractMergerMethodInvocations(entries, options) {
|
|
|
22025
22124
|
return Object.freeze(observed);
|
|
22026
22125
|
}
|
|
22027
22126
|
async function publishMergerArtifacts(admitted, roleOutcome, sessionDirectory, options) {
|
|
22127
|
+
await appendRunAttemptHistory(admitted, roleOutcome);
|
|
22028
22128
|
const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
|
|
22029
22129
|
const reportPath = join11(artifactsDir, "report.json");
|
|
22030
22130
|
const evidencePath = join11(artifactsDir, "evidence.json");
|
|
@@ -22246,6 +22346,15 @@ async function publishFailureArtifacts(admitted, failure) {
|
|
|
22246
22346
|
admitted.runDirectory
|
|
22247
22347
|
);
|
|
22248
22348
|
const priorIssues = baseAttempt === void 0 ? [] : [baseAttempt];
|
|
22349
|
+
try {
|
|
22350
|
+
await appendRunAttemptHistory(admitted, {
|
|
22351
|
+
kind: "failure",
|
|
22352
|
+
role: admitted.role,
|
|
22353
|
+
...failure
|
|
22354
|
+
});
|
|
22355
|
+
} catch (error) {
|
|
22356
|
+
priorIssues.push(publicationAttemptFromError(admitted.sessionFile, error));
|
|
22357
|
+
}
|
|
22249
22358
|
const underArtifacts = baseDir === join11(admitted.runDirectory, "artifacts");
|
|
22250
22359
|
const uniqueFallbackDirs = uniqueFailureFallbackDirs(
|
|
22251
22360
|
admitted.runDirectory,
|
|
@@ -22442,7 +22551,7 @@ function presentFailureTerminal(terminal, io) {
|
|
|
22442
22551
|
}));
|
|
22443
22552
|
}
|
|
22444
22553
|
}
|
|
22445
|
-
var CONCISE_DIAGNOSTIC_MAX_CHARS, COLLECTOR_INFRASTRUCTURE_TOOLS, COLLECTOR_INFRASTRUCTURE_FAILURE_SPEC, ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC;
|
|
22554
|
+
var CONCISE_DIAGNOSTIC_MAX_CHARS, COLLECTOR_INFRASTRUCTURE_TOOLS, COLLECTOR_INFRASTRUCTURE_FAILURE_SPEC, ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC, ATTEMPT_HISTORY_ENTRY_TYPE;
|
|
22446
22555
|
var init_settlement = __esm({
|
|
22447
22556
|
"src/public-cli/settlement.ts"() {
|
|
22448
22557
|
"use strict";
|
|
@@ -22485,10 +22594,18 @@ var init_settlement = __esm({
|
|
|
22485
22594
|
cause: "output",
|
|
22486
22595
|
identityName: "EngineDetourInfrastructureError"
|
|
22487
22596
|
};
|
|
22597
|
+
ATTEMPT_HISTORY_ENTRY_TYPE = "ak_run_attempt_history";
|
|
22488
22598
|
}
|
|
22489
22599
|
});
|
|
22490
22600
|
|
|
22491
22601
|
// src/public-cli/auto-resume.ts
|
|
22602
|
+
function presentTerminal(terminal, io) {
|
|
22603
|
+
if (terminal.roleOutcome.kind === "failure" || terminal.roleOutcome.kind === "no_receipt") {
|
|
22604
|
+
presentFailureTerminal(terminal, io);
|
|
22605
|
+
} else {
|
|
22606
|
+
io.stdout(formatTerminalResult(terminal));
|
|
22607
|
+
}
|
|
22608
|
+
}
|
|
22492
22609
|
async function runWithAutoResumeLoop(options) {
|
|
22493
22610
|
let autoResumeAttempts = 0;
|
|
22494
22611
|
let isFirst = true;
|
|
@@ -22496,7 +22613,10 @@ async function runWithAutoResumeLoop(options) {
|
|
|
22496
22613
|
while (true) {
|
|
22497
22614
|
let lease;
|
|
22498
22615
|
try {
|
|
22499
|
-
lease = await acquireRunWriterLease(
|
|
22616
|
+
lease = await acquireRunWriterLease(
|
|
22617
|
+
options.admitted.runDirectory,
|
|
22618
|
+
(diagnostic) => options.io.stderr(diagnostic)
|
|
22619
|
+
);
|
|
22500
22620
|
} catch (error) {
|
|
22501
22621
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
22502
22622
|
presentStructuralRejection(error, options.io);
|
|
@@ -22516,39 +22636,12 @@ async function runWithAutoResumeLoop(options) {
|
|
|
22516
22636
|
}
|
|
22517
22637
|
return result2;
|
|
22518
22638
|
}
|
|
22519
|
-
if (terminal !== void 0 && (terminal.roleOutcome.kind === "incomplete" || terminal.roleOutcome.kind === "audit_incomplete")) {
|
|
22520
|
-
if (terminal.roleOutcome.kind === "audit_incomplete") {
|
|
22521
|
-
options.io.stdout(formatTerminalResult(terminal));
|
|
22522
|
-
} else {
|
|
22523
|
-
options.io.stdout(formatTerminalResult(terminal));
|
|
22524
|
-
}
|
|
22525
|
-
return result2;
|
|
22526
|
-
}
|
|
22527
|
-
if (terminal !== void 0 && terminal.roleOutcome.kind === "failure") {
|
|
22528
|
-
const terminalJson = JSON.stringify(terminal);
|
|
22529
|
-
if (terminalJson.includes("retentionFailure") || terminalJson.includes("ComplianceResponseRetentionError")) {
|
|
22530
|
-
presentFailureTerminal(terminal, options.io);
|
|
22531
|
-
return result2;
|
|
22532
|
-
}
|
|
22533
|
-
}
|
|
22534
22639
|
if (autoResumeAttempts >= AUTO_RESUME_LIMIT) {
|
|
22535
|
-
if (terminal !== void 0)
|
|
22536
|
-
if (terminal.roleOutcome.kind === "failure" || terminal.roleOutcome.kind === "no_receipt") {
|
|
22537
|
-
presentFailureTerminal(terminal, options.io);
|
|
22538
|
-
} else {
|
|
22539
|
-
options.io.stdout(formatTerminalResult(terminal));
|
|
22540
|
-
}
|
|
22541
|
-
}
|
|
22640
|
+
if (terminal !== void 0) presentTerminal(terminal, options.io);
|
|
22542
22641
|
return result2;
|
|
22543
22642
|
}
|
|
22544
22643
|
if (!await isSessionPrincipalAvailable(options.admitted.sessionFile)) {
|
|
22545
|
-
if (terminal !== void 0)
|
|
22546
|
-
if (terminal.roleOutcome.kind === "failure" || terminal.roleOutcome.kind === "no_receipt") {
|
|
22547
|
-
presentFailureTerminal(terminal, options.io);
|
|
22548
|
-
} else {
|
|
22549
|
-
options.io.stdout(formatTerminalResult(terminal));
|
|
22550
|
-
}
|
|
22551
|
-
}
|
|
22644
|
+
if (terminal !== void 0) presentTerminal(terminal, options.io);
|
|
22552
22645
|
return result2;
|
|
22553
22646
|
}
|
|
22554
22647
|
autoResumeAttempts++;
|
|
@@ -22891,7 +22984,7 @@ async function runPublicCoderResume(argv, env, io) {
|
|
|
22891
22984
|
const { admitted } = loaded;
|
|
22892
22985
|
let lease;
|
|
22893
22986
|
try {
|
|
22894
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
22987
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
22895
22988
|
} catch (error) {
|
|
22896
22989
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
22897
22990
|
io.stderr(formatCliDiagnostic(error.message));
|
|
@@ -23155,7 +23248,7 @@ async function runPublicCollector(argv, env, io, parseCollectorArgv2) {
|
|
|
23155
23248
|
await markRunAdmitted(admitted);
|
|
23156
23249
|
let lease;
|
|
23157
23250
|
try {
|
|
23158
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
23251
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
23159
23252
|
} catch (error) {
|
|
23160
23253
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
23161
23254
|
presentStructuralRejection(error, io);
|
|
@@ -23390,7 +23483,7 @@ async function runPublicDoctor(argv, env, io, parseDoctorArgv2) {
|
|
|
23390
23483
|
await markRunAdmitted(admitted);
|
|
23391
23484
|
let lease;
|
|
23392
23485
|
try {
|
|
23393
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
23486
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
23394
23487
|
} catch (error) {
|
|
23395
23488
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
23396
23489
|
presentStructuralRejection(error, io);
|
|
@@ -23775,7 +23868,7 @@ async function runPublicFixerResume(argv, env, io) {
|
|
|
23775
23868
|
const { admitted } = loaded;
|
|
23776
23869
|
let lease;
|
|
23777
23870
|
try {
|
|
23778
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
23871
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
23779
23872
|
} catch (error) {
|
|
23780
23873
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
23781
23874
|
io.stderr(formatCliDiagnostic(error.message));
|
|
@@ -24137,7 +24230,7 @@ async function runPublicResume(argv, env, io) {
|
|
|
24137
24230
|
const { admitted } = loaded;
|
|
24138
24231
|
let lease;
|
|
24139
24232
|
try {
|
|
24140
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
24233
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
24141
24234
|
} catch (error) {
|
|
24142
24235
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
24143
24236
|
io.stderr(formatCliDiagnostic(error.message));
|
|
@@ -24586,7 +24679,7 @@ async function runPublicMergerResume(argv, env, io) {
|
|
|
24586
24679
|
const { admitted } = loaded;
|
|
24587
24680
|
let lease;
|
|
24588
24681
|
try {
|
|
24589
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
24682
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
24590
24683
|
} catch (error) {
|
|
24591
24684
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
24592
24685
|
io.stderr(formatCliDiagnostic(error.message));
|
|
@@ -25002,7 +25095,7 @@ async function runPublicReviewerResume(argv, env, io) {
|
|
|
25002
25095
|
const { admitted } = loaded;
|
|
25003
25096
|
let lease;
|
|
25004
25097
|
try {
|
|
25005
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
25098
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
25006
25099
|
} catch (error) {
|
|
25007
25100
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
25008
25101
|
io.stderr(formatCliDiagnostic(error.message));
|
package/package.json
CHANGED
|
@@ -10,6 +10,14 @@ import type { CliIo } from "./cli-io.ts";
|
|
|
10
10
|
|
|
11
11
|
const dummyIo: CliIo = { stdout: () => {}, stderr: () => {} };
|
|
12
12
|
|
|
13
|
+
function presentTerminal(terminal: TerminalResult, io: CliIo): void {
|
|
14
|
+
if (terminal.roleOutcome.kind === "failure" || terminal.roleOutcome.kind === "no_receipt") {
|
|
15
|
+
presentFailureTerminal(terminal, io);
|
|
16
|
+
} else {
|
|
17
|
+
io.stdout(formatTerminalResult(terminal));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
export type AutoResumeDispatchResult = {
|
|
14
22
|
exitCode: number;
|
|
15
23
|
terminal?: TerminalResult;
|
|
@@ -29,7 +37,9 @@ export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(
|
|
|
29
37
|
while (true) {
|
|
30
38
|
let lease: RunWriterLease;
|
|
31
39
|
try {
|
|
32
|
-
lease = await acquireRunWriterLease(options.admitted.runDirectory)
|
|
40
|
+
lease = await acquireRunWriterLease(options.admitted.runDirectory, (diagnostic) =>
|
|
41
|
+
options.io.stderr(diagnostic),
|
|
42
|
+
);
|
|
33
43
|
} catch (error) {
|
|
34
44
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
35
45
|
presentStructuralRejection(error, options.io);
|
|
@@ -54,42 +64,12 @@ export async function runWithAutoResumeLoop<T extends AutoResumeDispatchResult>(
|
|
|
54
64
|
return result;
|
|
55
65
|
}
|
|
56
66
|
|
|
57
|
-
// Deterministic incomplete/audit_incomplete (merger/collector/judge) would duplicate callId bindings on retry and lose settlement
|
|
58
|
-
if (terminal !== undefined && (terminal.roleOutcome.kind === "incomplete" || terminal.roleOutcome.kind === "audit_incomplete")) {
|
|
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
67
|
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
|
-
}
|
|
68
|
+
if (terminal !== undefined) presentTerminal(terminal, options.io);
|
|
83
69
|
return result;
|
|
84
70
|
}
|
|
85
71
|
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
|
-
}
|
|
72
|
+
if (terminal !== undefined) presentTerminal(terminal, options.io);
|
|
93
73
|
return result;
|
|
94
74
|
}
|
|
95
75
|
|
|
@@ -548,7 +548,7 @@ export async function runPublicCoderResume(
|
|
|
548
548
|
|
|
549
549
|
let lease: RunWriterLease;
|
|
550
550
|
try {
|
|
551
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
551
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
552
552
|
} catch (error) {
|
|
553
553
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
554
554
|
// 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);
|
|
@@ -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);
|
|
@@ -567,7 +567,7 @@ export async function runPublicFixerResume(
|
|
|
567
567
|
|
|
568
568
|
let lease: RunWriterLease;
|
|
569
569
|
try {
|
|
570
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
570
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
571
571
|
} catch (error) {
|
|
572
572
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
573
573
|
io.stderr(formatCliDiagnostic(error.message));
|
|
@@ -536,7 +536,7 @@ export async function runPublicResume(
|
|
|
536
536
|
|
|
537
537
|
let lease: RunWriterLease;
|
|
538
538
|
try {
|
|
539
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
539
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
540
540
|
} catch (error) {
|
|
541
541
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
542
542
|
// Concurrent resume: reject without second writer or dispatch.
|
|
@@ -648,7 +648,7 @@ export async function runPublicMergerResume(
|
|
|
648
648
|
|
|
649
649
|
let lease: RunWriterLease;
|
|
650
650
|
try {
|
|
651
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
651
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
652
652
|
} catch (error) {
|
|
653
653
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
654
654
|
io.stderr(formatCliDiagnostic(error.message));
|
|
@@ -587,7 +587,7 @@ export async function runPublicReviewerResume(
|
|
|
587
587
|
|
|
588
588
|
let lease: RunWriterLease;
|
|
589
589
|
try {
|
|
590
|
-
lease = await acquireRunWriterLease(admitted.runDirectory);
|
|
590
|
+
lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
|
|
591
591
|
} catch (error) {
|
|
592
592
|
if (error instanceof RunWriterLeaseHeldError) {
|
|
593
593
|
io.stderr(formatCliDiagnostic(error.message));
|
|
@@ -340,13 +340,52 @@ export type RunWriterLease = {
|
|
|
340
340
|
release(): Promise<void>;
|
|
341
341
|
};
|
|
342
342
|
|
|
343
|
+
/**
|
|
344
|
+
* True error identity for diagnostics — name/code/message as-is, never a
|
|
345
|
+
* guessed label (failure-honesty constitution).
|
|
346
|
+
*/
|
|
347
|
+
function describeErrorIdentity(error: unknown): string {
|
|
348
|
+
const candidate = error as { name?: unknown; code?: unknown; message?: unknown };
|
|
349
|
+
const name =
|
|
350
|
+
typeof candidate?.name === "string" && candidate.name !== ""
|
|
351
|
+
? candidate.name
|
|
352
|
+
: typeof error;
|
|
353
|
+
const code =
|
|
354
|
+
typeof candidate?.code === "string" || typeof candidate?.code === "number"
|
|
355
|
+
? ` code=${String(candidate.code)}`
|
|
356
|
+
: "";
|
|
357
|
+
const message =
|
|
358
|
+
typeof candidate?.message === "string" && candidate.message !== ""
|
|
359
|
+
? `: ${candidate.message}`
|
|
360
|
+
: "";
|
|
361
|
+
return `${name}${code}${message}`;
|
|
362
|
+
}
|
|
363
|
+
|
|
343
364
|
/**
|
|
344
365
|
* Acquire the one-writer lease for a Role run. Concurrent acquire rejects
|
|
345
366
|
* without dispatch. Exclusive create — no second writer.
|
|
367
|
+
*
|
|
368
|
+
* `onCleanupFailure` receives a non-terminal diagnostic line when release-time
|
|
369
|
+
* lock cleanup fails. Release stays best-effort (a stale lock resurfaces as
|
|
370
|
+
* RunWriterLeaseHeldError on next acquire), but the true error identity must
|
|
371
|
+
* still land somewhere observable — silent swallowing is forbidden.
|
|
346
372
|
*/
|
|
347
373
|
export async function acquireRunWriterLease(
|
|
348
374
|
runDirectory: string,
|
|
375
|
+
onCleanupFailure?: (diagnostic: string) => void,
|
|
349
376
|
): Promise<RunWriterLease> {
|
|
377
|
+
const reportCleanupFailure = (error: unknown): void => {
|
|
378
|
+
// Sink isolation: a throwing onCleanupFailure must not propagate through
|
|
379
|
+
// release() — release stays best-effort by contract. The true cleanup
|
|
380
|
+
// cause has already been handed to the sink as its argument.
|
|
381
|
+
try {
|
|
382
|
+
onCleanupFailure?.(
|
|
383
|
+
`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)}`,
|
|
384
|
+
);
|
|
385
|
+
} catch {
|
|
386
|
+
// diagnostic-sink failure is itself best-effort; never break release().
|
|
387
|
+
}
|
|
388
|
+
};
|
|
350
389
|
const lockPath = join(runDirectory, WRITER_LOCK_FILE);
|
|
351
390
|
try {
|
|
352
391
|
const handle = await open(lockPath, "wx");
|
|
@@ -371,7 +410,14 @@ export async function acquireRunWriterLease(
|
|
|
371
410
|
try {
|
|
372
411
|
await chmod(runDirectory, 0o755);
|
|
373
412
|
await unlink(lockPath);
|
|
374
|
-
} catch {
|
|
413
|
+
} catch (retryError) {
|
|
414
|
+
// best-effort cleanup: stale lock will surface as lease-held on next acquire (exit 2),
|
|
415
|
+
// but the true chmod/unlink cause must be recorded, not swallowed.
|
|
416
|
+
reportCleanupFailure(retryError);
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
// non-EACCES unlink failure is best-effort settlement cleanup; record true cause.
|
|
420
|
+
reportCleanupFailure(error);
|
|
375
421
|
}
|
|
376
422
|
}
|
|
377
423
|
},
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Controlled failures and audit human decisions settle here without washing causes.
|
|
5
5
|
*/
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import {
|
|
7
|
+
import { constants as fsConstants } from "node:fs";
|
|
8
|
+
import { appendFile, lstat, mkdir, open, readFile, readdir, writeFile } from "node:fs/promises";
|
|
8
9
|
import { dirname, join } from "node:path";
|
|
9
10
|
|
|
10
11
|
import { isAuditEscalationResult } from "../audit-escalation.ts";
|
|
@@ -14,6 +15,7 @@ import { JUDGE_AUDIT_TOOL_NAME } from "../judge-auditor.ts";
|
|
|
14
15
|
import { REVIEWER_AUDIT_TOOL_NAME } from "../reviewer-auditor.ts";
|
|
15
16
|
import { knownFailureFromProviderStop, type ExplicitInternalKnownFailure, readReviewerDispatchRejection } from "./explicit-internal.ts";
|
|
16
17
|
import {
|
|
18
|
+
RESUME_TRANSPORT_ENVELOPE,
|
|
17
19
|
isV1ResumableProvider,
|
|
18
20
|
readLatestTypedProviderHttpObservation,
|
|
19
21
|
readTypedHttp429Observation,
|
|
@@ -725,12 +727,24 @@ export async function readBoundAuditorKnownFailure(
|
|
|
725
727
|
}
|
|
726
728
|
const parentId = parentEntries.find((entry) => entry.type === "session")?.id;
|
|
727
729
|
if (parentId === undefined) return undefined;
|
|
730
|
+
const RESUME_ENVELOPE = RESUME_TRANSPORT_ENVELOPE;
|
|
731
|
+
const isResumeEnvelope = (msg: unknown): boolean => {
|
|
732
|
+
if (!isRecord(msg) || msg.role !== "user") return false;
|
|
733
|
+
const text = typeof msg.text === "string" ? msg.text : typeof (msg as { content?: unknown }).content === "string" ? (msg as { content: string }).content : undefined;
|
|
734
|
+
if (text === RESUME_ENVELOPE) return true;
|
|
735
|
+
const content = (msg as { content?: unknown }).content;
|
|
736
|
+
if (Array.isArray(content)) {
|
|
737
|
+
return content.some((p) => isRecord(p) && (p.text === RESUME_ENVELOPE || p.content === RESUME_ENVELOPE));
|
|
738
|
+
}
|
|
739
|
+
return false;
|
|
740
|
+
};
|
|
728
741
|
let latestParentUserIndex = -1;
|
|
729
742
|
for (let i = parentEntries.length - 1; i >= 0; i -= 1) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
743
|
+
const entry = parentEntries[i];
|
|
744
|
+
if (entry?.type !== "message" || entry.message?.role !== "user") continue;
|
|
745
|
+
if (isResumeEnvelope(entry.message)) continue;
|
|
746
|
+
latestParentUserIndex = i;
|
|
747
|
+
break;
|
|
734
748
|
}
|
|
735
749
|
const childDirectory = join(dirname(sessionFile), "auditor-roles");
|
|
736
750
|
let names: string[];
|
|
@@ -740,6 +754,12 @@ export async function readBoundAuditorKnownFailure(
|
|
|
740
754
|
if (isMissingPathError(error)) return undefined;
|
|
741
755
|
throw sessionReadFailure(error, "failed to read bound auditor session directory");
|
|
742
756
|
}
|
|
757
|
+
// Auto-resume seam (owner A): stale check must ignore resume envelope and
|
|
758
|
+
// prioritize retention. Previous `attemptEntryIndex < latest` discarded the
|
|
759
|
+
// first attempt's child after resume advanced latest, losing retentionFailure
|
|
760
|
+
// when retry had no compliance entry. Fix: ignore envelope for staleness and
|
|
761
|
+
// prefer any valid compliance failure before falling back to primary.
|
|
762
|
+
const validAuditorFiles: Array<{ file: string; entries: SessionEntry[]; attemptEntryId?: string }> = [];
|
|
743
763
|
for (const file of names.filter((name) => name.endsWith(".jsonl")).sort().reverse()) {
|
|
744
764
|
let entries: SessionEntry[];
|
|
745
765
|
try {
|
|
@@ -754,7 +774,10 @@ export async function readBoundAuditorKnownFailure(
|
|
|
754
774
|
const attemptEntryId = typeof bindingParent?.attemptEntryId === "string" ? bindingParent.attemptEntryId : undefined;
|
|
755
775
|
const attemptEntryIndex = attemptEntryId === undefined ? -1 : parentEntries.findIndex((entry) => entry.id === attemptEntryId);
|
|
756
776
|
if (bindingParent?.sessionId !== parentId || bindingParent.sessionFile !== sessionFile || attemptEntryIndex < latestParentUserIndex) continue;
|
|
757
|
-
|
|
777
|
+
validAuditorFiles.push({ file, entries, ...(attemptEntryId === undefined ? {} : { attemptEntryId }) });
|
|
778
|
+
}
|
|
779
|
+
// Prefer compliance failure (retention) from any valid attempt, newest first.
|
|
780
|
+
for (const { entries, attemptEntryId } of validAuditorFiles) {
|
|
758
781
|
const stop = extractSessionProviderStop(entries);
|
|
759
782
|
if (stop === undefined) continue;
|
|
760
783
|
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
@@ -774,6 +797,11 @@ export async function readBoundAuditorKnownFailure(
|
|
|
774
797
|
...(isRecord(failure.details) ? { details: failure.details } : {}),
|
|
775
798
|
};
|
|
776
799
|
}
|
|
800
|
+
}
|
|
801
|
+
// No compliance failure: fall back to most recent provider stop (auto-resume latest attempt).
|
|
802
|
+
for (const { entries } of validAuditorFiles) {
|
|
803
|
+
const stop = extractSessionProviderStop(entries);
|
|
804
|
+
if (stop === undefined) continue;
|
|
777
805
|
const primary = knownFailureFromProviderStop(stop)!;
|
|
778
806
|
return {
|
|
779
807
|
...primary,
|
|
@@ -1852,26 +1880,131 @@ async function ensureAuditEvidenceDirectory(runDirectory: string): Promise<strin
|
|
|
1852
1880
|
return artifactsDir;
|
|
1853
1881
|
}
|
|
1854
1882
|
|
|
1855
|
-
/**
|
|
1883
|
+
/**
|
|
1884
|
+
* #419 per-attempt process history. 史必追加,指针可覆盖;指针可以覆盖的前提是史已落。
|
|
1885
|
+
* Reuses the run session principal's append-only JSONL custom-entry shape
|
|
1886
|
+
* (plain custom entries are state records and never enter LLM context), so no
|
|
1887
|
+
* second ledger mechanism is introduced.
|
|
1888
|
+
*/
|
|
1889
|
+
export const ATTEMPT_HISTORY_ENTRY_TYPE = "ak_run_attempt_history" as const;
|
|
1890
|
+
|
|
1891
|
+
/** Complete per-attempt result as recorded in the appended history. */
|
|
1892
|
+
type AttemptHistoryOutcome =
|
|
1893
|
+
| TerminalRoleOutcome
|
|
1894
|
+
| ({ kind: "failure"; role: string } & ControlledFailure);
|
|
1895
|
+
|
|
1896
|
+
type AttemptHistorySource = {
|
|
1897
|
+
readonly role: string;
|
|
1898
|
+
readonly runId: string;
|
|
1899
|
+
readonly sessionFile: string;
|
|
1900
|
+
};
|
|
1901
|
+
|
|
1902
|
+
/**
|
|
1903
|
+
* Append one attempt's complete result to the run's session principal.
|
|
1904
|
+
* Append failure throws — callers must not overwrite a pointer artifact when
|
|
1905
|
+
* the history entry backing the overwrite did not land (fail closed).
|
|
1906
|
+
*/
|
|
1907
|
+
export async function appendRunAttemptHistory(
|
|
1908
|
+
admitted: AttemptHistorySource,
|
|
1909
|
+
outcome: AttemptHistoryOutcome,
|
|
1910
|
+
): Promise<void> {
|
|
1911
|
+
const entries = await readBoundSessionEntries(admitted.sessionFile);
|
|
1912
|
+
let parentId: string | null = null;
|
|
1913
|
+
let priorEntries = 0;
|
|
1914
|
+
for (const entry of entries) {
|
|
1915
|
+
if (typeof entry.id === "string" && entry.type !== "session") parentId = entry.id;
|
|
1916
|
+
if (
|
|
1917
|
+
entry.type === "custom" &&
|
|
1918
|
+
entry.customType === ATTEMPT_HISTORY_ENTRY_TYPE
|
|
1919
|
+
) {
|
|
1920
|
+
priorEntries += 1;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
const timestamp = new Date().toISOString();
|
|
1924
|
+
// Shape mirrors pi SessionManager.appendCustomEntry.
|
|
1925
|
+
const line = `${JSON.stringify({
|
|
1926
|
+
type: "custom",
|
|
1927
|
+
customType: ATTEMPT_HISTORY_ENTRY_TYPE,
|
|
1928
|
+
data: {
|
|
1929
|
+
sequence: priorEntries + 1,
|
|
1930
|
+
role: admitted.role,
|
|
1931
|
+
runId: admitted.runId,
|
|
1932
|
+
recordedAt: timestamp,
|
|
1933
|
+
outcome,
|
|
1934
|
+
},
|
|
1935
|
+
id: randomUUID(),
|
|
1936
|
+
parentId,
|
|
1937
|
+
timestamp,
|
|
1938
|
+
})}\n`;
|
|
1939
|
+
await appendFile(admitted.sessionFile, line, "utf8");
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
/**
|
|
1943
|
+
* Publish the retained residual with complete-write semantics (#419: the
|
|
1944
|
+
* previous attempt's pointer file is a rebuildable view once this attempt's
|
|
1945
|
+
* complete result is in the appended history; planted symlinks/directories
|
|
1946
|
+
* still fail loudly with their #182-A identities).
|
|
1947
|
+
*/
|
|
1856
1948
|
export async function publishComplianceAuditIncompleteEvidence(
|
|
1857
1949
|
admitted: AdmittedRoleInvocation,
|
|
1858
1950
|
outcome: ReturnType<typeof buildAuditIncompleteTerminalOutcome>,
|
|
1859
1951
|
): Promise<TerminalArtifactRef> {
|
|
1952
|
+
await appendRunAttemptHistory(admitted, outcome);
|
|
1953
|
+
if (
|
|
1954
|
+
typeof fsConstants.O_NOFOLLOW !== "number" ||
|
|
1955
|
+
typeof fsConstants.O_NONBLOCK !== "number"
|
|
1956
|
+
) {
|
|
1957
|
+
// #418 fail-closed: on platforms without O_NOFOLLOW (e.g. Windows,
|
|
1958
|
+
// nodejs/node#41590) the JS bitwise-or below would silently drop the flag
|
|
1959
|
+
// and the #182-A anti-symlink protection would vanish. Refuse loudly via
|
|
1960
|
+
// the existing publication-failure channel instead of publishing
|
|
1961
|
+
// unprotected; the complete attempt result above stays in appended history.
|
|
1962
|
+
throw auditArtifactPublicationError(
|
|
1963
|
+
"audit evidence publication requires O_NOFOLLOW|O_NONBLOCK open-flag support (anti-symlink/anti-planted protection must not be silently dropped); refusing to publish",
|
|
1964
|
+
"ENOSYS",
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1860
1967
|
const artifactsDir = await ensureAuditEvidenceDirectory(admitted.runDirectory);
|
|
1861
1968
|
const evidencePath = join(artifactsDir, "audit-incomplete.json");
|
|
1969
|
+
let existing: Awaited<ReturnType<typeof lstat>> | undefined;
|
|
1862
1970
|
try {
|
|
1863
|
-
|
|
1864
|
-
throw auditArtifactPublicationError(
|
|
1865
|
-
existing.isSymbolicLink()
|
|
1866
|
-
? "audit evidence destination is a symlink"
|
|
1867
|
-
: "audit evidence destination collision",
|
|
1868
|
-
existing.isSymbolicLink() ? "ELOOP" : "EEXIST",
|
|
1869
|
-
);
|
|
1971
|
+
existing = await lstat(evidencePath);
|
|
1870
1972
|
} catch (error) {
|
|
1871
1973
|
if (!isMissingPathError(error)) throw error;
|
|
1872
1974
|
}
|
|
1873
|
-
|
|
1975
|
+
if (existing?.isSymbolicLink()) {
|
|
1976
|
+
throw auditArtifactPublicationError(
|
|
1977
|
+
"audit evidence destination is a symlink",
|
|
1978
|
+
"ELOOP",
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
if (existing && !existing.isFile()) {
|
|
1982
|
+
throw auditArtifactPublicationError(
|
|
1983
|
+
"audit evidence destination is not a regular file",
|
|
1984
|
+
"EEXIST",
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
// #182-A protection restored atomically at this open seam: O_NOFOLLOW makes
|
|
1988
|
+
// the open itself refuse symlinks (ELOOP), so one swapped in after the lstat
|
|
1989
|
+
// above can never be followed or written through; O_NONBLOCK keeps a
|
|
1990
|
+
// race-planted FIFO from blocking this open forever. The fstat below
|
|
1991
|
+
// backstops any non-regular object that wins the window.
|
|
1992
|
+
const handle = await open(
|
|
1993
|
+
evidencePath,
|
|
1994
|
+
fsConstants.O_WRONLY |
|
|
1995
|
+
fsConstants.O_CREAT |
|
|
1996
|
+
fsConstants.O_TRUNC |
|
|
1997
|
+
fsConstants.O_NOFOLLOW |
|
|
1998
|
+
fsConstants.O_NONBLOCK,
|
|
1999
|
+
0o600,
|
|
2000
|
+
);
|
|
1874
2001
|
try {
|
|
2002
|
+
if (!(await handle.stat()).isFile()) {
|
|
2003
|
+
throw auditArtifactPublicationError(
|
|
2004
|
+
"audit evidence destination is not a regular file",
|
|
2005
|
+
"EEXIST",
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
1875
2008
|
await handle.writeFile(`${JSON.stringify(outcome, null, 2)}\n`, "utf8");
|
|
1876
2009
|
await handle.sync();
|
|
1877
2010
|
} finally {
|
|
@@ -2213,6 +2346,9 @@ export async function publishJudgeArtifacts(
|
|
|
2213
2346
|
roleOutcome: TerminalRoleOutcome,
|
|
2214
2347
|
sessionDirectory: string,
|
|
2215
2348
|
): Promise<TerminalArtifactRef[]> {
|
|
2349
|
+
// #419: history first — report/evidence stay last-write-wins views only
|
|
2350
|
+
// because every attempt's complete result has already been appended.
|
|
2351
|
+
await appendRunAttemptHistory(admitted, roleOutcome);
|
|
2216
2352
|
const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
|
|
2217
2353
|
const reportPath = join(artifactsDir, "report.json");
|
|
2218
2354
|
const evidencePath = join(artifactsDir, "evidence.json");
|
|
@@ -2268,6 +2404,7 @@ export async function publishCoderArtifacts(
|
|
|
2268
2404
|
readonly coderOutput?: CoderOutput;
|
|
2269
2405
|
} = {},
|
|
2270
2406
|
): Promise<TerminalArtifactRef[]> {
|
|
2407
|
+
await appendRunAttemptHistory(admitted, roleOutcome);
|
|
2271
2408
|
const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
|
|
2272
2409
|
const reportPath = join(artifactsDir, "report.json");
|
|
2273
2410
|
const evidencePath = join(artifactsDir, "evidence.json");
|
|
@@ -2573,6 +2710,7 @@ export async function publishFixerArtifacts(
|
|
|
2573
2710
|
readonly fixerOutput?: FixerOutput;
|
|
2574
2711
|
},
|
|
2575
2712
|
): Promise<TerminalArtifactRef[]> {
|
|
2713
|
+
await appendRunAttemptHistory(admitted, roleOutcome);
|
|
2576
2714
|
const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
|
|
2577
2715
|
const reportPath = join(artifactsDir, "report.json");
|
|
2578
2716
|
const evidencePath = join(artifactsDir, "evidence.json");
|
|
@@ -2732,6 +2870,7 @@ export async function publishCollectorArtifacts(
|
|
|
2732
2870
|
readonly collectorReceipt?: CollectorReceipt;
|
|
2733
2871
|
} = {},
|
|
2734
2872
|
): Promise<TerminalArtifactRef[]> {
|
|
2873
|
+
await appendRunAttemptHistory(admitted, roleOutcome);
|
|
2735
2874
|
const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
|
|
2736
2875
|
const reportPath = join(artifactsDir, "report.json");
|
|
2737
2876
|
const evidencePath = join(artifactsDir, "evidence.json");
|
|
@@ -2891,6 +3030,7 @@ export async function publishDoctorArtifacts(
|
|
|
2891
3030
|
readonly doctorOutput?: DoctorOutput;
|
|
2892
3031
|
} = {},
|
|
2893
3032
|
): Promise<TerminalArtifactRef[]> {
|
|
3033
|
+
await appendRunAttemptHistory(admitted, roleOutcome);
|
|
2894
3034
|
const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
|
|
2895
3035
|
const reportPath = join(artifactsDir, "report.json");
|
|
2896
3036
|
const evidencePath = join(artifactsDir, "evidence.json");
|
|
@@ -3155,6 +3295,7 @@ export async function publishReviewerArtifacts(
|
|
|
3155
3295
|
readonly reviewerReceipt?: RuntimeReviewerReceiptV2;
|
|
3156
3296
|
},
|
|
3157
3297
|
): Promise<TerminalArtifactRef[]> {
|
|
3298
|
+
await appendRunAttemptHistory(admitted, roleOutcome);
|
|
3158
3299
|
const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
|
|
3159
3300
|
const reportPath = join(artifactsDir, "report.json");
|
|
3160
3301
|
const evidencePath = join(artifactsDir, "evidence.json");
|
|
@@ -3412,6 +3553,7 @@ export async function publishMergerArtifacts(
|
|
|
3412
3553
|
readonly mergerOutput?: MergerOutput;
|
|
3413
3554
|
},
|
|
3414
3555
|
): Promise<TerminalArtifactRef[]> {
|
|
3556
|
+
await appendRunAttemptHistory(admitted, roleOutcome);
|
|
3415
3557
|
const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
|
|
3416
3558
|
const reportPath = join(artifactsDir, "report.json");
|
|
3417
3559
|
const evidencePath = join(artifactsDir, "evidence.json");
|
|
@@ -3746,6 +3888,19 @@ export async function publishFailureArtifacts(
|
|
|
3746
3888
|
);
|
|
3747
3889
|
const priorIssues: PublicationAttempt[] =
|
|
3748
3890
|
baseAttempt === undefined ? [] : [baseAttempt];
|
|
3891
|
+
// #419: each attempt's complete failure result joins the appended history
|
|
3892
|
+
// before any fixed-name artifact view is rewritten. History failure must not
|
|
3893
|
+
// strand the original controlled failure outside settlement — it rides
|
|
3894
|
+
// publicationIssues instead of aborting durability.
|
|
3895
|
+
try {
|
|
3896
|
+
await appendRunAttemptHistory(admitted, {
|
|
3897
|
+
kind: "failure",
|
|
3898
|
+
role: admitted.role,
|
|
3899
|
+
...failure,
|
|
3900
|
+
});
|
|
3901
|
+
} catch (error) {
|
|
3902
|
+
priorIssues.push(publicationAttemptFromError(admitted.sessionFile, error));
|
|
3903
|
+
}
|
|
3749
3904
|
|
|
3750
3905
|
// Prefer conventional names; unique fallback dirs keep colliding fixed paths
|
|
3751
3906
|
// from stranding the original failure outside settlement. Include the ledger
|