@claudexor/control-api 3.20.1 → 3.23.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.
Files changed (114) hide show
  1. package/dist/artifact-serve-routes.d.ts +7 -4
  2. package/dist/artifact-serve-routes.d.ts.map +1 -1
  3. package/dist/artifact-serve-routes.js +35 -7
  4. package/dist/artifact-serve-routes.js.map +1 -1
  5. package/dist/command-reads.d.ts +5 -0
  6. package/dist/command-reads.d.ts.map +1 -0
  7. package/dist/command-reads.js +18 -0
  8. package/dist/command-reads.js.map +1 -0
  9. package/dist/control-protocol.d.ts.map +1 -1
  10. package/dist/control-protocol.js +3 -1
  11. package/dist/control-protocol.js.map +1 -1
  12. package/dist/daemon-server.d.ts +1 -0
  13. package/dist/daemon-server.d.ts.map +1 -1
  14. package/dist/daemon-server.js +75 -109
  15. package/dist/daemon-server.js.map +1 -1
  16. package/dist/daemon-status-routes.d.ts +7 -0
  17. package/dist/daemon-status-routes.d.ts.map +1 -0
  18. package/dist/daemon-status-routes.js +16 -0
  19. package/dist/daemon-status-routes.js.map +1 -0
  20. package/dist/decision-rerun.js +1 -1
  21. package/dist/decision-rerun.js.map +1 -1
  22. package/dist/operation-catalog.d.ts.map +1 -1
  23. package/dist/operation-catalog.js +1 -0
  24. package/dist/operation-catalog.js.map +1 -1
  25. package/dist/operation-summaries.d.ts.map +1 -1
  26. package/dist/operation-summaries.js +1 -0
  27. package/dist/operation-summaries.js.map +1 -1
  28. package/dist/project-routes.d.ts +6 -1
  29. package/dist/project-routes.d.ts.map +1 -1
  30. package/dist/project-routes.js +18 -3
  31. package/dist/project-routes.js.map +1 -1
  32. package/dist/remote-operation-descriptors.d.ts +8 -0
  33. package/dist/remote-operation-descriptors.d.ts.map +1 -1
  34. package/dist/remote-operation-descriptors.js +4 -0
  35. package/dist/remote-operation-descriptors.js.map +1 -1
  36. package/dist/retention.d.ts.map +1 -1
  37. package/dist/retention.js +4 -1
  38. package/dist/retention.js.map +1 -1
  39. package/dist/run-apply-routes.d.ts.map +1 -1
  40. package/dist/run-apply-routes.js +6 -10
  41. package/dist/run-apply-routes.js.map +1 -1
  42. package/dist/run-audit.d.ts +5 -0
  43. package/dist/run-audit.d.ts.map +1 -0
  44. package/dist/run-audit.js +18 -0
  45. package/dist/run-audit.js.map +1 -0
  46. package/dist/run-continuation-projection.d.ts +7 -0
  47. package/dist/run-continuation-projection.d.ts.map +1 -0
  48. package/dist/run-continuation-projection.js +135 -0
  49. package/dist/run-continuation-projection.js.map +1 -0
  50. package/dist/run-continuation-start.d.ts +13 -0
  51. package/dist/run-continuation-start.d.ts.map +1 -0
  52. package/dist/run-continuation-start.js +62 -0
  53. package/dist/run-continuation-start.js.map +1 -0
  54. package/dist/run-delivery-state.d.ts.map +1 -1
  55. package/dist/run-delivery-state.js +6 -0
  56. package/dist/run-delivery-state.js.map +1 -1
  57. package/dist/run-discard.d.ts +10 -0
  58. package/dist/run-discard.d.ts.map +1 -0
  59. package/dist/run-discard.js +47 -0
  60. package/dist/run-discard.js.map +1 -0
  61. package/dist/run-exact-patch.d.ts +25 -0
  62. package/dist/run-exact-patch.d.ts.map +1 -0
  63. package/dist/run-exact-patch.js +55 -0
  64. package/dist/run-exact-patch.js.map +1 -0
  65. package/dist/run-execution-root.d.ts +22 -0
  66. package/dist/run-execution-root.d.ts.map +1 -0
  67. package/dist/run-execution-root.js +23 -0
  68. package/dist/run-execution-root.js.map +1 -0
  69. package/dist/run-list-fingerprint.d.ts.map +1 -1
  70. package/dist/run-list-fingerprint.js +7 -0
  71. package/dist/run-list-fingerprint.js.map +1 -1
  72. package/dist/run-list.d.ts +3 -57
  73. package/dist/run-list.d.ts.map +1 -1
  74. package/dist/run-list.js +2 -67
  75. package/dist/run-list.js.map +1 -1
  76. package/dist/run-record.d.ts +4 -3
  77. package/dist/run-record.d.ts.map +1 -1
  78. package/dist/run-record.js.map +1 -1
  79. package/dist/run-retry-routes.js +11 -2
  80. package/dist/run-retry-routes.js.map +1 -1
  81. package/dist/run-start.d.ts +12 -3
  82. package/dist/run-start.d.ts.map +1 -1
  83. package/dist/run-start.js +25 -25
  84. package/dist/run-start.js.map +1 -1
  85. package/dist/thread-create-route.d.ts +16 -0
  86. package/dist/thread-create-route.d.ts.map +1 -0
  87. package/dist/thread-create-route.js +83 -0
  88. package/dist/thread-create-route.js.map +1 -0
  89. package/dist/thread-delegated-execution.d.ts +19 -0
  90. package/dist/thread-delegated-execution.d.ts.map +1 -0
  91. package/dist/thread-delegated-execution.js +27 -0
  92. package/dist/thread-delegated-execution.js.map +1 -0
  93. package/dist/thread-lifecycle-routes.d.ts +2 -1
  94. package/dist/thread-lifecycle-routes.d.ts.map +1 -1
  95. package/dist/thread-lifecycle-routes.js +12 -3
  96. package/dist/thread-lifecycle-routes.js.map +1 -1
  97. package/dist/thread-mutation.d.ts +2 -4
  98. package/dist/thread-mutation.d.ts.map +1 -1
  99. package/dist/thread-mutation.js +1 -1
  100. package/dist/thread-mutation.js.map +1 -1
  101. package/dist/thread-projection.d.ts.map +1 -1
  102. package/dist/thread-projection.js +1 -0
  103. package/dist/thread-projection.js.map +1 -1
  104. package/dist/thread-recovery.js +1 -1
  105. package/dist/thread-recovery.js.map +1 -1
  106. package/dist/thread-turn-problems.d.ts +39 -0
  107. package/dist/thread-turn-problems.d.ts.map +1 -0
  108. package/dist/thread-turn-problems.js +86 -0
  109. package/dist/thread-turn-problems.js.map +1 -0
  110. package/dist/thread-turn-routes.d.ts +2 -25
  111. package/dist/thread-turn-routes.d.ts.map +1 -1
  112. package/dist/thread-turn-routes.js +19 -90
  113. package/dist/thread-turn-routes.js.map +1 -1
  114. package/package.json +7 -7
