fix(cso): count remainingSeconds to the investigation deadline; bound and validate the tool timeline

remainingSeconds now measures to deadline - 60s, the point where scan,
verify and record-review refuse with DEADLINE, and inspect carries it.
The post-command clock lookup can only omit the field, never fail a
command whose state change is already saved. The stream timeline records
end times from tool_result, indexes only recorded calls so it stays at
the 4096 cap, and receipts reject malformed timeline entries.
This commit is contained in:
garrytan committed 2026-10-17 05:37:17 +00:00
1 parent 6a432f00a0
commit d804fbb2fc
11 files changed
+90 -34

No files matched your search

+29 -17
View File
@@ -57,6 +57,7 @@ import {
readJson,
reclaimDeadAttemptScratch,
repoId,
executionDeadline,
requireTime,
retention,
runDirectory,
@@ -1609,6 +1610,7 @@ async function inspect(args: string[]) {
? readJson(join(dir, 'preparation.json'))
: undefined,
recovery: recoveryEvents(dir),
remainingSeconds: remainingSeconds(report.deadline),
});
}
async function read(args: string[]) {
@@ -2796,6 +2798,24 @@ async function replay(args: string[], dependencies: CsoCliDependencies) {
});
}
/** In-run replies that report the investigation time left; every one but `start` takes the run ID first. */
const RUN_CLOCK_COMMANDS = new Set([
'start',
'resume',
'submit',
'scan',
'scanner-outcome',
'import-sarif',
'record-review',
'test-plan',
'runtime-plan',
'verify',
]);
/** Whole seconds until the investigation deadline (the final reporting minute excluded), or undefined if unknown. */
function remainingSeconds(deadline: string): number | undefined {
const ends = executionDeadline({ deadline });
return Number.isFinite(ends) ? Math.max(0, Math.floor((ends - Date.now()) / 1000)) : undefined;
}
export async function dispatchCsoCommand(
command: string,
args: string[],
@@ -2877,27 +2897,19 @@ export async function dispatchCsoCommand(
default:
throw new CsoError('INVALID_ARGUMENT', `Unknown command: ${command}`);
}
if (RUN_CLOCK_COMMANDS.has(command) && result && typeof result === 'object' && !Array.isArray(result)) {
const deadline = (result as { deadline?: unknown }).deadline;
const ends = Date.parse(
if (!RUN_CLOCK_COMMANDS.has(command) || !result || typeof result !== 'object' || Array.isArray(result))
return result;
const deadline = (result as { deadline?: unknown }).deadline;
let seconds: number | undefined;
try {
seconds = remainingSeconds(
typeof deadline === 'string' ? deadline : loadReport(runDirectory(runId)).deadline,
);
result = { ...result, remainingSeconds: Math.max(0, Math.floor((ends - Date.now()) / 1000)) };
} catch {
seconds = undefined;
}
return result;
return seconds === undefined ? result : { ...result, remainingSeconds: seconds };
}
const RUN_CLOCK_COMMANDS = new Set([
'start',
'resume',
'submit',
'scan',
'scanner-outcome',
'import-sarif',
'record-review',
'test-plan',
'runtime-plan',
'verify',
]);
const PRODUCTION_CLI_DEPENDENCIES: CsoCliDependencies = Object.freeze({
runtimeCatalog: RUNTIME_CATALOG,
scannerCatalog: SCANNER_CATALOG,
+1 -1
View File
@@ -944,7 +944,7 @@ export function loadReport(dir: string): RunReportV3 {
export function event(report: RunReportV3, kind: string, message: string): void {
report.events.push({ at: new Date().toISOString(), kind, message: redact(message) });
}
export function executionDeadline(report: RunReportV3): number {
export function executionDeadline(report: Pick<RunReportV3, 'deadline'>): number {
return Date.parse(report.deadline) - 60_000;
}
export function requireTime(report: RunReportV3): void {