@claudexor/control-api 3.1.1 → 3.2.0
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/authorization.d.ts +3 -0
- package/dist/authorization.d.ts.map +1 -0
- package/dist/authorization.js +15 -0
- package/dist/authorization.js.map +1 -0
- package/dist/binary-response.d.ts +5 -0
- package/dist/binary-response.d.ts.map +1 -0
- package/dist/binary-response.js +31 -0
- package/dist/binary-response.js.map +1 -0
- package/dist/budget-valuation.d.ts +23 -0
- package/dist/budget-valuation.d.ts.map +1 -0
- package/dist/budget-valuation.js +90 -0
- package/dist/budget-valuation.js.map +1 -0
- package/dist/catalog-query.d.ts +310 -0
- package/dist/catalog-query.d.ts.map +1 -0
- package/dist/catalog-query.js +36 -0
- package/dist/catalog-query.js.map +1 -0
- package/dist/daemon-server.d.ts +33 -79
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +175 -281
- package/dist/daemon-server.js.map +1 -1
- package/dist/decision-accept-risk.d.ts +36 -0
- package/dist/decision-accept-risk.d.ts.map +1 -0
- package/dist/decision-accept-risk.js +79 -0
- package/dist/decision-accept-risk.js.map +1 -0
- package/dist/decision-rerun.d.ts +2 -1
- package/dist/decision-rerun.d.ts.map +1 -1
- package/dist/decision-rerun.js +79 -48
- package/dist/decision-rerun.js.map +1 -1
- package/dist/delegation-control.d.ts +23 -0
- package/dist/delegation-control.d.ts.map +1 -0
- package/dist/delegation-control.js +81 -0
- package/dist/delegation-control.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/maintenance-routes.d.ts +2 -2
- package/dist/maintenance-routes.d.ts.map +1 -1
- package/dist/maintenance-routes.js +11 -10
- package/dist/maintenance-routes.js.map +1 -1
- package/dist/operation-catalog.d.ts +9 -9
- package/dist/operation-catalog.d.ts.map +1 -1
- package/dist/operation-catalog.js +22 -79
- package/dist/operation-catalog.js.map +1 -1
- package/dist/operation-draft.d.ts +3 -0
- package/dist/operation-draft.d.ts.map +1 -0
- package/dist/operation-draft.js +2 -0
- package/dist/operation-draft.js.map +1 -0
- package/dist/operation-parameters.d.ts.map +1 -1
- package/dist/operation-summaries.d.ts +3 -0
- package/dist/operation-summaries.d.ts.map +1 -0
- package/dist/operation-summaries.js +75 -0
- package/dist/operation-summaries.js.map +1 -0
- package/dist/primary-output.d.ts +3 -3
- package/dist/primary-output.d.ts.map +1 -1
- package/dist/primary-output.js +70 -19
- package/dist/primary-output.js.map +1 -1
- package/dist/problem-response.d.ts.map +1 -1
- package/dist/problem-response.js +14 -9
- package/dist/problem-response.js.map +1 -1
- package/dist/project-routes.d.ts +21 -13
- package/dist/project-routes.d.ts.map +1 -1
- package/dist/project-routes.js +80 -34
- package/dist/project-routes.js.map +1 -1
- package/dist/remote-operation-descriptors.d.ts +36 -0
- package/dist/remote-operation-descriptors.d.ts.map +1 -0
- package/dist/remote-operation-descriptors.js +35 -0
- package/dist/remote-operation-descriptors.js.map +1 -0
- package/dist/resource-routes.d.ts +4 -3
- package/dist/resource-routes.d.ts.map +1 -1
- package/dist/resource-routes.js +63 -39
- package/dist/resource-routes.js.map +1 -1
- package/dist/route-stages.d.ts +15 -0
- package/dist/route-stages.d.ts.map +1 -0
- package/dist/route-stages.js +24 -0
- package/dist/route-stages.js.map +1 -0
- package/dist/run-facts-projection.d.ts +35 -0
- package/dist/run-facts-projection.d.ts.map +1 -0
- package/dist/run-facts-projection.js +133 -0
- package/dist/run-facts-projection.js.map +1 -0
- package/dist/run-list-fingerprint.d.ts +7 -0
- package/dist/run-list-fingerprint.d.ts.map +1 -0
- package/dist/run-list-fingerprint.js +68 -0
- package/dist/run-list-fingerprint.js.map +1 -0
- package/dist/run-record.d.ts +53 -0
- package/dist/run-record.d.ts.map +1 -0
- package/dist/run-record.js +61 -0
- package/dist/run-record.js.map +1 -0
- package/dist/run-retry-routes.d.ts +3 -2
- package/dist/run-retry-routes.d.ts.map +1 -1
- package/dist/run-retry-routes.js +129 -48
- package/dist/run-retry-routes.js.map +1 -1
- package/dist/run-start.d.ts +13 -1
- package/dist/run-start.d.ts.map +1 -1
- package/dist/run-start.js +90 -18
- package/dist/run-start.js.map +1 -1
- package/dist/run-timeline.d.ts.map +1 -1
- package/dist/run-timeline.js +17 -9
- package/dist/run-timeline.js.map +1 -1
- package/dist/thread-lifecycle-routes.d.ts +1 -1
- package/dist/thread-lifecycle-routes.d.ts.map +1 -1
- package/dist/thread-lifecycle-routes.js +3 -4
- package/dist/thread-lifecycle-routes.js.map +1 -1
- package/dist/thread-mutation.d.ts +6 -0
- package/dist/thread-mutation.d.ts.map +1 -1
- package/dist/thread-mutation.js +23 -0
- package/dist/thread-mutation.js.map +1 -1
- package/dist/thread-projection.d.ts +6 -1
- package/dist/thread-projection.d.ts.map +1 -1
- package/dist/thread-projection.js +28 -1
- package/dist/thread-projection.js.map +1 -1
- package/dist/thread-recovery.d.ts +54 -0
- package/dist/thread-recovery.d.ts.map +1 -0
- package/dist/thread-recovery.js +71 -0
- package/dist/thread-recovery.js.map +1 -0
- package/dist/thread-turn-routes.d.ts +22 -8
- package/dist/thread-turn-routes.d.ts.map +1 -1
- package/dist/thread-turn-routes.js +73 -49
- package/dist/thread-turn-routes.js.map +1 -1
- package/package.json +6 -6
package/dist/daemon-server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync,
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { basename, extname, join } from "node:path";
|
|
5
5
|
import { deriveApplyEligibility, revertInPlaceFromAnchor, validateApplyGate, verifyAndDeliver, } from "@claudexor/delivery";
|
|
@@ -8,31 +8,43 @@ import { safeArtifactPath, safeArtifactRoot } from "./artifact-paths.js";
|
|
|
8
8
|
import { TERMINAL_STATES } from "./sse-shared.js";
|
|
9
9
|
import { streamRunEvents } from "./run-events-stream.js";
|
|
10
10
|
import { boundedArtifactText, outputReadyState, primaryOutput } from "./primary-output.js";
|
|
11
|
+
import { budgetValuationFromEvents, cashEstimatedFromLedgerEvent, normalizeLegacyBudgetComponents, } from "./budget-valuation.js";
|
|
11
12
|
import { controlWebEvidence, eventPayload, evidenceLevel, latestPlanProgress, readRunEvents, readRunEventsWithIntegrity, timelineEvents, } from "./run-timeline.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { projectTurnRunCards } from "./thread-projection.js";
|
|
14
|
+
import { projectSession, projectThread, projectTurn } from "./thread-projection.js";
|
|
15
|
+
import { handleThreadTurnCreate, handleThreadTurnRetry, } from "./thread-turn-routes.js";
|
|
16
|
+
import { chainIdleRunMutation, chainThreadMutation } from "./thread-mutation.js";
|
|
15
17
|
import { handleThreadLifecycleRoutes, } from "./thread-lifecycle-routes.js";
|
|
16
18
|
import * as runStart from "./run-start.js";
|
|
19
|
+
import { cancelDelegationFamily } from "./delegation-control.js";
|
|
20
|
+
import { directDelegatedChildrenFromRecords, delegatedDescendantsFromRecords, paramsRecord, } from "./run-record.js";
|
|
17
21
|
import { handleRunRetryRoute } from "./run-retry-routes.js";
|
|
18
22
|
import { handleRunApplyRoutes, runIdempotentDelivery, } from "./run-apply-routes.js";
|
|
19
23
|
import { rerunWithFeedback } from "./decision-rerun.js";
|
|
24
|
+
import { acceptRiskDecision } from "./decision-accept-risk.js";
|
|
20
25
|
export { normalizeRunStartRequest } from "./run-start.js";
|
|
21
26
|
import { candidatesFor } from "./candidates.js";
|
|
22
27
|
import { handleProjectRoute } from "./project-routes.js";
|
|
28
|
+
import { writeBinaryResponse } from "./binary-response.js";
|
|
29
|
+
export { inlineContentDisposition } from "./binary-response.js";
|
|
23
30
|
import { handleRecoveryRoute } from "./recovery-routes.js";
|
|
24
31
|
import { handleJournalEventRoute } from "./journal-event-routes.js";
|
|
25
32
|
import { handleMaintenanceRoute } from "./maintenance-routes.js";
|
|
26
33
|
import { handleResourceRoute } from "./resource-routes.js";
|
|
27
34
|
import { handleArtifactServeRoute, listArtifacts, resolveProjectRoot, } from "./artifact-serve-routes.js";
|
|
28
35
|
import { requiredGateSpecsFromTaskArtifact } from "./task-contract-gates.js";
|
|
29
|
-
import {
|
|
36
|
+
import { bearerCredential } from "./authorization.js";
|
|
37
|
+
import { assertOnlyQueryParams, singleQuery } from "./query.js";
|
|
38
|
+
import { parseCredentialProfilesSnapshotQuery, parseHarnessListQuery, parseRunApplicabilityQuery, } from "./catalog-query.js";
|
|
30
39
|
import { lruGet, lruSet, parseRunListQuery, selectRunListPage, } from "./run-list.js";
|
|
40
|
+
import { summaryFingerprint } from "./run-list-fingerprint.js";
|
|
41
|
+
export { resetRunListFingerprintProbeCountForTests, runListFingerprintProbeCountForTests, } from "./run-list-fingerprint.js";
|
|
31
42
|
import { controlProblemError, normalizeRequestValidationError, revertRefusedProblem, thrownProblemCode, } from "./problem-response.js";
|
|
32
43
|
import { handleSecurityRoute } from "./security-routes.js";
|
|
33
|
-
import {
|
|
44
|
+
import { effectiveTerminalFacts, expectedRunFacts, projectRunFactsForDetail, } from "./run-facts-projection.js";
|
|
45
|
+
import { PlanQuestionsArtifact, CouncilProjection, derivePlanReadiness, ControlAuthReadinessRefreshRequest, ControlAuthReadinessRefreshResponse, ControlProblem, AccessProfile, AgentCapabilityCatalog, ControlHarnessListResponse, ControlHarnessModelsResponse, ControlSetupJob, ControlSetupJobCreateRequest, ControlSetupJobEvent, ControlSetupJobSnapshot, isTerminalControlSetupJobState, ControlSetupJobListFilter, ControlSetupJobListResponse, ControlRunStartInfo, ControlRunControlRequest, ControlRunControlResponse, ControlReviewerPanelEntry, ControlRunDetail, ControlRunListResponse, ControlRunSummary, ControlRunResult, ControlBudgetSnapshot, PaidBudget, ControlSettingsSnapshot, ControlSettingsUpdateRequest, ControlQuotaResponse, ControlCredentialProfileCreateRequest, ControlCredentialProfileCreateResponse, ControlCredentialProfileUpdateRequest, ControlCredentialProfileUpdateResponse, ControlCredentialProfileDeleteResponse, ControlInteractionAnswerRequest, ControlInteractionAnswerResponse, ControlRunDecisionRequest, ControlRunDecisionResponse, ControlRunApplicabilityResponse, ControlThreadCreateRequest, ControlThreadTurnRequest, ControlThreadUpdateRequest, ControlThreadDetail, ControlThreadListResponse, DecisionRecord, Id, ModeKind, RoutingGoal, ReviewFinding, RunDeliveryState, RunFailure, RunTelemetry, StructuredOutputConformance, TaskContract, ProtectedPathApproval, isTerminalLifecycle, needsDecision, needsOperatorAttention, outcomeBanner, outcomeFactsFromFailure, TestCommandInvocation, WorkProduct, } from "@claudexor/schema";
|
|
34
46
|
import { resolveControlProtocol } from "./operation-catalog.js";
|
|
35
|
-
import { assertNoInlineSecretValues, containsSecretLikeToken, errorCode, noProjectRepoRoot, nowIso, redactSecrets, sha256, } from "@claudexor/util";
|
|
47
|
+
import { assertNoInlineSecretValues, containsSecretLikeToken, errorCode, noProjectRepoRoot, nowIso, redactSecrets, safeProblemContext, safeProblemRequiredActions, sha256, } from "@claudexor/util";
|
|
36
48
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
37
49
|
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
38
50
|
const NO_PROJECT_ROOT = noProjectRepoRoot();
|
|
@@ -59,6 +71,8 @@ function stringArrayProperty(error, key) {
|
|
|
59
71
|
if (!error || typeof error !== "object" || !(key in error))
|
|
60
72
|
return [];
|
|
61
73
|
const value = error[key];
|
|
74
|
+
if (key === "requiredActions")
|
|
75
|
+
return safeProblemRequiredActions(value);
|
|
62
76
|
return Array.isArray(value)
|
|
63
77
|
? value
|
|
64
78
|
.filter((item) => typeof item === "string" && item.length > 0)
|
|
@@ -96,7 +110,7 @@ function problemBody(error, fallbackCode, fallbackRetryable, fallbackMessage) {
|
|
|
96
110
|
requiredActions: stringArrayProperty(error, "requiredActions"),
|
|
97
111
|
evidenceRefs: stringArrayProperty(error, "evidenceRefs"),
|
|
98
112
|
context: error && typeof error === "object" && "context" in error
|
|
99
|
-
? (error.context
|
|
113
|
+
? safeProblemContext(error.context)
|
|
100
114
|
: {},
|
|
101
115
|
});
|
|
102
116
|
}
|
|
@@ -252,15 +266,12 @@ export class DaemonControlApiServer {
|
|
|
252
266
|
if (!hostIsLoopback(req.headers.host) ||
|
|
253
267
|
!originIsLoopback(req.headers.origin))
|
|
254
268
|
return false;
|
|
255
|
-
|
|
256
|
-
return this.tokenMatches(m?.[1]?.trim());
|
|
269
|
+
return this.tokenMatches(bearerCredential(req.headers.authorization));
|
|
257
270
|
}
|
|
258
|
-
requestError(res, err) {
|
|
259
|
-
// QA-053:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const normalized = normalizeRequestValidationError(err);
|
|
263
|
-
this.problem(res, finiteHttpStatus(normalized, 400), normalized, "invalid_request", false, "bad request");
|
|
271
|
+
requestError(res, err, fallbackStatus = 400) {
|
|
272
|
+
// QA-053: normalize raw Zod issues only at the client-request boundary.
|
|
273
|
+
const normalized = fallbackStatus === 400 ? normalizeRequestValidationError(err) : err;
|
|
274
|
+
this.problem(res, finiteHttpStatus(normalized, fallbackStatus), normalized, fallbackStatus === 400 ? "invalid_request" : "internal_error", false, fallbackStatus === 400 ? "bad request" : "service failed");
|
|
264
275
|
}
|
|
265
276
|
json(res, status, body, contentType = "application/json") {
|
|
266
277
|
if (status >= 400 && contentType === "application/json") {
|
|
@@ -375,21 +386,22 @@ export class DaemonControlApiServer {
|
|
|
375
386
|
services: this.opts.services,
|
|
376
387
|
readBody: (request) => this.readBody(request),
|
|
377
388
|
json: (response, status, body) => this.json(response, status, body),
|
|
378
|
-
requestError: (response, error) => this.requestError(response, error),
|
|
389
|
+
requestError: (response, error, fallback) => this.requestError(response, error, fallback),
|
|
379
390
|
}, method, path, req, res))
|
|
380
391
|
return;
|
|
381
392
|
if (await handleMaintenanceRoute({
|
|
382
393
|
services: this.opts.services,
|
|
383
394
|
readBody: (request) => this.readBody(request),
|
|
384
395
|
json: (response, status, body) => this.json(response, status, body),
|
|
385
|
-
requestError: (response, error) => this.requestError(response, error),
|
|
396
|
+
requestError: (response, error, fallback) => this.requestError(response, error, fallback),
|
|
386
397
|
}, method, path, req, res))
|
|
387
398
|
return;
|
|
388
399
|
if (await handleProjectRoute({
|
|
389
400
|
services: this.opts.services,
|
|
390
401
|
readBody: (request) => this.readBody(request),
|
|
391
402
|
json: (response, status, body) => this.json(response, status, body),
|
|
392
|
-
requestError: (response, error) => this.requestError(response, error),
|
|
403
|
+
requestError: (response, error, fallback) => this.requestError(response, error, fallback),
|
|
404
|
+
binary: writeBinaryResponse,
|
|
393
405
|
}, method, path, req, res))
|
|
394
406
|
return;
|
|
395
407
|
if (method === "POST" && path === "/runs") {
|
|
@@ -411,8 +423,9 @@ export class DaemonControlApiServer {
|
|
|
411
423
|
services: this.opts.services,
|
|
412
424
|
findRun: (id) => this.findRun(id),
|
|
413
425
|
waitForRunStart: (id) => this.waitForRunStart(id),
|
|
426
|
+
serializeThreadMutation: (threadId, work) => chainThreadMutation(this.threadTurnChains, threadId, work),
|
|
414
427
|
json: (response, status, body) => this.json(response, status, body),
|
|
415
|
-
requestError: (response, error) => this.requestError(response, error),
|
|
428
|
+
requestError: (response, error, fallbackStatus) => this.requestError(response, error, fallbackStatus),
|
|
416
429
|
}, method, path, req, res))
|
|
417
430
|
return;
|
|
418
431
|
const runDetailMatch = /^\/runs\/([^/]+)$/.exec(path);
|
|
@@ -423,7 +436,9 @@ export class DaemonControlApiServer {
|
|
|
423
436
|
// Fence order: cursor FIRST, every projection (pending interactions
|
|
424
437
|
// included) after it — see the detailFor doc comment.
|
|
425
438
|
const lastSeq = rec.runDir ? lastSeqInFile(join(rec.runDir, "events.jsonl")) : 0;
|
|
426
|
-
|
|
439
|
+
const parentRunId = rec.runId ?? rec.id;
|
|
440
|
+
const children = directDelegatedChildrenFromRecords(parentRunId, await this.opts.daemon.list()).map((candidate) => this.summarizeRunOrDiagnostic(candidate));
|
|
441
|
+
return this.json(res, 200, detailFor(rec, this.pendingInteractionsFor(rec), lastSeq, this.validOperatorDecisionFor(rec), children));
|
|
427
442
|
}
|
|
428
443
|
const interactionAnswerMatch = /^\/runs\/([^/]+)\/interactions\/([^/]+)\/answer$/.exec(path);
|
|
429
444
|
if (method === "POST" && interactionAnswerMatch) {
|
|
@@ -481,13 +496,7 @@ export class DaemonControlApiServer {
|
|
|
481
496
|
const idempotencyKey = runStart.requiredIdempotencyKey(req);
|
|
482
497
|
let repoRoot = null;
|
|
483
498
|
if (parsed.scope.kind === "project") {
|
|
484
|
-
repoRoot = parsed.scope.root
|
|
485
|
-
const absoluteRepoError = runStart.validateAbsoluteRepoRoot(repoRoot);
|
|
486
|
-
if (absoluteRepoError)
|
|
487
|
-
throw Object.assign(new Error(absoluteRepoError), { status: 400 });
|
|
488
|
-
if (!existsSync(repoRoot) || !lstatSync(repoRoot).isDirectory()) {
|
|
489
|
-
throw Object.assign(new Error(`project root does not exist or is not a directory: ${repoRoot}`), { status: 400 });
|
|
490
|
-
}
|
|
499
|
+
repoRoot = runStart.normalizeExistingProjectRoot(parsed.scope.root);
|
|
491
500
|
}
|
|
492
501
|
const thread = await svc({
|
|
493
502
|
title: parsed.title,
|
|
@@ -533,15 +542,7 @@ export class DaemonControlApiServer {
|
|
|
533
542
|
const runs = await this.opts.daemon.list();
|
|
534
543
|
const byRun = new Map(runs.map((r) => [r.runId ?? r.id, r]));
|
|
535
544
|
const thread = detail.thread;
|
|
536
|
-
const cards =
|
|
537
|
-
for (const turn of detail.turns) {
|
|
538
|
-
const runId = turn.run_id ?? null;
|
|
539
|
-
if (runId && !cards.has(runId)) {
|
|
540
|
-
const rec = byRun.get(runId);
|
|
541
|
-
if (rec)
|
|
542
|
-
cards.set(runId, turnRunCard(this.summarizeRunOrDiagnostic(rec)));
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
+
const cards = projectTurnRunCards(detail.turns, runs, (record) => this.summarizeRunOrDiagnostic(record));
|
|
545
546
|
const headRec = byRun.get(thread.head_run_id ?? "");
|
|
546
547
|
const headNeedsHuman = headRec ? this.runNeedsAttention(headRec) : false;
|
|
547
548
|
return this.json(res, 200, ControlThreadDetail.parse({
|
|
@@ -633,70 +634,17 @@ export class DaemonControlApiServer {
|
|
|
633
634
|
return this.requestError(res, err);
|
|
634
635
|
}
|
|
635
636
|
if (body.action === "accept_risk" || body.action === "override_needs_human") {
|
|
636
|
-
const decisionBody = body;
|
|
637
|
-
const decisionAction = body.action;
|
|
638
637
|
try {
|
|
639
|
-
return await
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
? "run reported it needs more input; a risk override cannot supply the missing input — re-run with the input (rerun_with_feedback)"
|
|
650
|
-
: "run reported the work is incomplete; a risk override cannot finish it — re-run until it completes (rerun_with_feedback)",
|
|
651
|
-
code: "work_state_needs_input",
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
// The override unblocks a NEEDS-DECISION run (review blocked /
|
|
655
|
-
// checks failed, D8); recording one elsewhere would claim an apply
|
|
656
|
-
// permission that does not exist.
|
|
657
|
-
if (!this.runNeedsDecision(rec)) {
|
|
658
|
-
return this.json(res, 409, {
|
|
659
|
-
error: rec.state === "succeeded"
|
|
660
|
-
? "run does not need a decision; apply it directly if its review is clean (no risk override needed)"
|
|
661
|
-
: `run is ${rec.state}; risk overrides only unblock needs-decision runs (use rerun_with_feedback instead)`,
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
const patch = readPatch(rec);
|
|
665
|
-
if (patch === null)
|
|
666
|
-
return this.json(res, 409, {
|
|
667
|
-
error: "no patch artifact; there is nothing to unblock for apply",
|
|
668
|
-
});
|
|
669
|
-
const decision = this.recordOperatorDecision(rec, {
|
|
670
|
-
action: decisionAction,
|
|
671
|
-
findingIds: decisionBody.findingIds,
|
|
672
|
-
acceptedRisks: decisionBody.acceptedRisks,
|
|
673
|
-
patchSha256: sha256(patch),
|
|
674
|
-
decidedAt: nowIso(),
|
|
675
|
-
}, {
|
|
676
|
-
key: decisionKey,
|
|
677
|
-
client: "control-api",
|
|
678
|
-
request: { runId: rec.runId ?? rec.id, body: decisionBody },
|
|
679
|
-
});
|
|
680
|
-
try {
|
|
681
|
-
writeOperatorDecisionProjection(rec, decision);
|
|
682
|
-
appendRunAuditEvent(rec, "control.applied", {
|
|
683
|
-
decision: decisionAction,
|
|
684
|
-
finding_ids: decisionBody.findingIds,
|
|
685
|
-
accepted_risks: decisionBody.acceptedRisks,
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
catch {
|
|
689
|
-
// Journal authority remains queryable and replayable by Idempotency-Key.
|
|
690
|
-
}
|
|
691
|
-
return this.json(res, 200, ControlRunDecisionResponse.parse({
|
|
692
|
-
accepted: true,
|
|
693
|
-
status: "applied",
|
|
694
|
-
// Honest ACK (QA-032B): nothing is mutated at decision time. The
|
|
695
|
-
// risk is accepted for this exact patch; Apply is now available
|
|
696
|
-
// and a fresh final check runs just-in-time before any mutation.
|
|
697
|
-
message: `${decisionAction} recorded for this exact patch; Apply is now available and will run a fresh final check before changing the project`,
|
|
698
|
-
}));
|
|
699
|
-
});
|
|
638
|
+
return await acceptRiskDecision({
|
|
639
|
+
services: this.opts.services,
|
|
640
|
+
chainMutation: (record, work) => this.chainRunMutation(record, work),
|
|
641
|
+
workStateVeto: (record) => this.runWorkStateVeto(record),
|
|
642
|
+
needsDecision: (record) => this.runNeedsDecision(record),
|
|
643
|
+
readPatch,
|
|
644
|
+
writeProjection: writeOperatorDecisionProjection,
|
|
645
|
+
appendAudit: (record, payload) => appendRunAuditEvent(record, "control.applied", payload),
|
|
646
|
+
json: (response, status, responseBody) => this.json(response, status, responseBody),
|
|
647
|
+
}, rec, body, decisionKey, res);
|
|
700
648
|
}
|
|
701
649
|
catch (error) {
|
|
702
650
|
return this.requestError(res, error);
|
|
@@ -814,28 +762,23 @@ export class DaemonControlApiServer {
|
|
|
814
762
|
return this.requestError(res, error);
|
|
815
763
|
}
|
|
816
764
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
765
|
+
try {
|
|
766
|
+
return await rerunWithFeedback({
|
|
767
|
+
daemon: this.opts.daemon,
|
|
768
|
+
services: this.opts.services,
|
|
769
|
+
serializeThreadMutation: (threadId, work) => chainThreadMutation(this.threadTurnChains, threadId, work),
|
|
770
|
+
waitForRunStart: (id) => this.waitForRunStart(id),
|
|
771
|
+
appendAudit: (record, payload) => appendRunAuditEvent(record, "control.applied", payload),
|
|
772
|
+
json: (response, status, responseBody) => this.json(response, status, responseBody),
|
|
773
|
+
}, rec, body, decisionKey, res);
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
return this.requestError(res, error, 500);
|
|
777
|
+
}
|
|
824
778
|
}
|
|
825
779
|
if (method === "GET" && path === "/harnesses") {
|
|
826
780
|
try {
|
|
827
|
-
|
|
828
|
-
const fresh = optionalBooleanQuery(url, "fresh");
|
|
829
|
-
const includeFakes = optionalBooleanQuery(url, "all");
|
|
830
|
-
const harnessIds = url.searchParams
|
|
831
|
-
.getAll("harness")
|
|
832
|
-
.map((id) => id.trim())
|
|
833
|
-
.filter(Boolean);
|
|
834
|
-
return this.service(res, "harnesses", {
|
|
835
|
-
...(fresh === undefined ? {} : { fresh }),
|
|
836
|
-
...(includeFakes === undefined ? {} : { includeFakes }),
|
|
837
|
-
...(harnessIds.length === 0 ? {} : { harnessIds }),
|
|
838
|
-
}, ControlHarnessListResponse);
|
|
781
|
+
return this.service(res, "harnesses", parseHarnessListQuery(url), ControlHarnessListResponse);
|
|
839
782
|
}
|
|
840
783
|
catch (err) {
|
|
841
784
|
return this.requestError(res, err);
|
|
@@ -843,6 +786,14 @@ export class DaemonControlApiServer {
|
|
|
843
786
|
}
|
|
844
787
|
if (method === "GET" && path === "/agent-capabilities")
|
|
845
788
|
return this.service(res, "agentCapabilities", undefined, AgentCapabilityCatalog);
|
|
789
|
+
if (method === "GET" && path === "/run-applicability") {
|
|
790
|
+
try {
|
|
791
|
+
return this.service(res, "runApplicability", parseRunApplicabilityQuery(url), ControlRunApplicabilityResponse);
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
return this.requestError(res, error);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
846
797
|
const harnessModelsMatch = /^\/harnesses\/([^/]+)\/models$/.exec(path);
|
|
847
798
|
if (method === "GET" && harnessModelsMatch) {
|
|
848
799
|
try {
|
|
@@ -976,8 +927,15 @@ export class DaemonControlApiServer {
|
|
|
976
927
|
return this.service(res, "quota", undefined, ControlQuotaResponse);
|
|
977
928
|
if (method === "POST" && path === "/quota")
|
|
978
929
|
return this.service(res, "refreshQuota", undefined, ControlQuotaResponse);
|
|
979
|
-
if (method === "GET" && path === "/credential-profiles")
|
|
980
|
-
|
|
930
|
+
if (method === "GET" && path === "/credential-profiles") {
|
|
931
|
+
try {
|
|
932
|
+
const query = parseCredentialProfilesSnapshotQuery(url);
|
|
933
|
+
return this.service(res, "credentialProfiles", query.input, query.schema);
|
|
934
|
+
}
|
|
935
|
+
catch (error) {
|
|
936
|
+
return this.requestError(res, error);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
981
939
|
if (method === "POST" && path === "/credential-profiles") {
|
|
982
940
|
let body;
|
|
983
941
|
try {
|
|
@@ -1040,13 +998,30 @@ export class DaemonControlApiServer {
|
|
|
1040
998
|
error: `run is ${rec.state}; ${body.control.kind} has nothing to stop`,
|
|
1041
999
|
});
|
|
1042
1000
|
}
|
|
1043
|
-
|
|
1001
|
+
let activeDescendants;
|
|
1002
|
+
try {
|
|
1003
|
+
const cancelled = await cancelDelegationFamily({
|
|
1004
|
+
daemon: this.opts.daemon,
|
|
1005
|
+
parent: rec,
|
|
1006
|
+
descendantsAfterFence: () => this.delegatedDescendants(rec.runId ?? rec.id),
|
|
1007
|
+
pollMs: this.opts.pollMs,
|
|
1008
|
+
});
|
|
1009
|
+
activeDescendants = cancelled.descendants;
|
|
1010
|
+
}
|
|
1011
|
+
catch (error) {
|
|
1012
|
+
appendRunAuditEvent(rec, "control.rejected", {
|
|
1013
|
+
control: body.control,
|
|
1014
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1015
|
+
});
|
|
1016
|
+
return this.requestError(res, error);
|
|
1017
|
+
}
|
|
1044
1018
|
appendRunAuditEvent(rec, "control.applied", { control: body.control });
|
|
1045
1019
|
return this.json(res, 200, ControlRunControlResponse.parse({
|
|
1046
1020
|
accepted: true,
|
|
1047
1021
|
status: "applied",
|
|
1048
1022
|
runId: rec.runId ?? rec.id,
|
|
1049
|
-
|
|
1023
|
+
cascadeRunIds: activeDescendants.map((child) => child.runId ?? child.id),
|
|
1024
|
+
message: `${body.control.kind} requested${activeDescendants.length ? ` for parent and ${activeDescendants.length} delegated descendant(s)` : ""}`,
|
|
1050
1025
|
}));
|
|
1051
1026
|
}
|
|
1052
1027
|
const eventsMatch = /^\/runs\/([^/]+)\/events$/.exec(path);
|
|
@@ -1298,6 +1273,11 @@ export class DaemonControlApiServer {
|
|
|
1298
1273
|
const runs = await this.opts.daemon.list();
|
|
1299
1274
|
return runs.find((r) => r.id === id || r.runId === id) ?? null;
|
|
1300
1275
|
}
|
|
1276
|
+
/** Persisted Delegate-only descendant graph. `parentRunId` alone is broader
|
|
1277
|
+
* thread/retry lineage and deliberately does not participate. */
|
|
1278
|
+
async delegatedDescendants(parentRunId) {
|
|
1279
|
+
return delegatedDescendantsFromRecords(parentRunId, await this.opts.daemon.list());
|
|
1280
|
+
}
|
|
1301
1281
|
/**
|
|
1302
1282
|
* GET /v2/runs (QA-052): a bounded, newest-first, keyset-paginated page of run
|
|
1303
1283
|
* summaries. Ordering, filtering, and slicing all happen on the raw daemon
|
|
@@ -1390,23 +1370,8 @@ export class DaemonControlApiServer {
|
|
|
1390
1370
|
const state = facts.work_state?.state;
|
|
1391
1371
|
return state === "needs_input" || state === "incomplete" ? state : null;
|
|
1392
1372
|
}
|
|
1393
|
-
recordOperatorDecision(rec, decision, idempotency) {
|
|
1394
|
-
const record = this.opts.services?.recordOperatorDecision;
|
|
1395
|
-
if (!record) {
|
|
1396
|
-
throw Object.assign(new Error("operator decisions are not supported by this engine build"), {
|
|
1397
|
-
status: 501,
|
|
1398
|
-
});
|
|
1399
|
-
}
|
|
1400
|
-
return record(rec.runId ?? rec.id, rec.params, decision, idempotency);
|
|
1401
|
-
}
|
|
1402
1373
|
chainRunMutation(rec, work) {
|
|
1403
|
-
|
|
1404
|
-
if (!threadId)
|
|
1405
|
-
return work();
|
|
1406
|
-
return chainThreadMutation(this.threadTurnRouteCtx(), threadId, async () => {
|
|
1407
|
-
await assertThreadIdle(rec, () => this.opts.daemon.list());
|
|
1408
|
-
return work();
|
|
1409
|
-
});
|
|
1374
|
+
return chainIdleRunMutation(this.threadTurnChains, this.opts.daemon, rec, work);
|
|
1410
1375
|
}
|
|
1411
1376
|
threadTurnRouteCtx() {
|
|
1412
1377
|
const services = this.opts.services ?? {};
|
|
@@ -1417,10 +1382,12 @@ export class DaemonControlApiServer {
|
|
|
1417
1382
|
resolveRunArtifactPath: async (runId, rel) => this.resolveRunArtifactPath(runId, rel),
|
|
1418
1383
|
normalizeStart: runStart.normalizeRunStart,
|
|
1419
1384
|
preflightRunRequirements: services.preflightRunRequirements,
|
|
1385
|
+
preflightThreadRunRequirements: services.preflightThreadRunRequirements,
|
|
1420
1386
|
isTerminalState: (state) => TERMINAL_STATES.has(state),
|
|
1421
1387
|
daemon: this.opts.daemon,
|
|
1422
1388
|
threadDetail: services.threadDetail,
|
|
1423
1389
|
createThreadTurn: services.createThreadTurn,
|
|
1390
|
+
findThreadTurnByIdempotency: services.findThreadTurnByIdempotency,
|
|
1424
1391
|
setTurnEnqueueError: services.setTurnEnqueueError,
|
|
1425
1392
|
threadTurnChains: this.threadTurnChains,
|
|
1426
1393
|
};
|
|
@@ -1521,11 +1488,22 @@ export class DaemonControlApiServer {
|
|
|
1521
1488
|
return this.summarizeRunLive(rec);
|
|
1522
1489
|
}
|
|
1523
1490
|
catch (err) {
|
|
1491
|
+
const delegatedFromRunId = Id.safeParse(paramsRecord(rec)["delegatedFromRunId"]);
|
|
1492
|
+
let waitingOnUser = false;
|
|
1493
|
+
try {
|
|
1494
|
+
waitingOnUser = this.pendingInteractionsFor(rec).length > 0;
|
|
1495
|
+
}
|
|
1496
|
+
catch {
|
|
1497
|
+
// The diagnostic row remains fail-soft even when interaction state is
|
|
1498
|
+
// also corrupt; false is the only safe fallback without evidence.
|
|
1499
|
+
}
|
|
1524
1500
|
return ControlRunSummary.parse({
|
|
1525
1501
|
jobId: rec.id,
|
|
1526
1502
|
runId: rec.runId ?? rec.id,
|
|
1503
|
+
...(delegatedFromRunId.success ? { delegatedFromRunId: delegatedFromRunId.data } : {}),
|
|
1504
|
+
waitingOnUser,
|
|
1527
1505
|
state: "failed",
|
|
1528
|
-
error: `unprojectable job record: ${err instanceof Error ? err.message : String(err)}
|
|
1506
|
+
error: redactSecrets(`unprojectable job record: ${err instanceof Error ? err.message : String(err)}`),
|
|
1529
1507
|
});
|
|
1530
1508
|
}
|
|
1531
1509
|
}
|
|
@@ -1572,11 +1550,6 @@ export class DaemonControlApiServer {
|
|
|
1572
1550
|
}
|
|
1573
1551
|
}
|
|
1574
1552
|
/* ---- Thread projections (engine snake_case -> control camelCase) ---- */
|
|
1575
|
-
function paramsRecord(rec) {
|
|
1576
|
-
return rec.params && typeof rec.params === "object" && !Array.isArray(rec.params)
|
|
1577
|
-
? rec.params
|
|
1578
|
-
: {};
|
|
1579
|
-
}
|
|
1580
1553
|
function projectMetadata(rec) {
|
|
1581
1554
|
const p = paramsRecord(rec);
|
|
1582
1555
|
const scope = p["scope"];
|
|
@@ -1628,83 +1601,6 @@ function appendRunAuditEvent(rec, type, payload) {
|
|
|
1628
1601
|
/* audit append must not change control behavior */
|
|
1629
1602
|
}
|
|
1630
1603
|
}
|
|
1631
|
-
/**
|
|
1632
|
-
* QA-052 observability: how many artifact-path probes the run-list fingerprinter
|
|
1633
|
-
* has performed (each probe is a synchronous filesystem stat-family lookup). The
|
|
1634
|
-
* single-parse test pattern (eventsParseCount) proves warm-list work is BOUNDED
|
|
1635
|
-
* — by page size (only sliced records are fingerprinted) and, for terminal runs,
|
|
1636
|
-
* by the one-probe short-circuit below (1 path instead of 12).
|
|
1637
|
-
*/
|
|
1638
|
-
let fingerprintProbeCount = 0;
|
|
1639
|
-
export function runListFingerprintProbeCountForTests() {
|
|
1640
|
-
return fingerprintProbeCount;
|
|
1641
|
-
}
|
|
1642
|
-
export function resetRunListFingerprintProbeCountForTests() {
|
|
1643
|
-
fingerprintProbeCount = 0;
|
|
1644
|
-
}
|
|
1645
|
-
function summaryFingerprint(rec) {
|
|
1646
|
-
const mtime = (rel) => {
|
|
1647
|
-
fingerprintProbeCount++;
|
|
1648
|
-
if (!rec.runDir)
|
|
1649
|
-
return 0;
|
|
1650
|
-
const path = safeArtifactPath(rec.runDir, rel);
|
|
1651
|
-
if (!path)
|
|
1652
|
-
return 0;
|
|
1653
|
-
try {
|
|
1654
|
-
return statSync(path).mtimeMs;
|
|
1655
|
-
}
|
|
1656
|
-
catch {
|
|
1657
|
-
return 0;
|
|
1658
|
-
}
|
|
1659
|
-
};
|
|
1660
|
-
const identity = [rec.state, paramsFingerprint(rec), rec.finishedAt ?? "", rec.error ?? ""];
|
|
1661
|
-
// QA-052 terminal short-circuit: a TERMINAL run's every projected artifact
|
|
1662
|
-
// (events.jsonl, arbitration, telemetry, failure, the final/* outputs,
|
|
1663
|
-
// work_product) is written before the daemon marks the run terminal and never
|
|
1664
|
-
// changes afterward. The SOLE post-terminal mutation is the delivery/apply
|
|
1665
|
-
// overlay (final/delivery_state.yaml), which the apply/revert route is the one
|
|
1666
|
-
// writer of [B7]. So a warm re-list of a terminal run fingerprints ONE path
|
|
1667
|
-
// instead of twelve — the cheap probe still detects an apply/revert, and every
|
|
1668
|
-
// other axis is frozen. Active (queued/running) runs keep the full
|
|
1669
|
-
// artifact-mtime fingerprint because their events.jsonl and final/* land while
|
|
1670
|
-
// the run is still live.
|
|
1671
|
-
if (TERMINAL_STATES.has(rec.state)) {
|
|
1672
|
-
return [...identity, mtime("final/delivery_state.yaml")].join("|");
|
|
1673
|
-
}
|
|
1674
|
-
return [
|
|
1675
|
-
...identity,
|
|
1676
|
-
mtime("events.jsonl"),
|
|
1677
|
-
mtime("arbitration/decision.yaml"),
|
|
1678
|
-
mtime("final/telemetry.yaml"),
|
|
1679
|
-
mtime("final/failure.yaml"),
|
|
1680
|
-
mtime("final/summary.md"),
|
|
1681
|
-
// Primary outputs feed outputReadyState; a summary cached before the
|
|
1682
|
-
// answer/plan/report/patch landed must invalidate when it does.
|
|
1683
|
-
mtime("final/answer.md"),
|
|
1684
|
-
mtime("final/plan.md"),
|
|
1685
|
-
mtime("final/explore.md"),
|
|
1686
|
-
mtime("final/report.md"),
|
|
1687
|
-
mtime("final/patch.diff"),
|
|
1688
|
-
// The honest outcome (result kind / diffstat / adopted) is projected from
|
|
1689
|
-
// work_product.yaml; a summary cached before it landed must invalidate.
|
|
1690
|
-
mtime("final/work_product.yaml"),
|
|
1691
|
-
// The MUTABLE delivery state (applied / reverted / not_applied) is written
|
|
1692
|
-
// to delivery_state.yaml by markRunApplyState, and controlRunResult prefers
|
|
1693
|
-
// that overlay over work_product.yaml's immutable apply_state. It must be in
|
|
1694
|
-
// the fingerprint set or a summary cached before an apply/revert keeps
|
|
1695
|
-
// reporting the stale applyState (the dedicated apply route writes only this
|
|
1696
|
-
// file, so no other fingerprinted mtime would move). [B7]
|
|
1697
|
-
mtime("final/delivery_state.yaml"),
|
|
1698
|
-
].join("|");
|
|
1699
|
-
}
|
|
1700
|
-
function paramsFingerprint(rec) {
|
|
1701
|
-
try {
|
|
1702
|
-
return sha256(JSON.stringify(rec.params ?? null));
|
|
1703
|
-
}
|
|
1704
|
-
catch {
|
|
1705
|
-
return "unserializable-params";
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
1604
|
/** v0.9 strategy flags projected back so surfaces can tell a race from a repair loop. */
|
|
1709
1605
|
function strategyFromParams(p) {
|
|
1710
1606
|
if (p["untilClean"] === true)
|
|
@@ -1826,6 +1722,13 @@ function runOutcomeFacts(rec, decision, failure,
|
|
|
1826
1722
|
* folded into a DECISION-LESS terminal so a needs_input/incomplete ask
|
|
1827
1723
|
* carries its veto axis just like an arbitrated agent run. */
|
|
1828
1724
|
workState = null) {
|
|
1725
|
+
const legacy = legacyRunOutcomeFacts(rec, decision, failure, workState);
|
|
1726
|
+
if (!legacy)
|
|
1727
|
+
return null;
|
|
1728
|
+
return effectiveTerminalFacts(rec.runDir, legacy, decision, expectedRunFacts(rec)).outcomeFacts;
|
|
1729
|
+
}
|
|
1730
|
+
/** Legacy-only terminal projection retained for active/pre-RunFacts records. */
|
|
1731
|
+
function legacyRunOutcomeFacts(rec, decision, failure, workState = null) {
|
|
1829
1732
|
if (!isTerminalLifecycle(rec.state))
|
|
1830
1733
|
return null;
|
|
1831
1734
|
// Arbitrated runs already carry work_state on decision.facts (INV-116).
|
|
@@ -1875,6 +1778,15 @@ function summarizeRun(rec, eventsSnapshot) {
|
|
|
1875
1778
|
const externalContextPolicy = telemetry?.external_context_policy ?? task?.external_context.policy;
|
|
1876
1779
|
const webEvidence = controlWebEvidence(telemetry, task);
|
|
1877
1780
|
const decision = safeReadStructuredArtifact(rec, "arbitration/decision.yaml", DecisionRecord);
|
|
1781
|
+
const failure = readFailure(rec);
|
|
1782
|
+
const workState = telemetry?.final_attempt_id
|
|
1783
|
+
? (telemetry.attempts.find((a) => a.attempt_id === telemetry.final_attempt_id)?.outcome
|
|
1784
|
+
.work_state ?? null)
|
|
1785
|
+
: null;
|
|
1786
|
+
const legacyOutcome = legacyRunOutcomeFacts(rec, decision, failure, workState);
|
|
1787
|
+
const terminalFacts = legacyOutcome
|
|
1788
|
+
? effectiveTerminalFacts(rec.runDir, legacyOutcome, decision, expectedRunFacts(rec))
|
|
1789
|
+
: { runFacts: null, outcomeFacts: null, decision };
|
|
1878
1790
|
const budget = budgetSnapshot(rec, decision, eventsSnapshot);
|
|
1879
1791
|
const parsedReviewerPanel = Array.isArray(p["reviewerPanel"])
|
|
1880
1792
|
? ControlReviewerPanelEntry.array().safeParse(p["reviewerPanel"])
|
|
@@ -1894,11 +1806,18 @@ function summarizeRun(rec, eventsSnapshot) {
|
|
|
1894
1806
|
return ControlRunSummary.parse({
|
|
1895
1807
|
jobId: rec.id,
|
|
1896
1808
|
runId: rec.runId ?? rec.id,
|
|
1809
|
+
parentRunId: (typeof p["parentRunId"] === "string" ? p["parentRunId"] : null) ??
|
|
1810
|
+
task?.run_lineage.parent_run_id ??
|
|
1811
|
+
null,
|
|
1812
|
+
delegatedFromRunId: (typeof p["delegatedFromRunId"] === "string" ? p["delegatedFromRunId"] : null) ??
|
|
1813
|
+
task?.run_lineage.delegated_from_run_id ??
|
|
1814
|
+
null,
|
|
1815
|
+
delegation: telemetry?.delegation ?? null,
|
|
1897
1816
|
taskId: rec.taskId,
|
|
1898
1817
|
state: rec.state,
|
|
1899
1818
|
runDir: rec.runDir,
|
|
1900
1819
|
error: rec.error,
|
|
1901
|
-
failure
|
|
1820
|
+
failure,
|
|
1902
1821
|
project: projectMetadata(rec),
|
|
1903
1822
|
mode: parsedMode.success ? parsedMode.data : undefined,
|
|
1904
1823
|
strategy: strategyFromParams(p),
|
|
@@ -1925,7 +1844,6 @@ function summarizeRun(rec, eventsSnapshot) {
|
|
|
1925
1844
|
// The single engine validator's receipt, projected verbatim — surfaces
|
|
1926
1845
|
// never re-validate the answer (null = no structured-output contract).
|
|
1927
1846
|
outputConformance: outputConformance?.status ?? null,
|
|
1928
|
-
// Route receipt projected verbatim (INV-061 disclosure); never re-derived.
|
|
1929
1847
|
authRoute: telemetry?.auth_route
|
|
1930
1848
|
? {
|
|
1931
1849
|
requested: telemetry.auth_route.requested,
|
|
@@ -1934,6 +1852,7 @@ function summarizeRun(rec, eventsSnapshot) {
|
|
|
1934
1852
|
reason: telemetry.auth_route.reason,
|
|
1935
1853
|
harnessId: telemetry.auth_route.harness_id,
|
|
1936
1854
|
attemptId: telemetry.auth_route.attempt_id,
|
|
1855
|
+
profileId: telemetry.auth_route.profile_id,
|
|
1937
1856
|
modelMismatch: telemetry.auth_route.model_mismatch,
|
|
1938
1857
|
}
|
|
1939
1858
|
: null,
|
|
@@ -1946,13 +1865,10 @@ function summarizeRun(rec, eventsSnapshot) {
|
|
|
1946
1865
|
webEvidence,
|
|
1947
1866
|
requestRequirements: telemetry?.request_requirements ?? [],
|
|
1948
1867
|
toolPermissionPolicy: task?.tool_permission_policy,
|
|
1949
|
-
outputReadyState: outputReadyState(rec, parsedMode.success ? parsedMode.data : null,
|
|
1868
|
+
outputReadyState: outputReadyState(rec, parsedMode.success ? parsedMode.data : null, failure, terminalFacts.runFacts),
|
|
1950
1869
|
toolWarningsTotal: telemetry?.tool_warnings_total ?? 0,
|
|
1951
1870
|
result: controlRunResult(rec),
|
|
1952
|
-
outcomeFacts:
|
|
1953
|
-
? (telemetry.attempts.find((a) => a.attempt_id === telemetry.final_attempt_id)?.outcome
|
|
1954
|
-
.work_state ?? null)
|
|
1955
|
-
: null),
|
|
1871
|
+
outcomeFacts: terminalFacts.outcomeFacts,
|
|
1956
1872
|
route: controlRoute(telemetry, p),
|
|
1957
1873
|
// QA-034: routing rationale projected verbatim from the engine telemetry —
|
|
1958
1874
|
// surfaces never reconstruct the order from prose.
|
|
@@ -1989,7 +1905,7 @@ function controlRoute(telemetry, p) {
|
|
|
1989
1905
|
verified: observed !== null,
|
|
1990
1906
|
};
|
|
1991
1907
|
}
|
|
1992
|
-
function detailFor(rec, pendingInteractions = [], cursor, operator = null) {
|
|
1908
|
+
function detailFor(rec, pendingInteractions = [], cursor, operator = null, children = []) {
|
|
1993
1909
|
// Snapshot fence: capture the event cursor BEFORE building any projection.
|
|
1994
1910
|
// The fence promise is "every event with seq <= lastSeq is reflected" — a
|
|
1995
1911
|
// cursor read AFTER the projections could skip an event that landed in
|
|
@@ -2010,22 +1926,29 @@ function detailFor(rec, pendingInteractions = [], cursor, operator = null) {
|
|
|
2010
1926
|
? readRunEventsWithIntegrity(rec)
|
|
2011
1927
|
: { events: [], integrity: undefined };
|
|
2012
1928
|
const summary = summarizeRun(rec, events);
|
|
2013
|
-
//
|
|
2014
|
-
// "something to apply" banner signal (a convergence patch
|
|
2015
|
-
// with no meta.result_kind, so result.kind alone would
|
|
2016
|
-
|
|
1929
|
+
// applyEligibility is non-null exactly when the run has an applyable patch —
|
|
1930
|
+
// the authoritative "something to apply" banner signal (a convergence patch
|
|
1931
|
+
// may carry kind:patch with no meta.result_kind, so result.kind alone would
|
|
1932
|
+
// miss it).
|
|
1933
|
+
const { runFacts, outcomeFacts, applyEligibility, requiredActions } = projectRunFactsForDetail(rec.runDir, summary.outcomeFacts ?? null, operator !== null, () => applyEligibilityFor(rec, operator), expectedRunFacts(rec));
|
|
2017
1934
|
const planProjection = planProjectionFor(rec, summary.mode);
|
|
2018
1935
|
return ControlRunDetail.parse({
|
|
2019
|
-
summary: {
|
|
1936
|
+
summary: {
|
|
1937
|
+
...summary,
|
|
1938
|
+
outcomeFacts,
|
|
1939
|
+
waitingOnUser: pendingInteractions.length > 0,
|
|
1940
|
+
},
|
|
1941
|
+
children,
|
|
1942
|
+
runFacts,
|
|
2020
1943
|
// Server-owned outcome headline (D18), from the single projection owner —
|
|
2021
1944
|
// surfaces render it verbatim above model prose, never re-derive it.
|
|
2022
|
-
outcomeBanner: outcomeBanner(
|
|
1945
|
+
outcomeBanner: outcomeBanner(outcomeFacts, {
|
|
2023
1946
|
applyState: summary.result.applyState,
|
|
2024
1947
|
hasApplyableChange: applyEligibility !== null,
|
|
2025
1948
|
}),
|
|
2026
1949
|
lastSeq,
|
|
2027
1950
|
artifacts: rec.runDir ? listArtifacts(rec.runDir) : [],
|
|
2028
|
-
primaryOutput: primaryOutput(rec, summary.mode, failure),
|
|
1951
|
+
primaryOutput: primaryOutput(rec, summary.mode, failure, runFacts),
|
|
2029
1952
|
timeline: timelineEvents(rec, events, integrity),
|
|
2030
1953
|
budget: budgetSnapshot(rec, decision, events, integrity),
|
|
2031
1954
|
finalSummary: boundedArtifactText(rec, "final/summary.md"),
|
|
@@ -2049,8 +1972,9 @@ function detailFor(rec, pendingInteractions = [], cursor, operator = null) {
|
|
|
2049
1972
|
// Minimal typed required-actions (GH #29) for a succeeded-but-blocked run,
|
|
2050
1973
|
// from the single status-projection owner: review-blocked / checks-failed /
|
|
2051
1974
|
// needs-decision / work_state needs_input, keyed to the same validated
|
|
2052
|
-
// operator decision the needs-decision + apply gates consult
|
|
2053
|
-
|
|
1975
|
+
// operator decision the needs-decision + apply gates consult — the
|
|
1976
|
+
// canonical terminal actions plus legitimate post-terminal overlays.
|
|
1977
|
+
requiredActions,
|
|
2054
1978
|
});
|
|
2055
1979
|
}
|
|
2056
1980
|
function readTextArtifact(rec, relPath, redact = true) {
|
|
@@ -2074,42 +1998,6 @@ function parseAccessMaybe(value) {
|
|
|
2074
1998
|
const parsed = AccessProfile.safeParse(value);
|
|
2075
1999
|
return parsed.success ? parsed.data : undefined;
|
|
2076
2000
|
}
|
|
2077
|
-
/**
|
|
2078
|
-
* Pure subscription-VALUATION fold over a run's events (QA-023c/QA-017b),
|
|
2079
|
-
* independent of the cash truth. The owner-locked ledger settles native-
|
|
2080
|
-
* subscription work to cash $0 while accumulating its token valuation on
|
|
2081
|
-
* `budget.cash.valuation_usd` (last-wins, cumulative). An UNKNOWN valuation (no
|
|
2082
|
-
* usage ever reported — e.g. a Cursor draft) stays NULL and is never coerced to
|
|
2083
|
-
* a fake $0. The legacy fallback for runs predating budget.cash is the summed
|
|
2084
|
-
* `budget.observation` spend. Vendor token valuation is at best an estimate.
|
|
2085
|
-
*/
|
|
2086
|
-
export function budgetValuationFromEvents(events) {
|
|
2087
|
-
let valuationUsd = null;
|
|
2088
|
-
let observation = 0;
|
|
2089
|
-
let sawObservation = false;
|
|
2090
|
-
for (const ev of events) {
|
|
2091
|
-
const payload = eventPayload(ev);
|
|
2092
|
-
if (ev["type"] === "budget.cash") {
|
|
2093
|
-
const val = payload["valuation_usd"];
|
|
2094
|
-
if (typeof val === "number" && Number.isFinite(val))
|
|
2095
|
-
valuationUsd = val;
|
|
2096
|
-
continue;
|
|
2097
|
-
}
|
|
2098
|
-
if (ev["type"] === "budget.observation" && payload["kind"] === "spend") {
|
|
2099
|
-
const usd = payload["usd"];
|
|
2100
|
-
if (typeof usd === "number" && Number.isFinite(usd)) {
|
|
2101
|
-
observation += usd;
|
|
2102
|
-
sawObservation = true;
|
|
2103
|
-
}
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
if (valuationUsd === null && sawObservation)
|
|
2107
|
-
valuationUsd = observation;
|
|
2108
|
-
return {
|
|
2109
|
-
valuationUsd,
|
|
2110
|
-
valuationKnowledge: valuationUsd === null ? "unknown" : "estimated",
|
|
2111
|
-
};
|
|
2112
|
-
}
|
|
2113
2001
|
/** Typed severity per event type — no string matching over event names. */
|
|
2114
2002
|
function budgetSnapshot(rec, decision, eventsSnapshot, integrity) {
|
|
2115
2003
|
const p = paramsRecord(rec);
|
|
@@ -2126,7 +2014,9 @@ function budgetSnapshot(rec, decision, eventsSnapshot, integrity) {
|
|
|
2126
2014
|
let source = spendUsd === null ? "unknown" : "decision";
|
|
2127
2015
|
// Subscription VALUATION (QA-023c/QA-017b) beside cash — computed by the pure
|
|
2128
2016
|
// event fold so an UNKNOWN valuation stays null, never a fabricated $0.
|
|
2129
|
-
const
|
|
2017
|
+
const normalized = normalizeLegacyBudgetComponents(decision?.budget_summary ?? null, evs, budgetValuationFromEvents(evs));
|
|
2018
|
+
let { valuationUsd, valuationKnowledge } = normalized;
|
|
2019
|
+
estimated = normalized.cashEstimated;
|
|
2130
2020
|
if (spendUsd === null) {
|
|
2131
2021
|
// The CASH truth for a decision-less run (plan/ask/explore — they never
|
|
2132
2022
|
// write a decision record) is the ledger's own `budget.cash` disclosure:
|
|
@@ -2147,8 +2037,10 @@ function budgetSnapshot(rec, decision, eventsSnapshot, integrity) {
|
|
|
2147
2037
|
const payload = eventPayload(ev);
|
|
2148
2038
|
if (ev["type"] === "budget.cash") {
|
|
2149
2039
|
const cash = payload["cash_spend_usd"];
|
|
2150
|
-
if (typeof cash === "number" && Number.isFinite(cash))
|
|
2040
|
+
if (typeof cash === "number" && Number.isFinite(cash)) {
|
|
2151
2041
|
lastCash = cash;
|
|
2042
|
+
estimated = cashEstimatedFromLedgerEvent(payload);
|
|
2043
|
+
}
|
|
2152
2044
|
continue;
|
|
2153
2045
|
}
|
|
2154
2046
|
if (ev["type"] === "budget.observation" && payload["kind"] === "spend") {
|
|
@@ -2270,9 +2162,10 @@ function applyGateError(rec, patch, targetRepoRoot, operatorDecision, finalVerif
|
|
|
2270
2162
|
}
|
|
2271
2163
|
function applyGateInputFor(rec, patch, targetRepoRoot, operatorDecision) {
|
|
2272
2164
|
const decision = safeReadStructuredArtifact(rec, "arbitration/decision.yaml", DecisionRecord);
|
|
2165
|
+
const terminal = effectiveTerminalFacts(rec.runDir, decision?.facts ?? null, decision, expectedRunFacts(rec));
|
|
2273
2166
|
return {
|
|
2274
2167
|
state: rec.state,
|
|
2275
|
-
decision,
|
|
2168
|
+
decision: terminal.decision,
|
|
2276
2169
|
workProduct: safeReadStructuredArtifact(rec, "final/work_product.yaml", WorkProduct),
|
|
2277
2170
|
patch,
|
|
2278
2171
|
originalRepoRoot: runRepoRoot(rec),
|
|
@@ -2285,9 +2178,10 @@ function applyGateInputFor(rec, patch, targetRepoRoot, operatorDecision) {
|
|
|
2285
2178
|
// an already-applied / reverted run gets a terminal eligibility disposition
|
|
2286
2179
|
// instead of a stale "rerun a fresh check" (QA-021).
|
|
2287
2180
|
applyState: controlRunResult(rec).applyState,
|
|
2288
|
-
// D-16 work_state veto (INV-116): thread the model-attested work outcome
|
|
2289
|
-
// the
|
|
2290
|
-
|
|
2181
|
+
// D-16 work_state veto (INV-116): thread the model-attested work outcome
|
|
2182
|
+
// (the canonical terminal facts) so the gate refuses a needs_input/
|
|
2183
|
+
// incomplete winner even with a clean legacy review.
|
|
2184
|
+
workState: terminal.outcomeFacts?.work_state ?? null,
|
|
2291
2185
|
};
|
|
2292
2186
|
}
|
|
2293
2187
|
/**
|