@@ -1,5 +1,7 @@
1
+ import { readThreadCommands } from "./command-reads.js";
1
2
  import { recoverInterruptedOutput } from "./retained-output.js";
2
3
  import { recordedExecutionRoot } from "./run-delivery-state.js";
4
+ import * as exactPatch from "./run-exact-patch.js";
3
5
  import { readTextArtifact, readRawTextArtifact, readStructured, safeReadStructuredArtifact, } from "./run-artifact-read.js";
4
6
  import { controlRunResult, readDeliveryState, markRunApplyState } from "./run-delivery-state.js";
5
7
  import { timingSafeEqual } from "node:crypto";
@@ -7,10 +9,13 @@ import { existsSync, lstatSync, mkdirSync, readdirSync, writeFileSync } from "no
7
9
  import { createServer } from "node:http";
8
10
  import { basename, extname, join } from "node:path";
9
11
  import { deriveApplyEligibility, revertInPlaceFromAnchor, validateApplyGate, verifyAndDeliver, } from "@claudexor/delivery";
10
- import { appendRunEvent, lastSeqInFile } from "@claudexor/event-log";
12
+ import { lastSeqInFile } from "@claudexor/event-log";
11
13
  import { isVanishedErrno, safeArtifactPath, safeArtifactRoot } from "./artifact-paths.js";
12
14
  import { TERMINAL_STATES } from "./sse-shared.js";
13
15
  import { readFilesWorkProduct } from "./files-work-product.js";
16
+ import { appendRunAuditEvent } from "./run-audit.js";
17
+ import { discardRunResult } from "./run-discard.js";
18
+ import { continuationSummary } from "./run-continuation-projection.js";
14
19
  import { applyFilesResult } from "./files-apply-route.js";
15
20
  import { deliverableWorkspaceChanges } from "@claudexor/workspace";
16
21
  import { streamRunEvents } from "./run-events-stream.js";
@@ -21,6 +26,7 @@ import { projectTurnRunCards } from "./thread-projection.js";
21
26
  import { projectSession, projectThread, projectTurn } from "./thread-projection.js";
