@counterfact-labs/cli 0.2.6 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/bundle/client-release.json +1 -1
- package/dist/adapters/analysis-results.js +87 -0
- package/dist/adapters/combined-results.js +27 -0
- package/dist/adapters/completion-lock.js +95 -0
- package/dist/adapters/completion.js +132 -17
- package/dist/adapters/entry-files.js +129 -0
- package/dist/adapters/entry-packing.js +161 -0
- package/dist/adapters/entry-selection.js +57 -0
- package/dist/adapters/files.js +111 -5
- package/dist/adapters/git.js +10 -1
- package/dist/adapters/relay-plan.js +5 -7
- package/dist/adapters/release.js +4 -1
- package/dist/adapters/results.js +18 -3
- package/dist/adapters/selection.js +114 -0
- package/dist/adapters/settings.js +62 -4
- package/dist/adapters/source-relay-plan.js +7 -0
- package/dist/analysis-core/analysis/code-agents/skipped-files.js +1 -1
- package/dist/analysis-core/analysis/file-analysis-plan.js +1 -0
- package/dist/analysis-core/analysis/file-dependencies.js +1 -0
- package/dist/analysis-core/analysis/finding-activity-switch.js +1 -0
- package/dist/analysis-core/analysis/finding-identity.js +1 -0
- package/dist/analysis-core/analysis/finding-quality/policy.js +1 -0
- package/dist/analysis-core/analysis/folder-analysis-plan.js +1 -0
- package/dist/analysis-core/analysis/passes/full-file-passes.js +1 -0
- package/dist/analysis-core/analysis/project-architecture.js +1 -0
- package/dist/analysis-core/analysis/schemas.js +1 -1
- package/dist/analysis-core/chunks/2AASSZRO.js +35 -0
- package/dist/analysis-core/chunks/2YREYGOG.js +1 -0
- package/dist/analysis-core/chunks/33HRXJ5Q.js +1 -0
- package/dist/analysis-core/chunks/33IGHOYT.js +1 -0
- package/dist/analysis-core/chunks/4PT5C4IT.js +7 -0
- package/dist/analysis-core/chunks/4RXRAB5S.js +1 -0
- package/dist/analysis-core/chunks/4UDQBDDE.js +1 -0
- package/dist/analysis-core/chunks/{OPMA32A6.js → 5B5UCWD3.js} +1 -1
- package/dist/analysis-core/chunks/6YCABS5T.js +1 -0
- package/dist/analysis-core/chunks/CQQ4TTXE.js +1 -0
- package/dist/analysis-core/chunks/CTLHKFPH.js +19 -0
- package/dist/analysis-core/chunks/F54DOWKN.js +6 -0
- package/dist/analysis-core/chunks/FRXOQ2M6.js +3 -0
- package/dist/analysis-core/chunks/HCPO6QPU.js +1 -0
- package/dist/analysis-core/chunks/HHFDNY6V.js +14 -0
- package/dist/analysis-core/chunks/{3JWEFCTJ.js → HVQJKPQ4.js} +1 -1
- package/dist/analysis-core/chunks/INVTU73D.js +1 -0
- package/dist/analysis-core/chunks/JEWBPIQI.js +1 -0
- package/dist/analysis-core/chunks/JGK37AWN.js +1 -0
- package/dist/analysis-core/chunks/LFYTD7JF.js +1 -0
- package/dist/analysis-core/chunks/LMUXOKIM.js +1 -0
- package/dist/analysis-core/chunks/LOBIDUCA.js +1 -0
- package/dist/analysis-core/chunks/M5YWJY7P.js +1 -0
- package/dist/analysis-core/chunks/MGVIKA74.js +1 -0
- package/dist/analysis-core/chunks/MKYLK6DB.js +1 -0
- package/dist/analysis-core/chunks/MZOKZ3N6.js +1 -0
- package/dist/analysis-core/chunks/NXHVBJL4.js +1 -0
- package/dist/analysis-core/chunks/OKBK3JL3.js +1 -0
- package/dist/analysis-core/chunks/PBHHWFR6.js +1 -0
- package/dist/analysis-core/chunks/Q5WR6B2H.js +2 -0
- package/dist/analysis-core/chunks/QVXARKP7.js +1 -0
- package/dist/analysis-core/chunks/RQTNTIR4.js +1 -0
- package/dist/analysis-core/chunks/RUVMFAST.js +1 -0
- package/dist/analysis-core/chunks/S27VIQMZ.js +1 -0
- package/dist/analysis-core/chunks/{YDBO7P4X.js → S27Y2TP5.js} +1 -1
- package/dist/analysis-core/chunks/SJESFYBA.js +10 -0
- package/dist/analysis-core/chunks/SP6XFHNH.js +1 -0
- package/dist/analysis-core/chunks/VDUFPTDD.js +1 -0
- package/dist/analysis-core/chunks/W24SFYZM.js +67 -0
- package/dist/analysis-core/chunks/YTHF2E2S.js +1 -0
- package/dist/analysis-core/chunks/YWL52FVW.js +2 -0
- package/dist/analysis-core/contracts/analysis-selection.js +1 -0
- package/dist/analysis-core/contracts/analysis-size.js +1 -0
- package/dist/analysis-core/contracts/execution-request.js +1 -0
- package/dist/analysis-core/contracts/intake-finding.js +1 -0
- package/dist/analysis-core/contracts/model-call-identity.js +1 -0
- package/dist/analysis-core/contracts/module-resolution.js +1 -0
- package/dist/analysis-core/contracts/relay-analysis.js +1 -1
- package/dist/analysis-core/contracts/requests.js +1 -1
- package/dist/analysis-core/contracts/selection-paths.js +1 -0
- package/dist/analysis-core/conventions/analysis-fit.js +1 -1
- package/dist/analysis-core/conventions/file-admission.js +1 -1
- package/dist/analysis-core/conventions/read-limits.js +1 -0
- package/dist/analysis-core/import/calls/modules.js +1 -0
- package/dist/analysis-core/import/entry-selection.js +1 -0
- package/dist/analysis-core/import/outside-loader-sources.js +1 -1
- package/dist/analysis-core/index.js +3 -3
- package/dist/analysis-core/requests/answer-formats.js +1 -1
- package/dist/analysis-core/requests/local-plan.js +1 -1
- package/dist/analysis-core/requests/planned-limits.js +1 -0
- package/dist/analysis-core/requests/registry.js +1 -1
- package/dist/analysis-core/requests/template-version.js +1 -1
- package/dist/analysis-core/requests/tools.js +1 -1
- package/dist/analysis-core/settings.js +1 -1
- package/dist/analysis-core/stages/engine-entry.js +1 -1
- package/dist/bundle/seal/engine.js +1 -1
- package/dist/cli/analysis-selection.js +133 -0
- package/dist/cli/api.js +1 -1
- package/dist/cli/commands/analyze.js +82 -14
- package/dist/cli/commands/skipped.js +65 -0
- package/dist/cli/completion-delivery.js +63 -0
- package/dist/cli/completion-recovery.js +68 -14
- package/dist/cli/context.js +4 -2
- package/dist/cli/finding-display.js +102 -7
- package/dist/cli/finding-settings.js +3 -1
- package/dist/cli/intake-payload.js +26 -2
- package/dist/cli/main.js +37 -11
- package/dist/cli/mcp-failure.js +6 -5
- package/dist/cli/mcp.js +191 -26
- package/dist/cli/prior-completion.js +35 -0
- package/dist/cli/relay-check.js +260 -110
- package/dist/cli/relay-failure.js +32 -0
- package/dist/cli/relay-preparation.js +72 -21
- package/dist/cli/result-check.js +31 -12
- package/dist/cli/results-upload.js +131 -0
- package/dist/cli/saved-results.js +10 -0
- package/dist/cli/scope-choices.js +57 -0
- package/dist/cli/sign-in.js +2 -2
- package/dist/cli/skipped-files.js +22 -0
- package/dist/cli/summary.js +15 -1
- package/dist/contract/agent-choice.js +25 -2
- package/dist/contract/cli-api-messages.js +3 -0
- package/dist/contract/cli-refusals.js +39 -5
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console-url.js +3 -0
- package/dist/contract/console.js +24 -40
- package/dist/contract/engine.js +1 -1
- package/dist/contract/messages.js +4 -3
- package/dist/contract/project-architecture.js +1 -0
- package/dist/contract/scope-choice.js +98 -0
- package/dist/contract/selection-metadata.js +9 -0
- package/dist/contract/selection-offer.js +40 -0
- package/dist/gatekeeper/cli-api.js +70 -4
- package/dist/gatekeeper/console.js +55 -28
- package/dist/gatekeeper/model.js +5 -23
- package/dist/gatekeeper/trust/policy.js +2 -1
- package/package.json +5 -3
- package/dist/analysis-core/analysis/source-notes/types.js +0 -1
- package/dist/analysis-core/chunks/2PSNOSJ2.js +0 -1
- package/dist/analysis-core/chunks/377MLK4M.js +0 -21
- package/dist/analysis-core/chunks/5DGPLXYB.js +0 -1
- package/dist/analysis-core/chunks/6B2HILSY.js +0 -1
- package/dist/analysis-core/chunks/6QY5BV5B.js +0 -14
- package/dist/analysis-core/chunks/6U2IR4XB.js +0 -1
- package/dist/analysis-core/chunks/DLPVYWHZ.js +0 -1
- package/dist/analysis-core/chunks/FOKDE64Y.js +0 -1
- package/dist/analysis-core/chunks/FSLWD2PE.js +0 -1
- package/dist/analysis-core/chunks/K2GNDX3W.js +0 -1
- package/dist/analysis-core/chunks/KTNA2KTN.js +0 -7
- package/dist/analysis-core/chunks/KWECQJ6X.js +0 -1
- package/dist/analysis-core/chunks/NIOS7U6C.js +0 -41
- package/dist/analysis-core/chunks/PECX3OXB.js +0 -1
- package/dist/analysis-core/chunks/PYKAERZZ.js +0 -1
- package/dist/analysis-core/chunks/QFY6A3MS.js +0 -66
- package/dist/analysis-core/chunks/SKVEBOGA.js +0 -1
- package/dist/analysis-core/chunks/STI4JE7L.js +0 -1
- package/dist/analysis-core/chunks/SVMEWGD3.js +0 -1
- package/dist/analysis-core/chunks/X7WZAA7S.js +0 -1
- package/dist/analysis-core/chunks/ZTOVBAG7.js +0 -1
- package/dist/analysis-core/data/finding-types.js +0 -1
package/dist/cli/relay-check.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
+
import { skippedFilesEnabled } from "../analysis-core/settings.js";
|
|
2
|
+
import { analysisSkippedFiles, sizeSkippedFiles } from "../analysis-core/analysis/code-agents/skipped-files.js";
|
|
3
|
+
import { loadAnalysisResults, loadPublicReport, saveAnalysisContext, settleAnalysisContext } from "../adapters/analysis-results.js";
|
|
4
|
+
import { fingerprintOf } from "../analysis-core/analysis/finding-identity.js";
|
|
5
|
+
import { combineAssessments } from "../adapters/combined-results.js";
|
|
1
6
|
import { randomUUID } from "node:crypto";
|
|
2
7
|
import { disabledSetupPings } from "../setup-pings/client.js";
|
|
3
|
-
import {
|
|
8
|
+
import { fileScopeEnabled, scopeChoicesEnabled } from "../adapters/settings.js";
|
|
9
|
+
import { saveCompletion, discardCompletion, lockCompletion } from "../adapters/completion.js";
|
|
4
10
|
import { saveLocalResults } from "../adapters/results.js";
|
|
5
11
|
import { GITIGNORE_OUTPUT_NOTICE } from "../adapters/gitignore-output.js";
|
|
12
|
+
import { priorCompletion } from "./prior-completion.js";
|
|
6
13
|
import { OutputPathError, outputPaths } from "../adapters/output-paths.js";
|
|
7
14
|
import { validateClientRelease } from "../adapters/release.js";
|
|
8
15
|
import { selectAnalysisExecution } from "../adapters/models.js";
|
|
9
16
|
import { resolveFolderPath } from "../adapters/folder-path.js";
|
|
10
17
|
import { AgentChoiceError } from "../contract/agent-choice.js";
|
|
11
|
-
import { savedAnalysisUrl } from "../contract/console-url.js";
|
|
12
|
-
import { prepareRelayCheck } from "./relay-preparation.js";
|
|
13
|
-
import { cliRefusals,
|
|
18
|
+
import { savedAnalysisUrl, savedArchitectureUrl } from "../contract/console-url.js";
|
|
19
|
+
import { prepareRelayCheck, prepareFolderChoice } from "./relay-preparation.js";
|
|
20
|
+
import { cliRefusals, refusalExitCode } from "../contract/cli-refusals.js";
|
|
14
21
|
import { ConsoleClient, ConsoleError } from "../gatekeeper/console.js";
|
|
15
22
|
import { GatekeeperClient } from "../gatekeeper/client.js";
|
|
16
23
|
import { connectRelay, relayOrigin } from "../gatekeeper/connection.js";
|
|
@@ -19,51 +26,17 @@ import { runAnalysisToEnd } from "../core/analysis/run-to-end.js";
|
|
|
19
26
|
import { findingPresentation } from "../analysis-core/findings/presentation.js";
|
|
20
27
|
import { formatSummary } from "./summary.js";
|
|
21
28
|
import { buildIntakePayload } from "./intake-payload.js";
|
|
22
|
-
import {
|
|
29
|
+
import { matchesCheck, observedCompletionTiming } from "./completion-recovery.js";
|
|
23
30
|
import { savedFindings } from "./finding-display.js";
|
|
24
|
-
import { INVALID_PROPOSAL_MESSAGE } from "../contract/agent-proposal.js";
|
|
25
31
|
import { readFindingSettings } from "./finding-settings.js";
|
|
32
|
+
import { deliverCompletedResults } from "./completion-delivery.js";
|
|
33
|
+
import { requireIntakeReservation, ResultsUploadError, submitResults, uploadFailure } from "./results-upload.js";
|
|
34
|
+
import { recoverRelayFailure } from "./relay-failure.js";
|
|
35
|
+
import { analysisSaveOptions, resolveRunSelection } from "./analysis-selection.js";
|
|
36
|
+
import { SCOPE_CHOICE_EXIT_CODE, progressLine } from "../contract/scope-choice.js";
|
|
37
|
+
import { ScopeChoiceError, scopeChoiceRefusal, selectionScopeChoices } from "../contract/selection-offer.js";
|
|
38
|
+
import { fileScopeOf } from "../analysis-core/analysis/file-analysis-plan.js";
|
|
26
39
|
const stateLine = (status) => `Reservation ${status.state}; settlement ${status.settlement}; lease ${new Date(status.expiresAt).toLocaleString("en-US", { timeZone: "America/Los_Angeles", timeZoneName: "short" })}.\n`;
|
|
27
|
-
function requireIntakeReservation(status) {
|
|
28
|
-
if (status.state === "reserved")
|
|
29
|
-
return;
|
|
30
|
-
if (status.state === "closing")
|
|
31
|
-
throw new ConsoleError("reservation_closing");
|
|
32
|
-
if (status.state === "closed")
|
|
33
|
-
throw new ConsoleError("reservation_closed");
|
|
34
|
-
throw new ConsoleError("reservation_expired", "intake");
|
|
35
|
-
}
|
|
36
|
-
/** Retry an exact completed payload; a lost intake response must never purchase another analysis. */
|
|
37
|
-
async function submit(client, payload, signal) {
|
|
38
|
-
signal.throwIfAborted();
|
|
39
|
-
const status = await client.status(payload.reservationId);
|
|
40
|
-
const committed = (value) => ({ checkId: value.checkId, projectId: value.projectId, analysisId: value.analysisId, findingIds: value.findingIds,
|
|
41
|
-
...(value.findings ? { findings: value.findings } : {}), settlement: value.settlement });
|
|
42
|
-
if (!matchesCheck(status, payload))
|
|
43
|
-
throw new ConsoleError("check_conflict");
|
|
44
|
-
if (status.uploadState === "saved")
|
|
45
|
-
return committed(status);
|
|
46
|
-
requireIntakeReservation(status);
|
|
47
|
-
signal.throwIfAborted();
|
|
48
|
-
try {
|
|
49
|
-
return await client.intake(payload);
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
52
|
-
// Keep the refusal for display; giving up the saved payload requires confirmed recovery status.
|
|
53
|
-
const after = await client.status(payload.reservationId).catch(() => { throw error; });
|
|
54
|
-
if (!matchesCheck(after, payload))
|
|
55
|
-
throw new ConsoleError("check_conflict");
|
|
56
|
-
if (after.uploadState === "saved")
|
|
57
|
-
return committed(after);
|
|
58
|
-
// Exact idempotent retry only; a second uncertainty leaves the completion record for restart.
|
|
59
|
-
if (error instanceof ConsoleError && error.code === "unavailable") {
|
|
60
|
-
requireIntakeReservation(after);
|
|
61
|
-
signal.throwIfAborted();
|
|
62
|
-
return client.intake(payload);
|
|
63
|
-
}
|
|
64
|
-
throw error;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
40
|
export function runRelayCheck(folder, options) {
|
|
68
41
|
return runRelayLifecycle(folder, options);
|
|
69
42
|
}
|
|
@@ -73,23 +46,73 @@ export function runReservedRelayCheck(folder, options) {
|
|
|
73
46
|
}
|
|
74
47
|
async function runRelayLifecycle(folder, options, existing) {
|
|
75
48
|
const { config, stdout, stderr } = options;
|
|
49
|
+
const retainUnsaved = config.CF_LOCAL_RESULTS_RETRY !== "0";
|
|
76
50
|
let settings = options.settings;
|
|
77
|
-
const summaryOptions = { audienceEnabled: false };
|
|
51
|
+
const summaryOptions = { audienceEnabled: false, notActiveEnabled: false, skippedFilesEnabled: skippedFilesEnabled(settings), skippedOffer: !options.addToAnalysisId, analyzedFolder: options.analyzedFolder ?? folder };
|
|
52
|
+
let skippedFiles = [];
|
|
78
53
|
let client, reservationId, projectId;
|
|
79
54
|
let connection = existing?.connection;
|
|
80
|
-
let identity;
|
|
81
55
|
let completedPayload;
|
|
82
56
|
let displayFindings;
|
|
83
|
-
let completionOrigin;
|
|
57
|
+
let completionOrigin, completionRecordPath;
|
|
58
|
+
let completionTiming = {};
|
|
59
|
+
let combinedFindings;
|
|
60
|
+
let topFindingIds;
|
|
61
|
+
let refusedCalls;
|
|
84
62
|
let completed = false, uploaded = false, heartbeat;
|
|
63
|
+
let selection, membership;
|
|
64
|
+
let selectedPaths, completionIdentity;
|
|
65
|
+
let outputDir = options.outputDir;
|
|
85
66
|
let renewal;
|
|
67
|
+
let releaseCompletion;
|
|
68
|
+
let localErrorMessage;
|
|
69
|
+
let completionStored = false, lostCompletionLock = false;
|
|
70
|
+
let uploadOutcome;
|
|
71
|
+
let savedResult;
|
|
86
72
|
const controller = new AbortController();
|
|
87
73
|
const signal = options.signal ? AbortSignal.any([controller.signal, options.signal]) : controller.signal;
|
|
74
|
+
// A completed upload must survive the analysis lease closing during local report writes.
|
|
75
|
+
const uploadSignal = retainUnsaved ? options.signal ?? new AbortController().signal : signal;
|
|
76
|
+
const persist = async (payload, origin, uploaded = false, finished = false) => {
|
|
77
|
+
if (lostCompletionLock)
|
|
78
|
+
throw new OutputPathError();
|
|
79
|
+
const path = await saveCompletion(folder, payload, origin, { uploaded, finished, completedAt: completionTiming.completedAt,
|
|
80
|
+
absoluteExpiresAt: completionTiming.absoluteExpiresAt, retainUnsaved, identity: completionIdentity });
|
|
81
|
+
completionStored = true;
|
|
82
|
+
completionRecordPath = path;
|
|
83
|
+
return path;
|
|
84
|
+
};
|
|
85
|
+
const submit = (payload, alreadySaved = false) => {
|
|
86
|
+
if (retainUnsaved) {
|
|
87
|
+
try {
|
|
88
|
+
options.onSaving?.();
|
|
89
|
+
stderr("Saving the results…\n");
|
|
90
|
+
}
|
|
91
|
+
catch { /* Progress output must not prevent saving completed work. */ }
|
|
92
|
+
}
|
|
93
|
+
return submitResults(client, payload, uploadSignal, { retry: retainUnsaved, alreadySaved, timing: completionTiming }).then(result => { savedResult = result; return result; });
|
|
94
|
+
};
|
|
88
95
|
const showSaved = (result) => {
|
|
89
96
|
const url = savedAnalysisUrl(result.projectId, config.CF_CONSOLE_URL);
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
const architectureUrl = completedPayload?.architecture ? savedArchitectureUrl(result.projectId, config.CF_CONSOLE_URL) : undefined;
|
|
98
|
+
const findings = combinedFindings ?? savedFindings({ findingIds: result.findingIds ?? [], findings: result.findings }, displayFindings ?? completedPayload?.findings ?? []);
|
|
99
|
+
options.onSaved?.({ ...result, ...(combinedFindings ? { findingIds: combinedFindings.map(f => f.id), topFindingIds } : {}), ...(!skippedFilesEnabled(settings) ? {} : { skippedFiles }), findings, url, ...(architectureUrl ? { architectureUrl } : {}), ...summaryOptions });
|
|
100
|
+
stdout(result.analysisId ? `Saved analysis ${result.analysisId}: ${url}\n` : `Full results: ${url}\n`);
|
|
101
|
+
if (architectureUrl)
|
|
102
|
+
stdout(`Architecture: ${architectureUrl}\n`);
|
|
103
|
+
};
|
|
104
|
+
const settleSaved = async (result) => {
|
|
105
|
+
const payload = completedPayload;
|
|
106
|
+
if (!result.analysisId || !skippedFilesEnabled(settings) || !payload?.coverage || !payload.addToAnalysisId && !sizeSkippedFiles(payload.coverage.skippedFiles).length)
|
|
107
|
+
return;
|
|
108
|
+
const context = await settleAnalysisContext(folder, { ...result, analysisId: result.analysisId, findingIds: result.findingIds ?? [] }, payload.checkId, payload.addToAnalysisId, outputDir);
|
|
109
|
+
const { report } = await loadAnalysisResults(folder, outputDir);
|
|
110
|
+
skippedFiles = report.refusedFiles;
|
|
111
|
+
if (payload.addToAnalysisId) {
|
|
112
|
+
topFindingIds = context.topFindingIds;
|
|
113
|
+
const references = new Map(context.findings?.map(f => [f.fingerprint, f]));
|
|
114
|
+
combinedFindings = report.findings.flatMap(finding => { const saved = references.get(fingerprintOf(finding)); return saved ? [saved] : []; });
|
|
115
|
+
}
|
|
93
116
|
};
|
|
94
117
|
try {
|
|
95
118
|
if (!config.CF_CONSOLE_URL || !existing && (!config.CF_RELAY_URL || !config.CF_CONSOLE_TOKEN)) {
|
|
@@ -101,61 +124,103 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
101
124
|
throw new ConsoleError("check_conflict");
|
|
102
125
|
reservationId = existing.reservation.reservationId;
|
|
103
126
|
projectId = existing.reservation.projectId;
|
|
104
|
-
identity = existing.reservation;
|
|
105
127
|
client = existing.client;
|
|
106
128
|
}
|
|
107
129
|
else {
|
|
108
130
|
relayOrigin(config.CF_RELAY_URL);
|
|
109
131
|
client = new ConsoleClient({ url: config.CF_CONSOLE_URL, token: config.CF_CONSOLE_TOKEN });
|
|
110
132
|
}
|
|
111
|
-
// Signature verification precedes all
|
|
133
|
+
// Signature verification precedes all file content reads, status calls and spending.
|
|
112
134
|
const validated = validateClientRelease(options.release, settings);
|
|
113
135
|
const { bundle } = validated;
|
|
114
136
|
settings = validated.settings;
|
|
115
|
-
|
|
116
|
-
|
|
137
|
+
const findingSettings = readFindingSettings(settings);
|
|
138
|
+
summaryOptions.audienceEnabled = findingSettings.audienceEnabled;
|
|
139
|
+
summaryOptions.notActiveEnabled = findingSettings.notActiveEnabled;
|
|
140
|
+
selection = await resolveRunSelection(folder, options.cwd ?? process.cwd(), settings, options);
|
|
141
|
+
// Verify the selected folder before output and recovery operations can fail for their own reasons.
|
|
142
|
+
folder = await resolveFolderPath(selection?.outputAnchor ?? folder).catch(error => {
|
|
143
|
+
if (error instanceof Error)
|
|
144
|
+
Object.assign(error, { localOperation: "read" });
|
|
145
|
+
throw error;
|
|
146
|
+
});
|
|
147
|
+
outputDir ??= selection?.outputDir;
|
|
148
|
+
if (selection?.kind === "file")
|
|
149
|
+
completionIdentity = {
|
|
150
|
+
consoleOrigin: new URL(config.CF_CONSOLE_URL).origin, selectionKey: selection.selectionKey,
|
|
151
|
+
};
|
|
117
152
|
signal.throwIfAborted();
|
|
118
|
-
|
|
119
|
-
|
|
153
|
+
if (retainUnsaved)
|
|
154
|
+
releaseCompletion = await lockCompletion(folder, () => { lostCompletionLock = true; controller.abort(); }, completionIdentity);
|
|
155
|
+
const recovery = await priorCompletion(folder, { retryEnabled: retainUnsaved,
|
|
156
|
+
consoleOrigin: new URL(config.CF_CONSOLE_URL).origin, projectId: existing?.expectedProjectId, retryUpload: options.retryUpload,
|
|
157
|
+
identity: completionIdentity });
|
|
158
|
+
for (const notice of recovery.notices) {
|
|
159
|
+
try {
|
|
160
|
+
options.onNotice?.(notice);
|
|
161
|
+
stderr(`${notice}\n`);
|
|
162
|
+
}
|
|
163
|
+
catch { /* Notices must not prevent saving the current setup's results. */ }
|
|
164
|
+
}
|
|
165
|
+
if (recovery.prior) {
|
|
166
|
+
const { record: prior, path } = recovery.prior;
|
|
120
167
|
// Recovering another check would leave the caller's paid reservation unexecuted.
|
|
121
168
|
if (existing && (prior.payload.reservationId !== reservationId || !matchesCheck(prior.payload, existing.reservation))) {
|
|
122
169
|
throw new ConsoleError("check_conflict");
|
|
123
170
|
}
|
|
171
|
+
completionStored = true;
|
|
172
|
+
completionRecordPath = path;
|
|
124
173
|
completedPayload = prior.payload;
|
|
174
|
+
skippedFiles = prior.payload.coverage?.skippedFiles ?? [];
|
|
125
175
|
completionOrigin = prior.consoleOrigin;
|
|
176
|
+
completionTiming = { completedAt: prior.completedAt, absoluteExpiresAt: prior.absoluteExpiresAt };
|
|
126
177
|
uploaded = prior.uploaded ?? false;
|
|
127
|
-
const recovery = completionRecoveryDecision({ statusSucceeded: false,
|
|
128
|
-
localMismatch: prior.consoleOrigin !== new URL(config.CF_CONSOLE_URL).origin });
|
|
129
|
-
if (recovery.message)
|
|
130
|
-
throw new Error(recovery.message);
|
|
131
178
|
reservationId = prior.payload.reservationId;
|
|
132
|
-
identity = prior.payload;
|
|
133
179
|
completed = true;
|
|
134
|
-
const saved = await submit(
|
|
180
|
+
const saved = await submit(prior.payload, retainUnsaved && uploaded);
|
|
135
181
|
uploaded = true;
|
|
136
|
-
await
|
|
182
|
+
await persist(prior.payload, prior.consoleOrigin, true);
|
|
183
|
+
await settleSaved(saved);
|
|
137
184
|
showSaved(saved);
|
|
138
185
|
return 0;
|
|
139
186
|
}
|
|
140
187
|
signal.throwIfAborted();
|
|
141
188
|
if (options.retryUpload)
|
|
142
189
|
throw new Error("The saved completion record is missing. Keep the local report and check the output directory.");
|
|
190
|
+
// Negotiate once before paid work: older strict intake schemas cannot accept optional metadata.
|
|
191
|
+
const capabilities = skippedFilesEnabled(settings) && options.skippedFilesSupported === undefined
|
|
192
|
+
|| settings.CF_LOCAL_ARCHITECTURE === "1" && options.consoleSupportsArchitecture === undefined
|
|
193
|
+
? await client.capabilities() : undefined;
|
|
194
|
+
const consoleSupportsArchitecture = options.consoleSupportsArchitecture ?? capabilities?.architecture ?? false;
|
|
195
|
+
if (skippedFilesEnabled(settings) && !(options.skippedFilesSupported ?? capabilities?.skippedFiles)) {
|
|
196
|
+
if (options.addToAnalysisId || fileScopeOf(settings) !== undefined)
|
|
197
|
+
throw new ConsoleError("unavailable", "start");
|
|
198
|
+
settings = Object.freeze({ ...settings, CF_SKIPPED_FILES: "0" });
|
|
199
|
+
summaryOptions.skippedFilesEnabled = false;
|
|
200
|
+
}
|
|
143
201
|
try {
|
|
144
|
-
await outputPaths(folder, ["findings.json", "summary.md"],
|
|
202
|
+
await outputPaths(folder, ["findings.json", "summary.md", ...(selection?.kind === "file" ? ["selection.json"] : [])], outputDir);
|
|
145
203
|
}
|
|
146
204
|
catch (error) {
|
|
147
205
|
if (error.code !== "ENOENT")
|
|
148
206
|
throw error;
|
|
149
207
|
}
|
|
150
|
-
const
|
|
208
|
+
const prepared = await prepareRelayCheck(selection?.path ?? folder, { ...options, settings, selection, signal, expectedCommit: existing?.expectedCommit, expectedFiles: existing?.expectedFiles }, { ...validated, settings });
|
|
209
|
+
const { source, provenance, plan, start, startMetadata } = prepared;
|
|
210
|
+
membership = prepared.membership;
|
|
211
|
+
const includedPaths = Object.keys(source.files);
|
|
212
|
+
selectedPaths = includedPaths;
|
|
213
|
+
refusedCalls = plan.calls;
|
|
214
|
+
skippedFiles = source.skippedFiles;
|
|
151
215
|
let reservation = existing?.reservation;
|
|
152
216
|
if (!reservation) {
|
|
153
|
-
const started = await client.start(start, startMetadata);
|
|
217
|
+
const started = await client.start(start, { ...startMetadata, ...(options.addToAnalysisId ? { addToAnalysisId: options.addToAnalysisId } : {}) });
|
|
218
|
+
if (options.expectedProjectId && started.projectId !== options.expectedProjectId)
|
|
219
|
+
throw new ConsoleError("check_conflict");
|
|
154
220
|
projectId = started.projectId;
|
|
155
221
|
signal.throwIfAborted();
|
|
156
222
|
const created = await client.create({ ...started, checkId: randomUUID(), plan });
|
|
157
223
|
reservationId = created.reservationId;
|
|
158
|
-
identity = created;
|
|
159
224
|
signal.throwIfAborted();
|
|
160
225
|
// Attestation contains no source and precedes reserve, so a bad relay never starts paid work.
|
|
161
226
|
connection = await (options.connect ?? (url => connectRelay(url, { trust: productionTrust })))(config.CF_RELAY_URL);
|
|
@@ -180,23 +245,55 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
180
245
|
reservationId: activeId, reservationToken: reservation.reservationToken, instructions: { bundle, verificationKey: options.release.verificationKey }, signal,
|
|
181
246
|
});
|
|
182
247
|
options.onStart?.();
|
|
183
|
-
stderr(
|
|
248
|
+
stderr(progressLine(selection));
|
|
184
249
|
(options.setupPings ?? disabledSetupPings).ping("first_analysis_started");
|
|
185
|
-
const report = await runAnalysisToEnd(source, execution.model, settings, { execution });
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
250
|
+
const report = await runAnalysisToEnd(source, execution.model, settings, { execution, ...(options.addToAnalysisId ? { findingsOnly: true } : {}) });
|
|
251
|
+
skippedFiles = !skippedFilesEnabled(settings) ? source.skippedFiles : analysisSkippedFiles(source.skippedFiles, report.skippedFiles);
|
|
252
|
+
const priorResults = options.addToAnalysisId ? await loadAnalysisResults(folder, outputDir) : undefined;
|
|
253
|
+
const assessments = combineAssessments(priorResults?.context.assessments ?? [], report.findingCheck.qualityAudit?.assessments ?? []);
|
|
254
|
+
completionTiming = observedCompletionTiming({ completedAt: Date.now() }, { kind: "found", status: reservation }, reservation);
|
|
255
|
+
// The 30-minute lease covers upload retries; renewal failure must not cancel completed results.
|
|
256
|
+
if (retainUnsaved)
|
|
257
|
+
clearInterval(heartbeat);
|
|
258
|
+
else
|
|
259
|
+
signal.throwIfAborted();
|
|
260
|
+
let saveContext;
|
|
261
|
+
const saveReports = async () => {
|
|
262
|
+
const paths = await saveLocalResults(folder, report, skippedFiles, outputDir, { ...summaryOptions, settings,
|
|
263
|
+
...analysisSaveOptions(selection, options.outputDir), selection: membership, includedPaths,
|
|
264
|
+
...(options.addToAnalysisId ? { append: { readPaths: includedPaths, assessments } } : {}) });
|
|
265
|
+
try {
|
|
266
|
+
options.onLocalSaved?.(paths);
|
|
267
|
+
if (paths.ignoreAdded)
|
|
268
|
+
stderr(GITIGNORE_OUTPUT_NOTICE);
|
|
269
|
+
}
|
|
270
|
+
catch { /* Output failure must not change a server outcome. */ }
|
|
271
|
+
await saveContext?.();
|
|
272
|
+
return paths;
|
|
273
|
+
};
|
|
274
|
+
// The retry off path saves the full report even when intake validation refuses it.
|
|
275
|
+
let paths = retainUnsaved ? undefined : await saveReports();
|
|
276
|
+
const { payload, resultCheck, findingIndexes, findingActivities } = buildIntakePayload({ checkId: reservation.checkId, projectId: projectId, reservationId,
|
|
277
|
+
bundleVersion: bundle.version, relayVersion: options.release.relayVersion, counterfactLocalVersion: options.release.localVersion }, provenance, report, { settings, files: source.snapshot, consoleSupportsActivity: options.consoleSupportsActivity, consoleSupportsArchitecture,
|
|
278
|
+
...(!skippedFilesEnabled(settings) || !options.addToAnalysisId && !sizeSkippedFiles(skippedFiles).length ? {} : { coverage: { skippedFiles: sizeSkippedFiles(skippedFiles), readPaths: includedPaths },
|
|
279
|
+
addToAnalysisId: options.addToAnalysisId, findingAssessments: report.findingCheck.qualityAudit?.assessments ?? [] }) });
|
|
280
|
+
displayFindings = findingIndexes.map((index, acceptedIndex) => {
|
|
197
281
|
const finding = report.findings[index];
|
|
198
|
-
|
|
282
|
+
const activity = findingActivities[acceptedIndex];
|
|
283
|
+
return { title: findingPresentation(finding).displayTitle, audience: finding.audience,
|
|
284
|
+
severity: finding.severity, site: finding.site, fixes: finding.fixes,
|
|
285
|
+
...(activity === undefined ? {} : { activity }) };
|
|
199
286
|
});
|
|
287
|
+
if (skippedFilesEnabled(settings) && (options.addToAnalysisId || sizeSkippedFiles(skippedFiles).length)) {
|
|
288
|
+
saveContext = async () => {
|
|
289
|
+
await saveAnalysisContext(folder, { projectId: projectId, analysisId: options.addToAnalysisId ?? reservation.checkId,
|
|
290
|
+
settings: priorResults?.context.settings ?? settings, assessments,
|
|
291
|
+
...(priorResults?.context.findings ? { findings: priorResults.context.findings } : {}),
|
|
292
|
+
pending: { checkId: reservation.checkId, fingerprints: findingIndexes.map(index => fingerprintOf(report.findings[index])), titles: [...displayFindings] } }, outputDir);
|
|
293
|
+
};
|
|
294
|
+
if (paths)
|
|
295
|
+
await saveContext();
|
|
296
|
+
}
|
|
200
297
|
if (resultCheck?.droppedFindings) {
|
|
201
298
|
const count = resultCheck.droppedFindings;
|
|
202
299
|
const reason = resultCheck.schemaDroppedFindings || resultCheck.capDroppedFindings
|
|
@@ -207,54 +304,106 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
207
304
|
catch { /* Output failure must not change a server outcome. */ }
|
|
208
305
|
}
|
|
209
306
|
completionOrigin = new URL(config.CF_CONSOLE_URL).origin;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
307
|
+
let saved;
|
|
308
|
+
if (retainUnsaved) {
|
|
309
|
+
completedPayload = payload;
|
|
310
|
+
completed = true;
|
|
311
|
+
const delivery = await deliverCompletedResults({ persist: uploaded => persist(payload, completionOrigin, uploaded),
|
|
312
|
+
submit: () => submit(payload), saveReports });
|
|
313
|
+
uploaded = delivery.uploaded;
|
|
314
|
+
saved = delivery.saved;
|
|
315
|
+
paths = delivery.paths;
|
|
316
|
+
localErrorMessage = delivery.localErrorMessage;
|
|
317
|
+
uploadOutcome = delivery.outcome;
|
|
318
|
+
completionTiming = delivery.timing ?? completionTiming;
|
|
319
|
+
if (delivery.error)
|
|
320
|
+
throw delivery.error;
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
await persist(payload, completionOrigin);
|
|
324
|
+
completedPayload = payload;
|
|
325
|
+
completed = true;
|
|
326
|
+
paths ??= await saveReports();
|
|
327
|
+
saved = await submit(payload);
|
|
328
|
+
uploaded = true;
|
|
329
|
+
await persist(payload, completionOrigin, true);
|
|
330
|
+
}
|
|
331
|
+
await settleSaved(saved);
|
|
332
|
+
stdout(formatSummary(options.addToAnalysisId ? await loadPublicReport(folder, outputDir) : { ...report, refusedFiles: skippedFiles }, summaryOptions));
|
|
217
333
|
stdout(`\nSaved ${paths.findingsPath}\nSaved ${paths.summaryPath}\n`);
|
|
218
334
|
showSaved(saved);
|
|
219
335
|
return 0;
|
|
220
336
|
}
|
|
221
|
-
catch (
|
|
337
|
+
catch (caught) {
|
|
338
|
+
let error = caught;
|
|
222
339
|
let status;
|
|
340
|
+
let statusLookup = { kind: "skipped" };
|
|
223
341
|
if (client && reservationId) {
|
|
224
342
|
try {
|
|
225
343
|
status = await (completed || uploaded ? client.status(reservationId) : client.fail(reservationId));
|
|
344
|
+
statusLookup = { kind: "found", status };
|
|
226
345
|
}
|
|
227
|
-
catch {
|
|
228
|
-
|
|
346
|
+
catch (statusError) {
|
|
347
|
+
statusLookup = { ...uploadFailure(statusError), kind: "failed" };
|
|
229
348
|
}
|
|
230
349
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
350
|
+
const sizeRefusal = error instanceof ConsoleError && ["plan_over_limit", "folder_too_large"].includes(error.code);
|
|
351
|
+
const closed = !reservationId || status && (status.state === "closed" || status.state === "expired");
|
|
352
|
+
if (scopeChoicesEnabled(settings) && !completedPayload && closed) {
|
|
353
|
+
const choice = error instanceof ScopeChoiceError ? error.choice
|
|
354
|
+
: sizeRefusal && membership ? selectionScopeChoices(membership, { currentPaths: selectedPaths,
|
|
355
|
+
oversizedPaths: membership.kind === "file" && selectedPaths?.length === 1 && selectedPaths[0] === membership.entry
|
|
356
|
+
? [membership.entry] : undefined }) : undefined;
|
|
357
|
+
if (choice) {
|
|
358
|
+
const refusal = scopeChoiceRefusal(choice);
|
|
359
|
+
if (refusal)
|
|
360
|
+
error = new Error(refusal);
|
|
361
|
+
else {
|
|
362
|
+
options.onScopeChoice?.(choice);
|
|
363
|
+
if (!options.onScopeChoice)
|
|
364
|
+
stderr(`${choice.text}\n`);
|
|
365
|
+
return SCOPE_CHOICE_EXIT_CODE;
|
|
235
366
|
}
|
|
236
|
-
catch { /* Committed server state still survives a local write failure. */ }
|
|
237
367
|
}
|
|
368
|
+
}
|
|
369
|
+
if (error instanceof ResultsUploadError)
|
|
370
|
+
completionTiming = error.timing ?? completionTiming;
|
|
371
|
+
const failure = await recoverRelayFailure({ error, retryEnabled: retainUnsaved, payload: completedPayload, completed,
|
|
372
|
+
timing: completionTiming, statusLookup, uploadOutcome, savedResult,
|
|
373
|
+
canWrite: !lostCompletionLock, completionStored, localErrorMessage, fileSelection: selection?.kind === "file", fileMode: fileScopeEnabled(settings) }, {
|
|
374
|
+
markUploaded: () => persist(completedPayload, completionOrigin, true),
|
|
375
|
+
keepPending: timing => { completionTiming = timing; return persist(completedPayload, completionOrigin); },
|
|
376
|
+
retire: () => retainUnsaved ? completionRecordPath ? discardCompletion(folder, completionRecordPath, completionIdentity) : Promise.resolve() : persist(completedPayload, completionOrigin, false, true),
|
|
377
|
+
});
|
|
378
|
+
if (failure.saved) {
|
|
238
379
|
try {
|
|
239
|
-
|
|
380
|
+
const saved = savedResult ?? { projectId: status.projectId, analysisId: status.analysisId, findingIds: status.findingIds, findings: status.findings };
|
|
381
|
+
await settleSaved(saved);
|
|
382
|
+
showSaved(saved);
|
|
240
383
|
}
|
|
241
384
|
catch { /* Output failure must not change a server outcome. */ }
|
|
242
|
-
|
|
385
|
+
if (!failure.message)
|
|
386
|
+
return 0;
|
|
387
|
+
try {
|
|
388
|
+
options.onError?.({ reason: failure.reason, message: failure.message });
|
|
389
|
+
stderr(`${failure.message}\n`);
|
|
390
|
+
}
|
|
391
|
+
catch { /* Reporting failure cannot undo saved results. */ }
|
|
392
|
+
return 1;
|
|
243
393
|
}
|
|
244
|
-
|
|
245
|
-
if (
|
|
394
|
+
let scopeChoice;
|
|
395
|
+
if (!reservationId && !existing && error instanceof ConsoleError && (error.code === "folder_too_large" || error.code === "plan_over_limit")) {
|
|
246
396
|
try {
|
|
247
|
-
await
|
|
397
|
+
const choice = await prepareFolderChoice(folder, { ...options, settings, signal, limit: error.code, refusedCalls });
|
|
398
|
+
if (choice)
|
|
399
|
+
scopeChoice = new AgentChoiceError(choice);
|
|
248
400
|
}
|
|
249
|
-
catch { /*
|
|
401
|
+
catch { /* A failed local plan preserves the console's authoritative refusal. */ }
|
|
250
402
|
}
|
|
251
|
-
const known = error instanceof AgentChoiceError || error instanceof ConsoleError || error instanceof OutputPathError || error instanceof Error && ([cliRefusals.release_unavailable, COMMITTED_FILES_MESSAGE, RESULTS_TOO_LARGE_MESSAGE, INVALID_PROPOSAL_MESSAGE].includes(error.message)
|
|
252
|
-
|| error.message.startsWith("The saved completion") || error.message === COMPLETION_SETTINGS_MISMATCH_MESSAGE);
|
|
253
|
-
const message = known ? error.message : FINISH_FAILURE_MESSAGE;
|
|
254
403
|
try {
|
|
255
|
-
options.onError?.({ code: error instanceof ConsoleError ? error.code : undefined, reason:
|
|
256
|
-
...(error instanceof AgentChoiceError ? { agentChoice: error.agentChoice } : {}) });
|
|
257
|
-
stderr(`${message}\n`);
|
|
404
|
+
options.onError?.({ code: error instanceof ConsoleError ? error.code : undefined, reason: failure.reason, message: scopeChoice?.message ?? failure.message,
|
|
405
|
+
...(scopeChoice ? { agentChoice: scopeChoice.agentChoice } : error instanceof AgentChoiceError ? { agentChoice: error.agentChoice } : {}) });
|
|
406
|
+
stderr(`${scopeChoice?.message ?? failure.message}\n`);
|
|
258
407
|
if (status && !(error instanceof ConsoleError && error.code === "low_balance"))
|
|
259
408
|
stderr(stateLine(status));
|
|
260
409
|
}
|
|
@@ -264,6 +413,7 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
264
413
|
finally {
|
|
265
414
|
clearInterval(heartbeat);
|
|
266
415
|
await renewal;
|
|
416
|
+
await releaseCompletion?.().catch(() => { });
|
|
267
417
|
try {
|
|
268
418
|
connection?.close();
|
|
269
419
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CompletionBusyError } from "../adapters/completion-lock.js";
|
|
2
|
+
import { OutputPathError } from "../adapters/output-paths.js";
|
|
3
|
+
import { AgentChoiceError } from "../contract/agent-choice.js";
|
|
4
|
+
import { INVALID_PROPOSAL_MESSAGE } from "../contract/agent-proposal.js";
|
|
5
|
+
import { cliRefusals, COMMITTED_FILES_MESSAGE, failureReason, FINISH_FAILURE_MESSAGE, localFailure, RESULTS_TOO_LARGE_MESSAGE } from "../contract/cli-refusals.js";
|
|
6
|
+
import { isSelectionRefusal, selectionRefusalMessage } from "../contract/scope-choice.js";
|
|
7
|
+
import { ConsoleError } from "../gatekeeper/console.js";
|
|
8
|
+
import { recoverCompletedResults } from "./completion-delivery.js";
|
|
9
|
+
import { CompletionSettingsMismatchError } from "./prior-completion.js";
|
|
10
|
+
import { ResultsUploadError } from "./results-upload.js";
|
|
11
|
+
/** Cleanup reports the recovery owner's decision without exposing private diagnostics. */
|
|
12
|
+
export async function recoverRelayFailure(input, operations) {
|
|
13
|
+
const { error, retryEnabled } = input;
|
|
14
|
+
const carried = retryEnabled ? input.uploadOutcome ?? (error instanceof ResultsUploadError ? error.outcome : undefined) : undefined;
|
|
15
|
+
const recovery = input.payload && input.completed ? await recoverCompletedResults({
|
|
16
|
+
retryEnabled, payload: input.payload, timing: input.timing, statusLookup: input.statusLookup,
|
|
17
|
+
uploadOutcome: retryEnabled && input.savedResult ? { kind: "saved" } : carried ?? { kind: "cancelled" },
|
|
18
|
+
canWrite: input.canWrite, completionStored: input.completionStored, localErrorMessage: input.localErrorMessage,
|
|
19
|
+
}, operations) : undefined;
|
|
20
|
+
if (recovery?.decision.action === "saved")
|
|
21
|
+
return { saved: true, message: recovery.message, reason: "results_saved_report_failed" };
|
|
22
|
+
const known = error instanceof AgentChoiceError || error instanceof ConsoleError || error instanceof OutputPathError
|
|
23
|
+
|| isSelectionRefusal(error, input.fileSelection ?? false)
|
|
24
|
+
|| error instanceof CompletionBusyError || error instanceof CompletionSettingsMismatchError || error instanceof Error
|
|
25
|
+
&& ([cliRefusals.release_unavailable, COMMITTED_FILES_MESSAGE, RESULTS_TOO_LARGE_MESSAGE, INVALID_PROPOSAL_MESSAGE].includes(error.message)
|
|
26
|
+
|| error.message.startsWith("The saved completion"));
|
|
27
|
+
const message = recovery?.message ?? input.localErrorMessage ?? localFailure(error)?.message ?? (known ? error.message : FINISH_FAILURE_MESSAGE);
|
|
28
|
+
const reason = !retryEnabled ? failureReason(error)
|
|
29
|
+
: recovery?.decision.action === "retry" && input.completionStored ? "results_pending"
|
|
30
|
+
: recovery?.decision.action === "give-up" ? "results_discarded" : failureReason(error);
|
|
31
|
+
return { saved: false, message: selectionRefusalMessage(message, input.fileMode ?? true), reason };
|
|
32
|
+
}
|