22
27
  import { handleThreadTurnCreate, handleThreadTurnRetry, } from "./thread-turn-routes.js";
23
28
  import { chainIdleRunMutation, chainThreadMutation } from "./thread-mutation.js";
29
+ import { handleThreadCreate } from "./thread-create-route.js";
24
30
  import { handleThreadLifecycleRoutes, } from "./thread-lifecycle-routes.js";
25
31
  import * as runStart from "./run-start.js";
26
32
  import { cancelDelegationFamily } from "./delegation-control.js";
@@ -37,10 +43,12 @@ import { writeBinaryResponse } from "./binary-response.js";
37
43
  export { inlineContentDisposition } from "./binary-response.js";
38
44
  import { handleRecoveryRoute } from "./recovery-routes.js";
39
45
  import { handleJournalEventRoute } from "./journal-event-routes.js";
46
+ import { handleDaemonStatusRoute } from "./daemon-status-routes.js";
40
47
  import { handleMaintenanceRoute } from "./maintenance-routes.js";
41
48
  import { handleResourceRoute } from "./resource-routes.js";
42
49
  import { handleModelRoute } from "./model-routes.js";
43
50
  import { handleArtifactServeRoute, listArtifacts, resolveProjectRoot, } from "./artifact-serve-routes.js";
51
+ import { runExecutionRoot, runExecutionWorkspaceRoot } from "./run-execution-root.js";
44
52
  import { requiredGateSpecsFromTaskArtifact } from "./task-contract-gates.js";
45
53
  import { bearerCredential } from "./authorization.js";
46
54
  import { assertOnlyQueryParams, singleQuery } from "./query.js";
@@ -51,10 +59,10 @@ export { resetRunListFingerprintProbeCountForTests, runListFingerprintProbeCount
51
59
  import { controlProblemError, normalizeRequestValidationError, revertRefusedProblem, thrownProblemCode, } from "./problem-response.js";
52
60
  import { handleSecurityRoute } from "./security-routes.js";
53
61
  import { effectiveTerminalFacts, expectedRunFacts, projectRunFactsForDetail, } from "./run-facts-projection.js";
54
- import { PlanQuestionsArtifact, CouncilProjection, derivePlanReadiness, directDelegatedChildrenFromRecords, ControlAuthReadinessRefreshRequest, ControlAuthReadinessRefreshResponse, ControlProblem, AccessProfile, ControlApplyRequest, AgentCapabilityCatalog, ControlHarnessListResponse, ControlHarnessModelsQueryResponse, ControlSetupJob, ControlSetupJobCreateRequest, ControlSetupJobInputRequest, ControlSetupJobEvent, ControlSetupJobSnapshot, isTerminalControlSetupJobState, ControlSetupJobListFilter, ControlSetupJobListResponse, ControlRunStartInfo, ControlRunControlRequest, ControlRunControlResponse, ControlReviewerPanelEntry, ControlRunDetail, ControlRunListResponse, ControlRunSummary, ControlBudgetSnapshot, PaidBudget, ControlSettingsSnapshot, ControlSettingsUpdateRequest, ControlQuotaRefreshRequest, ControlQuotaResponse, ControlAccountPoolsResponse, ControlAccountsMigrationRollbackRequest, ControlAccountsMigrationRollbackResponse, ControlCredentialProfileCreateRequest, ControlCredentialProfileCreateResponse, ControlCredentialProfileUpdateRequest, ControlCredentialProfileUpdateResponse, ControlCredentialProfileDeleteResponse, ControlInteractionAnswerRequest, ControlInteractionAnswerResponse, ControlRunDecisionRequest, ControlRunDecisionResponse, ControlRunApplicabilityResponse, ControlThreadCreateRequest, ControlThreadTurnRequest, ControlThreadUpdateRequest, ControlThreadDetail, ControlThreadListResponse, DecisionRecord, Id, ModeKind, RoutingGoal, ReviewFinding, RunFailure, RunTelemetry, StructuredOutputConformance, TaskContract, ProtectedPathApproval, isEphemeralRunScope, isTerminalLifecycle, needsDecision, needsOperatorAttention, outcomeBanner, outcomeFactsFromFailure, TestCommandInvocation, WorkProduct, } from "@claudexor/schema";
62
+ import { PlanQuestionsArtifact, CouncilProjection, derivePlanReadiness, directDelegatedChildrenFromRecords, ControlAuthReadinessRefreshRequest, ControlAuthReadinessRefreshResponse, ControlProblem, AccessProfile, ControlApplyRequest, AgentCapabilityCatalog, ControlHarnessListResponse, ControlHarnessModelsQueryResponse, ControlSetupJob, ControlSetupJobCreateRequest, ControlSetupJobInputRequest, ControlSetupJobEvent, ControlSetupJobSnapshot, isTerminalControlSetupJobState, ControlSetupJobListFilter, ControlSetupJobListResponse, ControlRunStartInfo, ControlRunControlRequest, ControlRunControlResponse, ControlReviewerPanelEntry, ControlRunDetail, ControlRunListResponse, ControlRunSummary, ControlBudgetSnapshot, PaidBudget, ControlSettingsSnapshot, ControlSettingsUpdateRequest, ControlQuotaRefreshRequest, ControlQuotaResponse, ControlAccountPoolsResponse, ControlAccountsMigrationRollbackRequest, ControlAccountsMigrationRollbackResponse, ControlCredentialProfileCreateRequest, ControlCredentialProfileCreateResponse, ControlCredentialProfileUpdateRequest, ControlCredentialProfileUpdateResponse, ControlCredentialProfileDeleteResponse, ControlInteractionAnswerRequest, ControlInteractionAnswerResponse, ControlRunDecisionRequest, ControlRunDecisionResponse, ControlRunApplicabilityResponse, ControlThreadTurnRequest, ControlThreadUpdateRequest, ControlThreadDetail, ControlThreadListResponse, DecisionRecord, Id, ModeKind, RoutingGoal, ReviewFinding, RunFailure, RunTelemetry, StructuredOutputConformance, TaskContract, ProtectedPathApproval, isTerminalLifecycle, needsDecision, needsOperatorAttention, outcomeBanner, outcomeFactsFromFailure, TestCommandInvocation, WorkProduct, } from "@claudexor/schema";
55
63
  import { resolveControlProtocol } from "./control-protocol.js";
56
64
  import { readControlRequestBody } from "./request-body.js";
57
- import { assertNoInlineSecretValues, containsSecretLikeToken, errorCode, noProjectRepoRoot, redactSecrets, safeProblemContext, safeProblemRequiredActions, sha256, } from "@claudexor/util";
65
+ import { assertNoInlineSecretValues, errorCode, noProjectRepoRoot, redactSecrets, safeProblemContext, safeProblemRequiredActions, sha256, } from "@claudexor/util";
58
66
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
59
67
  const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
60
68
  const NO_PROJECT_ROOT = noProjectRepoRoot();
@@ -358,12 +366,18 @@ export class DaemonControlApiServer {
358
366
  }
359
367
  const path = protocol.path;
360
368
  const dataRoutes = {
369
+ daemon: this.opts.daemon,
361
370
  services: this.opts.services,
362
371
  readBody: (request) => this.readBody(request),
363
372
  json: (response, status, body) => this.json(response, status, body),
364
373
  requestError: (response, error, fallback) => this.requestError(response, error, fallback),
365
374
  };
366
- for (const route of [handleResourceRoute, handleModelRoute, handleMaintenanceRoute]) {
375
+ for (const route of [
376
+ handleResourceRoute,
377
+ handleModelRoute,
378
+ handleMaintenanceRoute,
379
+ handleDaemonStatusRoute,
380
+ ]) {
367
381
  if (await route(dataRoutes, method, path, req, res))
368
382
  return;
369
383
  }
@@ -373,13 +387,14 @@ export class DaemonControlApiServer {
373
387
  json: (response, status, body) => this.json(response, status, body),
374
388
  requestError: (response, error, fallback) => this.requestError(response, error, fallback),
375
389
  binary: writeBinaryResponse,
390
+ findRun: (id) => this.findRun(id),
376
391
  }, method, path, req, res))
377
392
  return;
378
393
  if (method === "POST" && path === "/runs") {
379
394
  return runStart.handleRunCreate({
380
395
  daemon: this.opts.daemon,
381
396
  readBody: (request) => this.readBody(request),
382
- requestError: (response, error) => this.requestError(response, error),
397
+ requestError: (response, error, fallback) => this.requestError(response, error, fallback),
383
398
  json: (response, status, body) => this.json(response, status, body),
384
399
  respondToAcceptedJob: (response, jobId) => this.respondToAcceptedJob(response, jobId),
385
400
  validateResources: this.opts.services?.validateResources,
@@ -469,51 +484,19 @@ export class DaemonControlApiServer {
469
484
  }, method, path, req, res))
470
485
  return;
471
486
  if (method === "POST" && path === "/threads") {
472
- const svc = this.opts.services?.createThread;
473
- if (!svc)
474
- return this.json(res, 501, { error: "threads are not supported by this engine build" });
475
- try {
476
- const body = await this.readBody(req);
477
- assertNoInlineSecretValues(body);
478
- const parsed = ControlThreadCreateRequest.parse(body);
479
- const idempotencyKey = runStart.requiredIdempotencyKey(req);
480
- let repoRoot = null;
481
- if (parsed.scope.kind === "project") {
482
- repoRoot = runStart.normalizeExistingProjectRoot(parsed.scope.root);
483
- }
484
- const thread = await svc({
485
- title: parsed.title,
486
- folder: parsed.folder,
487
- repoRoot,
488
- // Carried explicitly through the SAME predicate the run route and the
489
- // partition router use: dropping it here would register a root the
490
- // wire contract promises never to register.
491
- ephemeral: isEphemeralRunScope(parsed.scope),
492
- mode: parsed.mode,
493
- workspace: parsed.workspace,
494
- authPreference: parsed.authPreference,
495
- credentialProfileId: parsed.credentialProfileId ?? null,
496
- access: parsed.access,
497
- primaryHarness: parsed.primaryHarness ?? null,
498
- eligibleHarnesses: parsed.eligibleHarnesses,
499
- idempotency: {
500
- key: idempotencyKey,
501
- client: "control-api",
502
- request: parsed,
503
- },
504
- });
505
- return this.json(res, 200, projectThread(thread, false));
506
- }
507
- catch (err) {
508
- return this.requestError(res, err);
509
- }
487
+ return handleThreadCreate({
488
+ services: this.opts.services,
489
+ readBody: (request) => this.readBody(request),
490
+ json: (response, status, body) => this.json(response, status, body),
491
+ requestError: (response, error) => this.requestError(response, error),
492
+ }, req, res);
510
493
  }
511
494
  if (method === "GET" && path === "/threads") {
512
495
  const svc = this.opts.services?.listThreads;
513
496
  if (!svc)
514
497
  return this.json(res, 501, { error: "threads are not supported by this engine build" });
515
498
  const { threads, problems } = await svc();
516
- const runs = await this.opts.daemon.list();
499
+ const runs = await readThreadCommands(this.opts.daemon, threads);
517
500
  const blocked = new Set(runs.filter((r) => this.runNeedsAttention(r)).map((r) => r.runId ?? r.id));
518
501
  return this.json(res, 200, ControlThreadListResponse.parse({
519
502
  threads: threads.map((t) => projectThread(t, blocked.has(t.head_run_id ?? ""))),
@@ -527,7 +510,9 @@ export class DaemonControlApiServer {
527
510
  return this.json(res, 501, { error: "threads are not supported by this engine build" });
528
511
  try {
529
512
  const detail = await svc(decodeURIComponent(threadDetailMatch[1]));
530
- const runs = await this.opts.daemon.list();
513
+ const runs = await this.opts.daemon.list({
514
+ threadId: decodeURIComponent(threadDetailMatch[1]),
515
+ });
531
516
  const byRun = new Map(runs.map((r) => [r.runId ?? r.id, r]));
532
517
  const thread = detail.thread;
533
518
  const cards = projectTurnRunCards(detail.turns, runs, (record) => this.summarizeRunOrDiagnostic(record));
@@ -653,7 +638,11 @@ export class DaemonControlApiServer {
653
638
  if (!result.revertable || !result.revertAnchorId) {
654
639
  throw Object.assign(new Error("this run produced no revertable in-place change"), { status: 409 });
655
640
  }
656
- const repoRoot = recordedExecutionRoot(rec) ?? applyTargetRoot({ kind: "original_project" }, rec);
641
+ // A delegated run's live tree is its recorded caller-owned
642
+ // address; it never falls back to the stable project identity.
643
+ const repoRoot = recordedExecutionRoot(rec) ??
644
+ runExecutionWorkspaceRoot(rec) ??
645
+ applyTargetRoot({ kind: "original_project" }, rec);
657
646
  if (!repoRoot) {
658
647
  throw Object.assign(new Error("cannot resolve the in-place project root to revert"), { status: 400 });
659
648
  }
@@ -707,25 +696,7 @@ export class DaemonControlApiServer {
707
696
  key: decisionKey,
708
697
  operation: "run.decision.discard",
709
698
  request: { runId: rec.runId ?? rec.id, body },
710
- work: async () => {
711
- const files = readFilesWorkProduct(rec);
712
- const state = controlRunResult(rec).applyState;
713
- if (!TERMINAL_STATES.has(rec.state) ||
714
- !files ||
715
- files.manifest.isolation !== "envelope" ||
716
- (state !== "not_applied" && state !== "discarded"))
717
- throw Object.assign(new Error("Only an unapplied copied files result can be discarded; direct effects remain in place"), { status: 409 });
718
- markRunApplyState(rec, "discarded", undefined, true);
719
- appendRunAuditEvent(rec, "control.applied", {
720
- decision: "discard",
721
- manifest_sha256: files.manifestSha256,
722
- });
723
- return ControlRunDecisionResponse.parse({
724
- accepted: true,
725
- status: "discarded",
726
- message: "Pending file application discarded; retained result follows normal retention.",
727
- });
728
- },
699
+ work: () => discardRunResult(rec),
729
700
  }));
730
701
  return this.json(res, 200, response);
731
702
  }
@@ -751,18 +722,16 @@ export class DaemonControlApiServer {
751
722
  return this.requestError(res, error);
752
723
  }
753
724
  }
754
- const patch = readPatch(rec);
755
- if (patch === null)
756
- return this.json(res, 404, { error: "no patch artifact for this run" });
757
- if (containsSecretLikeToken(patch))
758
- return this.json(res, 409, { error: "patch contains secret-like token; refusing apply" });
759
- const repoRoot = applyTargetRoot(body.target ?? { kind: "original_project" }, rec);
760
- if (!repoRoot)
761
- return this.json(res, 400, { error: "project root is required for apply" });
762
- const absoluteRepoError = runStart.validateAbsoluteRepoRoot(repoRoot);
763
- if (absoluteRepoError)
764
- return this.json(res, 400, { error: absoluteRepoError });
765
725
  try {
726
+ const patch = readPatch(rec); // typed 409 when the exact bytes are unavailable
727
+ if (patch === null)
728
+ return this.json(res, 404, { error: "no patch artifact for this run" });
729
+ const repoRoot = applyTargetRoot(body.target ?? { kind: "original_project" }, rec);
730
+ if (!repoRoot)
731
+ return this.json(res, 400, { error: "project root is required for apply" });
732
+ const absoluteRepoError = runStart.validateAbsoluteRepoRoot(repoRoot);
733
+ if (absoluteRepoError)
734
+ return this.json(res, 400, { error: absoluteRepoError });
766
735
  const response = await this.chainRunMutation(rec, () => runIdempotentDelivery(this.opts.services, {
767
736
  params: rec.params,
768
737
  key: decisionKey,
@@ -774,7 +743,10 @@ export class DaemonControlApiServer {
774
743
  repoRoot,
775
744
  },
776
745
  work: async () => {
777
- const delivered = await verifyAndDeliver(repoRoot, patch, { mode: body.applyMode ?? "apply" }, gateSpecsForRun(rec), (freshVerify) => applyGateError(rec, patch, repoRoot, this.operatorDecisionFor(rec), freshVerify));
746
+ const delivered = await verifyAndDeliver(repoRoot, patch, {
747
+ mode: body.applyMode ?? "apply",
748
+ secretLikeBinary: exactPatch.runPatchHasSecretLikeBinary(rec),
749
+ }, gateSpecsForRun(rec), (freshVerify) => applyGateError(rec, patch, repoRoot, this.operatorDecisionFor(rec), freshVerify));
778
750
  if (delivered.refused) {
779
751
  throw Object.assign(new Error(delivered.detail ?? "delivery refused"), {
780
752
  status: 409,
@@ -1370,7 +1342,7 @@ export class DaemonControlApiServer {
1370
1342
  /** Persisted Delegate-only descendant graph. `parentRunId` alone is broader
1371
1343
  * thread/retry lineage and deliberately does not participate. */
1372
1344
  async delegatedDescendants(parentRunId) {
1373
- return delegatedDescendantsFromRecords(parentRunId, await this.opts.daemon.list());
1345
+ return delegatedDescendantsFromRecords(parentRunId, await this.opts.daemon.list({ delegatedDescendantsOf: parentRunId }));
1374
1346
  }
1375
1347
  /**
1376
1348
  * GET /v2/runs (QA-052): a bounded, newest-first, keyset-paginated page of run
@@ -1391,7 +1363,7 @@ export class DaemonControlApiServer {
1391
1363
  catch (error) {
1392
1364
  return this.requestError(res, error);
1393
1365
  }
1394
- const { page, hasMore, nextCursor } = selectRunListPage(await this.opts.daemon.list(), query);
1366
+ const { page, hasMore, nextCursor } = selectRunListPage(await this.opts.daemon.list({ page: query }), query);
1395
1367
  return this.json(res, 200, ControlRunListResponse.parse({
1396
1368
  runs: page.map((r) => this.summarizeRunOrDiagnostic(r)),
1397
1369
  nextCursor,
@@ -1415,10 +1387,9 @@ export class DaemonControlApiServer {
1415
1387
  const ws = thread?.workspace;
1416
1388
  if (!ws)
1417
1389
  return null;
1418
- return {
1419
- mode: ws.mode === "isolated" ? "isolated" : "in_place",
1420
- worktreePath: typeof ws.worktree_path === "string" && ws.worktree_path.trim() ? ws.worktree_path : null,
1421
- };
1390
+ const mode = ws.mode === "isolated" || ws.mode === "delegated" ? ws.mode : "in_place";
1391
+ const tree = mode === "delegated" ? ws.workspace_root : ws.worktree_path;
1392
+ return { mode, worktreePath: typeof tree === "string" && tree.trim() ? tree : null };
1422
1393
  }
1423
1394
  operatorDecisionFor(rec) {
1424
1395
  return this.opts.services?.operatorDecision?.(rec.runId ?? rec.id, rec.params) ?? null;
@@ -1427,8 +1398,10 @@ export class DaemonControlApiServer {
1427
1398
  const decision = this.operatorDecisionFor(rec);
1428
1399
  if (!decision)
1429
1400
  return null;
1430
- const patch = readTextArtifact(rec, "final/patch.diff", false);
1431
- return patch !== null && decision.patchSha256 === sha256(patch) ? decision : null;
1401
+ // INV-062: the decision binds to the EXACT patch, never to a redacted copy.
1402
+ return decision.patchSha256 === exactPatch.runPatchDigest(rec, runRepoRoot(rec))
1403
+ ? decision
1404
+ : null;
1432
1405
  }
1433
1406
  /** The RISK-OVERRIDABLE needs-decision signal: a terminal run whose review is
1434
1407
  * blocked or checks failed, with no valid operator decision recorded. This is
@@ -1515,7 +1488,7 @@ export class DaemonControlApiServer {
1515
1488
  return {
1516
1489
  turnCtx: this.threadTurnRouteCtx(),
1517
1490
  services: this.opts.services,
1518
- listRuns: () => this.opts.daemon.list(),
1491
+ listRuns: (query) => this.opts.daemon.list(query),
1519
1492
  readBody: (req) => this.readBody(req),
1520
1493
  json: (res, status, body) => this.json(res, status, body),
1521
1494
  requestError: (res, error) => this.requestError(res, error),
@@ -1692,20 +1665,6 @@ function readFailure(rec) {
1692
1665
  runDir: rec.runDir ?? null,
1693
1666
  });
1694
1667
  }
1695
- function appendRunAuditEvent(rec, type, payload) {
1696
- if (!rec.runDir)
1697
- return;
1698
- try {
1699
- // Single-counter invariant: while the run is active its EventLog owns the
1700
- // seq space, so audit records MUST route through it (appendRunEvent does;
1701
- // file-tail stamping only applies once the run is terminal). A tail-read
1702
- // here would duplicate ids and break SSE Last-Event-ID resume.
1703
- appendRunEvent(join(rec.runDir, "events.jsonl"), rec.runId ?? rec.id, rec.taskId ?? "unknown", type, payload);
1704
- }
1705
- catch {
1706
- /* audit append must not change control behavior */
1707
- }
1708
- }
1709
1668
  /** v0.9 strategy flags projected back so surfaces can tell a race from a repair loop. */
1710
1669
  function strategyFromParams(p) {
1711
1670
  if (p["untilClean"] === true)
@@ -1778,6 +1737,7 @@ function winnerWorkStateFor(rec) {
1778
1737
  }
1779
1738
  function summarizeRun(rec, eventsSnapshot) {
1780
1739
  const p = paramsRecord(rec);
1740
+ const project = projectMetadata(rec);
1781
1741
  // safeParse everywhere: one malformed job record (e.g. an old/foreign mode id)
1782
1742
  // must degrade to an unknown field, never 500 the whole run list forever.
1783
1743
  const parsedMode = ModeKind.safeParse(p["mode"]);
@@ -1828,15 +1788,18 @@ function summarizeRun(rec, eventsSnapshot) {
1828
1788
  task?.run_lineage.delegated_from_run_id ??
1829
1789
  null,
1830
1790
  delegation: telemetry?.delegation ?? null,
1791
+ ...continuationSummary(rec, eventsSnapshot),
1831
1792
  taskId: rec.taskId,
1832
1793
  state: rec.state,
1833
1794
  runDir: rec.runDir,
1834
1795
  error: rec.error,
1835
1796
  failure,
1836
- project: projectMetadata(rec),
1797
+ project,
1798
+ executionRoot: runExecutionRoot(rec, project.root),
1837
1799
  mode: parsedMode.success ? parsedMode.data : undefined,
1838
1800
  strategy: strategyFromParams(p),
1839
- prompt: typeof p["prompt"] === "string" ? redactPrompt(p["prompt"]) : undefined,
1801
+ prompt: rec.promptPreview ??
1802
+ (typeof p["prompt"] === "string" ? redactPrompt(p["prompt"]) : undefined),
1840
1803
  harnesses: Array.isArray(p["harnesses"])
1841
1804
  ? p["harnesses"].filter((x) => typeof x === "string")
1842
1805
  : undefined,
@@ -2125,7 +2088,7 @@ function budgetSnapshot(rec, decision, eventsSnapshot, integrity) {
2125
2088
  });
2126
2089
  }
2127
2090
  function readPatch(rec) {
2128
- return readRawTextArtifact(rec, "final/patch.diff");
2091
+ return exactPatch.readExactRunPatch(rec, runRepoRoot(rec));
2129
2092
  }
2130
2093
  function runRepoRoot(rec) {
2131
2094
  const p = paramsRecord(rec);
@@ -2159,7 +2122,7 @@ function runRepoRoot(rec) {
2159
2122
  function applyTargetRoot(target, rec) {
2160
2123
  if (target.kind === "project")
2161
2124
  return target.root;
2162
- return runRepoRoot(rec);
2125
+ return runExecutionRoot(rec, runRepoRoot(rec));
2163
2126
  }
2164
2127
  /** Project the run record into the delivery package's single-owner apply gate. */
2165
2128
  function applyGateError(rec, patch, targetRepoRoot, operatorDecision, finalVerify) {
@@ -2178,7 +2141,7 @@ function applyGateInputFor(rec, patch, targetRepoRoot, operatorDecision) {
2178
2141
  workProduct: safeReadStructuredArtifact(rec, "final/work_product.yaml", WorkProduct),
2179
2142
  patch,
2180
2143
  ...(files ? { filesManifest: files.manifest, manifestSha256: files.manifestSha256 } : {}),
2181
- originalRepoRoot: runRepoRoot(rec),
2144
+ originalRepoRoot: runExecutionRoot(rec, runRepoRoot(rec)),
2182
2145
  targetRepoRoot,
2183
2146
  operatorDecision: operatorDecision
2184
2147
  ? {
@@ -2202,7 +2165,7 @@ function applyGateInputFor(rec, patch, targetRepoRoot, operatorDecision) {
2202
2165
  /**
2203
2166
  * The GET /runs/:id projection of the apply gate: null when the run has no
2204
2167
  * patch artifact (nothing to apply); otherwise the derived verdict against
2205
- * the run's own original project root.
2168
+ * the run's recorded delivery target.
2206
2169
  */
2207
2170
  /** Readiness AND open questions of a plan run, from ONE read of the same
2208
2171
  * final/questions.json artifact (D17) — no plan-text re-parse, no double read. */
@@ -2222,14 +2185,17 @@ function councilFor(rec, mode) {
2222
2185
  return safeReadStructuredArtifact(rec, "council/membership.yaml", CouncilProjection);
2223
2186
  }
2224
2187
  function applyEligibilityFor(rec, operatorDecision) {
2225
- const patch = readPatch(rec);
2188
+ const root = runExecutionRoot(rec, runRepoRoot(rec));
2189
+ const { patch, unavailable } = exactPatch.resolveRunPatch(rec, runRepoRoot(rec));
2226
2190
  const files = readFilesWorkProduct(rec);
2227
2191
  if (!files && (patch === null || patch.trim() === ""))
2228
2192
  return null;
2229
- const root = runRepoRoot(rec);
2230
2193
  if (!root)
2231
2194
  return null;
2232
- return deriveApplyEligibility(applyGateInputFor(rec, patch ?? "", root, operatorDecision));
2195
+ return deriveApplyEligibility({
2196
+ ...applyGateInputFor(rec, patch ?? "", root, operatorDecision),
2197
+ exactPatchUnavailable: unavailable,
2198
+ });
2233
2199
  }
2234
2200
  /** Compatibility projection for artifact-only CLI reads; the journal record is authority. */
2235
2201
  function writeOperatorDecisionProjection(rec, record) {