@zq-silk/yui 0.13.9 → 0.14.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 (229) hide show
  1. package/ARCHITECTURE.md +69 -428
  2. package/README.md +108 -298
  3. package/dist/agent/managedRuntimeEnvironment.js +2 -2
  4. package/dist/cli/commandCatalog.js +39 -77
  5. package/dist/cli/interactionCandidates.js +4 -4
  6. package/dist/cli/interactionPolicy.js +7 -10
  7. package/dist/cli/invocationRouter.js +1 -1
  8. package/dist/cli/updateCommand.js +6 -27
  9. package/dist/cli/updateOrchestrator.js +63 -484
  10. package/dist/cli/updatePorts.js +15 -333
  11. package/dist/cli/upgradeCommand.js +8 -65
  12. package/dist/cli.js +57 -242
  13. package/dist/commands/configCommands.js +11 -62
  14. package/dist/commands/executionAuditCommands.js +23 -23
  15. package/dist/commands/globalRoleCommands.js +2 -7
  16. package/dist/commands/jobCommands.js +1 -4
  17. package/dist/commands/projectCommands.js +5 -5
  18. package/dist/commands/releaseCommands.js +3 -10
  19. package/dist/commands/resourcesCommands.js +2 -2
  20. package/dist/commands/sessionCommands.js +2 -2
  21. package/dist/commands/taskActor.js +31 -39
  22. package/dist/commands/taskChangeSetCommands.js +5 -12
  23. package/dist/commands/taskCommands.js +464 -1249
  24. package/dist/commands/taskContextCommand.js +35 -36
  25. package/dist/commands/taskExecutionCommands.js +33 -46
  26. package/dist/commands/taskInputCommands.js +20 -42
  27. package/dist/commands/taskNextActionCommand.js +9 -9
  28. package/dist/commands/taskOverviewCommand.js +56 -95
  29. package/dist/commands/taskRoleRuntimeStatus.js +77 -107
  30. package/dist/commands/taskUpstreamCommands.js +3 -3
  31. package/dist/commands/taskWorkspaceCommands.js +7 -79
  32. package/dist/commands/telemetryCommands.js +32 -124
  33. package/dist/commands/workflowCommands.js +3 -3
  34. package/dist/completion/fileCompletionManager.js +1 -1
  35. package/dist/config/configCatalog.js +4 -6
  36. package/dist/config/yuiConfig.js +6 -43
  37. package/dist/context/dispatchContext.js +14 -30
  38. package/dist/context/roleSessionContext.js +2 -2
  39. package/dist/context/sessionBootstrapManifest.js +2 -2
  40. package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
  41. package/dist/context/turnInputContract.js +204 -0
  42. package/dist/context/wakeNotification.js +2 -2
  43. package/dist/controller/agentRuntimeObserver.js +18 -18
  44. package/dist/controller/clientRuntime.js +13 -20
  45. package/dist/controller/controller.js +89 -116
  46. package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
  47. package/dist/controller/jobControl.js +17 -17
  48. package/dist/controller/resourceInventory.js +6 -6
  49. package/dist/controller/resourceInventoryLinux.js +38 -59
  50. package/dist/controller/resourceInventoryRpc.js +3 -3
  51. package/dist/controller/resourceInventoryWorker.js +1 -1
  52. package/dist/controller/runtime.js +46 -53
  53. package/dist/controller/runtimeEventInbox.js +23 -31
  54. package/dist/controller/runtimeEventProcessor.js +15 -15
  55. package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
  56. package/dist/controller/runtimeLaunchCoordinator.js +3 -3
  57. package/dist/controller/runtimeObservationHook.js +11 -8
  58. package/dist/controller/sessionNotify.js +11 -11
  59. package/dist/controller/structuredProviderObservation.js +89 -16
  60. package/dist/coordination/workMailbox.js +309 -579
  61. package/dist/coordination/workMailboxQueue.js +5 -6
  62. package/dist/core/controllerClient.js +1 -91
  63. package/dist/core/controllerServer.js +3 -6
  64. package/dist/doctor/doctor.js +51 -314
  65. package/dist/execution/candidateConvergence.js +3 -3
  66. package/dist/execution/executionGroup.js +32 -33
  67. package/dist/execution/executionHealth.js +36 -38
  68. package/dist/execution/resourceBroker.js +12 -12
  69. package/dist/executor/agentExecutor.js +30 -265
  70. package/dist/executor/effectiveLaunch.js +9 -19
  71. package/dist/executor/executorRegistry.js +43 -17
  72. package/dist/executor/fileRoleLaunchPlanner.js +56 -56
  73. package/dist/executor/launchPlan.js +3 -3
  74. package/dist/executor/workspacePreflightClassification.js +16 -16
  75. package/dist/grant/capabilityGrant.js +20 -25
  76. package/dist/input/inputRequest.js +12 -12
  77. package/dist/integration/changeSet.js +9 -13
  78. package/dist/integration/changeSetManifest.js +1 -1
  79. package/dist/integration/gitIntegrationService.js +2 -2
  80. package/dist/integration/integrationQueueService.js +5 -21
  81. package/dist/integration/overlapDiagnostics.js +2 -2
  82. package/dist/interaction/operatorPresentation.js +1 -1
  83. package/dist/job/jobRunner.js +1 -1
  84. package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
  85. package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
  86. package/dist/message/message.js +6 -6
  87. package/dist/observability/executionAudit.js +74 -86
  88. package/dist/observability/faultClassification.js +7 -24
  89. package/dist/observability/orchestrationMetrics.js +5 -5
  90. package/dist/observability/runtimeIdentity.js +34 -101
  91. package/dist/profile/agentProfile.js +1 -1
  92. package/dist/release/cliHomeReleaseFence.js +3 -3
  93. package/dist/release/releaseWorkflowEngine.js +6 -10
  94. package/dist/release/releaseWorkflowPorts.js +2 -2
  95. package/dist/release/workflowFileLock.js +1 -1
  96. package/dist/repository/checkoutSwap.js +1 -1
  97. package/dist/repository/gitWorkspace.js +0 -13
  98. package/dist/repository/project.js +1 -1
  99. package/dist/repository/projectMaintenanceLock.js +1 -2
  100. package/dist/repository/taskBaseFreshness.js +13 -13
  101. package/dist/repository/taskWorkspaceCoordinator.js +21 -25
  102. package/dist/repository/taskWorkspaceIdentity.js +8 -21
  103. package/dist/repository/taskWorkspacePreparer.js +63 -620
  104. package/dist/resources/autoResourceGc.js +3 -3
  105. package/dist/resources/liveReferences.js +1 -1
  106. package/dist/resources/resourceGc.js +1 -1
  107. package/dist/resources/sqliteResourceRegistry.js +1 -1
  108. package/dist/review/reviewDecision.js +7 -7
  109. package/dist/review/reviewFindingLedger.js +1 -1
  110. package/dist/review/reviewOutcomeClassifier.js +35 -50
  111. package/dist/review/reviewRound.js +14 -25
  112. package/dist/review/reviewerAvailability.js +4 -4
  113. package/dist/review/taskFinalReviewContractResolution.js +41 -0
  114. package/dist/role/role.js +6 -3
  115. package/dist/runtime/agentDriver.js +4 -0
  116. package/dist/runtime/agentHost.js +151 -36
  117. package/dist/runtime/builtinAgentDrivers.js +79 -2
  118. package/dist/runtime/codexAppServerRuntime.js +122 -0
  119. package/dist/runtime/continuationManager.js +3 -5
  120. package/dist/runtime/exactControlPlane.js +17 -42
  121. package/dist/runtime/firstProgressAdvisory.js +1 -1
  122. package/dist/runtime/index.js +2 -2
  123. package/dist/runtime/launchDiagnostics.js +2 -2
  124. package/dist/runtime/lifecycleReservation.js +4 -4
  125. package/dist/runtime/processExitObservation.js +1 -1
  126. package/dist/runtime/processExitOutbox.js +0 -32
  127. package/dist/runtime/promptEnvelope.js +9 -9
  128. package/dist/runtime/providerContinuation.js +2 -2
  129. package/dist/runtime/providerContinuationReconciliationService.js +2 -2
  130. package/dist/runtime/providerRuntimeIdentity.js +76 -24
  131. package/dist/runtime/recentTurnIds.js +38 -0
  132. package/dist/runtime/runtimeContinuationProjection.js +2 -2
  133. package/dist/runtime/runtimeHealthPolicy.js +1 -1
  134. package/dist/runtime/runtimeObservation.js +58 -21
  135. package/dist/runtime/runtimeProjection.js +19 -27
  136. package/dist/runtime/sessionLaunchRequest.js +2 -2
  137. package/dist/runtime/structuredProviderHost.js +177 -12
  138. package/dist/runtime/tmuxAdapters.js +46 -11
  139. package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
  140. package/dist/scheduler/leaderWakeupProcessor.js +147 -445
  141. package/dist/scheduler/operatorEvent.js +4 -6
  142. package/dist/scheduler/resourceQueueProjection.js +71 -0
  143. package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
  144. package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
  145. package/dist/scheduler/taskExecutionProjection.js +99 -99
  146. package/dist/scheduler/taskObservabilityProjection.js +10 -10
  147. package/dist/scheduler/taskWake.js +4 -4
  148. package/dist/scheduler/wakeReason.js +6 -10
  149. package/dist/scheduler/wakeupQueue.js +0 -9
  150. package/dist/setup/setupCommand.js +2 -2
  151. package/dist/storage/currentTaskStore.js +46 -0
  152. package/dist/storage/persistenceWorker.js +1 -1
  153. package/dist/storage/sqliteSchema.js +81 -485
  154. package/dist/storage/sqliteStore.js +206 -365
  155. package/dist/storage/storageSchema.js +1 -15
  156. package/dist/storage/storageVersions.js +8 -9
  157. package/dist/storage/storeRpc.js +10 -34
  158. package/dist/storage/taskStore.js +63 -3415
  159. package/dist/storage/upgrade/recordVersions.js +55 -149
  160. package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
  161. package/dist/task/completionReadiness.js +5 -5
  162. package/dist/task/deliveryGuard.js +10 -10
  163. package/dist/task/nextAction.js +74 -77
  164. package/dist/task/task.js +2 -4
  165. package/dist/task/taskRecordReference.js +3 -3
  166. package/dist/task/taskRecordRetirement.js +1 -1
  167. package/dist/telemetry/sqliteTelemetryStore.js +53 -53
  168. package/dist/telemetry/telemetryConfig.js +14 -14
  169. package/dist/telemetry/telemetryWiring.js +8 -14
  170. package/dist/turn/turn.js +356 -0
  171. package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
  172. package/dist/verification/gateArtifactStore.js +2 -2
  173. package/dist/web/assets/client/app.js +1 -1
  174. package/dist/web/assets/client/components.js +29 -54
  175. package/dist/web/assets/client/i18n.js +17 -37
  176. package/dist/web/assets/client/view.js +37 -37
  177. package/dist/web/assets/shell.js +1 -1
  178. package/dist/web/assets/styles/cards.js +5 -5
  179. package/dist/web/assets/styles/widgets.js +1 -2
  180. package/dist/web/webSnapshot.js +65 -60
  181. package/dist/workItem/workItem.js +11 -11
  182. package/dist/workspace/workItemChangeSetManager.js +1 -1
  183. package/docs/task-local-identity.md +5 -5
  184. package/i18n/README.zh-CN.md +41 -54
  185. package/package.json +1 -1
  186. package/skills/yui-leader/SKILL.md +85 -83
  187. package/skills/yui-operator/SKILL.md +16 -16
  188. package/skills/yui-reviewer/SKILL.md +10 -10
  189. package/skills/yui-runtime/SKILL.md +17 -18
  190. package/skills/yui-worker/SKILL.md +16 -18
  191. package/dist/context/runContextContract.js +0 -162
  192. package/dist/executor/turnCompletion.js +0 -126
  193. package/dist/review/taskFinalReviewContractEvent.js +0 -1
  194. package/dist/review/taskFinalReviewContractRebind.js +0 -367
  195. package/dist/run/agentRun.js +0 -356
  196. package/dist/run/rejectedYieldAttempt.js +0 -221
  197. package/dist/run/runControlRequest.js +0 -50
  198. package/dist/run/yieldReceipt.js +0 -65
  199. package/dist/runtime/turnCompletion.js +0 -3
  200. package/dist/scheduler/actionability.js +0 -323
  201. package/dist/scheduler/activeRoleRunDelivery.js +0 -868
  202. package/dist/scheduler/activeTaskProgress.js +0 -126
  203. package/dist/storage/compatibleTaskStore.js +0 -248
  204. package/dist/storage/migration/baseline.js +0 -78
  205. package/dist/storage/migration/classifier.js +0 -51
  206. package/dist/storage/migration/compatibleCodec.js +0 -53
  207. package/dist/storage/migration/engine.js +0 -147
  208. package/dist/storage/migration/index.js +0 -33
  209. package/dist/storage/migration/planner.js +0 -154
  210. package/dist/storage/migration/productionRegistry.js +0 -3931
  211. package/dist/storage/migration/registry.js +0 -169
  212. package/dist/storage/migration/report.js +0 -54
  213. package/dist/storage/migration/types.js +0 -31
  214. package/dist/storage/upgrade/homeClassification.js +0 -353
  215. package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
  216. package/dist/storage/upgrade/migrationReceipt.js +0 -67
  217. package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
  218. package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
  219. package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
  220. package/dist/storage/upgrade/recordVersionScan.js +0 -176
  221. package/dist/storage/upgrade/sqliteFileSet.js +0 -50
  222. package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
  223. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
  224. package/dist/storage/upgrade/sqliteStateMigration.js +0 -918
  225. package/dist/storage/upgrade/switchProgress.js +0 -80
  226. package/dist/storage/upgrade/upgradeReceipt.js +0 -161
  227. package/dist/storage/upgradeCoordination.js +0 -187
  228. package/dist/storage/upgradeFence.js +0 -366
  229. package/dist/telemetry/telemetryCompaction.js +0 -252
@@ -1,60 +1,16 @@
1
- import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
1
+ import { lstatSync, mkdirSync, realpathSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
4
  import { isDeepStrictEqual } from "node:util";
5
- import { validateConfiguredAgent } from "../agent/agent.js";
6
5
  import { validateCapabilityGrant } from "../grant/capabilityGrant.js";
7
6
  import { validateReleaseWorkflow } from "../release/releaseWorkflow.js";
8
- import { publicationExternalKey, validatePublicationReference } from "../task/publicationReference.js";
9
- import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveContextBudget, resolveDeliveryTimeoutSeconds, resolveLeaderNextActionMode, resolveLeaderSemanticBudgetTurns, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode, resolveResourcesQuarantineTtlHours, resolveRuntimeHealth, resolveTelemetryEnabled, resolveTelemetryRunCap, resolveTelemetryTerminalKeep, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
7
+ import { validatePublicationReference } from "../task/publicationReference.js";
8
+ import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveDeliveryTimeoutSeconds, resolveLeaderNextActionMode, resolveLeaderSemanticBudgetTurns, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode, resolveResourcesQuarantineTtlHours, resolveRuntimeHealth, resolveTelemetryEnabled, resolveTelemetryTurnCap, resolveTelemetryTerminalKeep, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
10
9
  import { resolveTimeZone } from "../output/timePresentation.js";
11
- import { mailboxBatches, consumePendingBatch, mailboxHasWork, mailboxTargetKey, pendingLane, validateWorkMailbox } from "../coordination/workMailbox.js";
12
- import { validateContextSnapshot } from "../context/contextSnapshot.js";
13
- import { TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT } from "../review/taskFinalReviewContractEvent.js";
14
- import { validateInputRequest } from "../input/inputRequest.js";
15
- import { validateRoleSessionSet } from "../executor/agentExecutor.js";
16
- import { validateTaskMessage } from "../message/message.js";
17
- import { agentRunDeliveryReceiptId, validateAgentRun } from "../run/agentRun.js";
18
- import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
19
- import { FileSessionOwnerRegistry } from "../runtime/sessionOwnerRegistry.js";
20
10
  import { validateReviewConfig } from "../review/reviewConfig.js";
21
- import { validateReviewRound } from "../review/reviewRound.js";
22
- import { reviewFindingLedgerMode } from "../review/reviewFindingLedger.js";
23
- import { sameTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
24
- import { assertProjectCatalog, validateProject } from "../repository/project.js";
25
- import { generateHomeIdentity, validateHomeIdentity } from "../repository/homeIdentity.js";
26
- import { validateAgentProfile } from "../profile/agentProfile.js";
27
- import { validateChangeSet } from "../integration/changeSet.js";
28
- import { validateIntegrationAttempt } from "../integration/integrationAttempt.js";
29
- import { validateIntegrationQueueEntry } from "../integration/integrationQueueEntry.js";
30
- import { CURRENT_DURABLE_JOB_SCHEMA_VERSION, validDurableJobTransition, validateDurableJob } from "../job/durableJob.js";
31
- import { validateGlobalRole, validateTaskRole } from "../role/role.js";
32
- import { CURRENT_LEADER_FAILURE_SCHEMA_VERSION } from "../scheduler/leaderFailure.js";
33
- import { CURRENT_TASK_WAKE_SCHEMA_VERSION, validateTaskWake } from "../scheduler/taskWake.js";
34
- import { validateTask } from "../task/task.js";
35
- import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
36
- import { TASK_RECORD_ID_PREFIXES, validateTaskRecordReference } from "../task/taskRecordReference.js";
37
- import { workItemExecutionGroupById, validateWorkItem } from "../workItem/workItem.js";
38
- import { isExecutionGroupTransition, validateExecutionGroup } from "../execution/executionGroup.js";
39
- import { managedWorkspaceKey, validateManagedWorkspace } from "../worktree/managedWorkspace.js";
40
- import { gateArtifactKey, validateGateArtifact } from "../verification/gateArtifact.js";
41
- import { writeTextFileAtomically } from "./durableFile.js";
42
- import { assertHomeWritable } from "./upgradeFence.js";
43
- import { CURRENT_AGGREGATE_SCHEMA_VERSION, requireCompatibleStorageSchema, requireStorageSchema, writeCurrentStorageManifest } from "./storageSchema.js";
44
- export const STORAGE_STATE_FILE = "state.json";
45
- /** The root StorageState schema is the persisted aggregate document version. */
46
- export const CURRENT_STORAGE_STATE_SCHEMA_VERSION = CURRENT_AGGREGATE_SCHEMA_VERSION;
47
- export const CURRENT_CONFIG_SCHEMA_VERSION = 4;
48
- export const CURRENT_HOME_IDENTITY_SCHEMA_VERSION = 1;
49
- export const CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION = 3;
50
- /**
51
- * Persisted StorageState/StoredTask family versions owned by this boundary.
52
- *
53
- * Keep these names next to the strict parser/writer so the upgrade record map
54
- * can classify exactly the bytes this store accepts and emits. Nested session
55
- * members and the PendingWakeup projection are included as parser boundaries
56
- * below, but are not independent record-axis families.
57
- */
11
+ import { validateTaskRecordReference } from "../task/taskRecordReference.js";
12
+ export const CURRENT_CONFIG_SCHEMA_VERSION = 6;
13
+ /** Current SQLite payload-family versions owned by this storage boundary. */
58
14
  export const CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION = 2;
59
15
  export const CURRENT_PROJECT_SCHEMA_VERSION = 5;
60
16
  export const CURRENT_AGENT_PROFILE_SCHEMA_VERSION = 2;
@@ -65,38 +21,34 @@ export const CURRENT_TASK_BRIEF_SCHEMA_VERSION = 2;
65
21
  export const CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION = 1;
66
22
  export const CURRENT_TASK_ROLE_SCHEMA_VERSION = 4;
67
23
  export const CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION = 2;
68
- export const CURRENT_WORK_ITEM_SCHEMA_VERSION = 12;
24
+ export const CURRENT_WORK_ITEM_SCHEMA_VERSION = 13;
69
25
  export const CURRENT_REVIEW_ROUND_SCHEMA_VERSION = 6;
70
- export const CURRENT_CHANGE_SET_SCHEMA_VERSION = 3;
26
+ export const CURRENT_CHANGE_SET_SCHEMA_VERSION = 4;
71
27
  export const CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION = 5;
72
28
  export const CURRENT_MESSAGE_SCHEMA_VERSION = 3;
73
- export const CURRENT_INPUT_REQUEST_SCHEMA_VERSION = 2;
29
+ export const CURRENT_INPUT_REQUEST_SCHEMA_VERSION = 3;
74
30
  export const CURRENT_DECISION_SCHEMA_VERSION = 1;
75
31
  export const CURRENT_MILESTONE_SCHEMA_VERSION = 2;
76
32
  export const CURRENT_EVENT_SCHEMA_VERSION = 2;
77
- export const CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION = 1;
33
+ export const CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION = 2;
78
34
  export const CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION = 1;
79
- export const CURRENT_WORK_MAILBOX_SCHEMA_VERSION = 3;
35
+ export const CURRENT_WORK_MAILBOX_SCHEMA_VERSION = 5;
80
36
  export const CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION = 1;
81
- export const CURRENT_ROLE_AGENT_SESSION_SCHEMA_VERSION = 4;
82
37
  export const CURRENT_PENDING_WAKEUP_SCHEMA_VERSION = 1;
83
- const STORAGE_LOCK_DIRECTORY = ".state.lock";
84
- const LOCK_TIMEOUT_MS = 5_000;
85
- const LOCK_RETRY_MS = 10;
86
38
  export const COMPLETION_SHELLS = ["bash", "zsh", "fish"];
87
39
  /**
88
- * Lane-scoped active pointers live beside the legacy role pointers. The
40
+ * Lane-scoped active pointers live beside Role pointers. The
89
41
  * namespace starts with a slash because Role identities reject slashes; this
90
42
  * makes the two key spaces disjoint even for legal Role names such as
91
43
  * `lane:worker:1`.
92
44
  */
93
- export function executionLaneActiveRunKey(executionGroupId, executionLaneId) {
45
+ export function executionLaneActiveTurnKey(executionGroupId, executionLaneId) {
94
46
  return `/execution-lane/${encodeLaneKeyPart(executionGroupId)}:${encodeLaneKeyPart(executionLaneId)}`;
95
47
  }
96
48
  function encodeLaneKeyPart(value) {
97
49
  return encodeURIComponent(value).replace(/:/gu, "%3A");
98
50
  }
99
- export function executionLaneActiveRunKeyParts(key) {
51
+ export function executionLaneActiveTurnKeyParts(key) {
100
52
  const match = /^\/execution-lane\/([^:]+):([^:]+)$/u.exec(key);
101
53
  if (match === null)
102
54
  return null;
@@ -110,1774 +62,12 @@ export function executionLaneActiveRunKeyParts(key) {
110
62
  return null;
111
63
  }
112
64
  }
113
- /** The schema version of each persisted `state.json#/tasks/*` aggregate. */
114
- export const CURRENT_STORED_TASK_SCHEMA_VERSION = 18;
115
- /**
116
- * Persisted nested-record versions consumed by this store's strict parser.
117
- * Keep these named at the storage boundary so the upgrade record-axis map can
118
- * assert it is classifying the same bytes the store reads and writes.
119
- */
120
- export const CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION = 8;
121
65
  /**
122
- * v7 combines optional Issue 04 retry/receipt fields and Issue 05 Leader
123
- * actionability fields. All are optional, so the v6→v7 migration is a
124
- * version-only rewrite.
66
+ * Nested-record versions consumed by the current SQLite payload validators.
125
67
  */
126
- export const CURRENT_AGENT_RUN_SCHEMA_VERSION = 10;
68
+ export const CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION = 11;
69
+ export const CURRENT_TURN_SCHEMA_VERSION = 1;
127
70
  export const CURRENT_INTEGRATION_QUEUE_SCHEMA_VERSION = 1;
128
- export class FileTaskStore {
129
- rootDir;
130
- #transaction = null;
131
- #readCache = null;
132
- #normalizeState;
133
- #sessionOwnerRegistry;
134
- constructor(rootDir, options = {}) {
135
- this.rootDir = rootDir;
136
- this.#normalizeState = options.normalizeState;
137
- this.#requireReadableSchema();
138
- const snapshot = options.initialStateSnapshot;
139
- if (snapshot !== undefined) {
140
- // The one-snapshot open already fenced these bytes; run the same strict
141
- // parser the lazy path would, then warm the cache under the snapshot's
142
- // fingerprint. A later external writer still invalidates it on next read.
143
- this.#readCache = {
144
- fingerprint: snapshot.fingerprint,
145
- state: this.#parseState(snapshot.raw)
146
- };
147
- }
148
- }
149
- rootDirectory() { return this.rootDir; }
150
- transaction(execute) {
151
- if (this.#transaction !== null)
152
- return synchronousResult(execute(this));
153
- return this.#withWriteLock(() => {
154
- const state = this.#readCachedState();
155
- this.#transaction = { state, baseRevision: state.revision, dirty: false };
156
- try {
157
- const result = synchronousResult(execute(this));
158
- if (this.#transaction.dirty)
159
- this.#commit(state, this.#transaction.baseRevision);
160
- return result;
161
- }
162
- catch (error) {
163
- this.#readCache = null;
164
- throw error;
165
- }
166
- finally {
167
- this.#transaction = null;
168
- }
169
- });
170
- }
171
- withRuntimeEventTransaction(execute) {
172
- return this.transaction(() => execute());
173
- }
174
- async transactionAsync(execute) {
175
- if (this.#transaction !== null)
176
- return execute(this);
177
- const release = acquireStorageLock(this.rootDir);
178
- try {
179
- const state = this.#readCachedState();
180
- this.#transaction = { state, baseRevision: state.revision, dirty: false };
181
- try {
182
- const result = await execute(this);
183
- if (this.#transaction.dirty)
184
- this.#commit(state, this.#transaction.baseRevision);
185
- return result;
186
- }
187
- catch (error) {
188
- this.#readCache = null;
189
- throw error;
190
- }
191
- finally {
192
- this.#transaction = null;
193
- }
194
- }
195
- finally {
196
- release();
197
- }
198
- }
199
- getConfig() { return clone(this.#state().config); }
200
- getHomeIdentity() { return clone(this.#state().homeIdentity); }
201
- saveConfig(config) {
202
- const stored = versioned(config, CURRENT_CONFIG_SCHEMA_VERSION, "Yui config");
203
- validateYuiConfig(stored);
204
- this.#mutate((state) => { state.config = stored; });
205
- }
206
- saveConfiguredAgent(agent) {
207
- const stored = identified(agent, CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION, "id", agent.id, "Configured Agent");
208
- validateConfiguredAgent(stored);
209
- this.#mutate((state) => { state.configuredAgents[stored.id] = stored; });
210
- }
211
- createConfiguredAgentIfAbsent(agent) {
212
- return this.transaction((store) => {
213
- if (store.getConfiguredAgent(agent.id) !== null)
214
- return null;
215
- store.saveConfiguredAgent(agent);
216
- return clone(agent);
217
- });
218
- }
219
- updateConfiguredAgent(id, patch, now) {
220
- return this.transaction((store) => {
221
- const existing = store.getConfiguredAgent(id);
222
- if (existing === null)
223
- return null;
224
- const candidate = versioned({
225
- ...existing,
226
- ...clone(patch),
227
- updatedAt: now.toISOString()
228
- }, CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION, "Configured Agent");
229
- const unchanged = isDeepStrictEqual({ ...existing, updatedAt: candidate.updatedAt }, candidate);
230
- if (unchanged)
231
- return { status: "unchanged", agent: existing };
232
- store.saveConfiguredAgent(candidate);
233
- return { status: "updated", agent: candidate };
234
- });
235
- }
236
- listConfiguredAgents() { return values(this.#state().configuredAgents, "id"); }
237
- getConfiguredAgent(id) { return optional(this.#state().configuredAgents[id]); }
238
- removeConfiguredAgent(id) {
239
- return this.#remove((state) => state.configuredAgents, id);
240
- }
241
- nextProjectId() {
242
- return this.#nextGlobalId("project", (state) => Object.keys(state.projects));
243
- }
244
- saveProject(project) {
245
- const stored = identified(project, CURRENT_PROJECT_SCHEMA_VERSION, "id", project.id, "Project");
246
- validateProject(stored);
247
- this.#mutate((state) => {
248
- assertProjectCatalog([
249
- ...Object.values(state.projects).filter(({ id }) => id !== stored.id),
250
- stored
251
- ]);
252
- state.projects[stored.id] = stored;
253
- });
254
- }
255
- createProjectIfAbsent(project) {
256
- validateProject(project);
257
- return this.transaction((store) => {
258
- if (store.getProject(project.id) !== null)
259
- return null;
260
- try {
261
- assertProjectCatalog([...store.listProjects(), project]);
262
- }
263
- catch {
264
- return null;
265
- }
266
- store.saveProject(project);
267
- return clone(project);
268
- });
269
- }
270
- listProjects() { return values(this.#state().projects, "id"); }
271
- getProject(id) { return optional(this.#state().projects[id]); }
272
- removeProject(id) {
273
- return this.transaction((store) => {
274
- if (store.listTasks().some((task) => task.projectBindings.some((binding) => binding.projectId === id))) {
275
- throw new StorageRecordError(`Project is still used by a Task: ${id}`);
276
- }
277
- return this.#remove((state) => state.projects, id);
278
- });
279
- }
280
- summarizeProjectReferences(projectId) {
281
- const boundTasks = this.listTasks().filter((task) => task.projectBindings.some((binding) => binding.projectId === projectId));
282
- const activeTasks = boundTasks.filter((task) => task.status === "active");
283
- const unresolvedWorkItemRefs = [];
284
- const activeRunRefs = [];
285
- const unresolvedIntegrationRefs = [];
286
- for (const task of activeTasks) {
287
- for (const workItem of this.listWorkItems(task.id)) {
288
- if (workItem.status !== "completed" && workItem.status !== "retired") {
289
- unresolvedWorkItemRefs.push(`${task.id}/${workItem.id}`);
290
- }
291
- }
292
- for (const run of this.listAgentRuns(task.id)) {
293
- if (run.status === "active")
294
- activeRunRefs.push(`${task.id}/${run.id}`);
295
- }
296
- for (const attempt of this.listIntegrationAttempts(task.id)) {
297
- if (attempt.status === "running" || attempt.status === "blocked") {
298
- unresolvedIntegrationRefs.push(`${task.id}/${attempt.id}`);
299
- }
300
- }
301
- }
302
- return {
303
- projectId,
304
- boundTaskIds: boundTasks.map(({ id }) => id),
305
- activeTaskIds: activeTasks.map(({ id }) => id),
306
- unresolvedWorkItemRefs,
307
- activeRunRefs,
308
- unresolvedIntegrationRefs
309
- };
310
- }
311
- saveAgentProfile(profile) {
312
- const stored = identified(profile, CURRENT_AGENT_PROFILE_SCHEMA_VERSION, "id", profile.id, "Agent Profile");
313
- validateAgentProfile(stored);
314
- this.#mutate((state) => {
315
- const existing = state.agentProfiles[stored.id];
316
- if (existing !== undefined) {
317
- if (stored.revision < existing.revision) {
318
- throw new StorageRecordError(`Agent Profile revision cannot move backwards: ${stored.id}.`);
319
- }
320
- if (stored.revision === existing.revision
321
- && !isDeepStrictEqual(stored, existing)) {
322
- throw new StorageRecordError(`Agent Profile revision cannot be overwritten: ${stored.id}/${stored.revision}.`);
323
- }
324
- if (stored.revision > existing.revision + 1) {
325
- throw new StorageRecordError(`Agent Profile revision must be contiguous: ${stored.id}/${stored.revision}.`);
326
- }
327
- }
328
- else if (stored.revision !== 1) {
329
- throw new StorageRecordError(`A new Agent Profile must start at revision 1: ${stored.id}.`);
330
- }
331
- state.agentProfiles[stored.id] = stored;
332
- });
333
- }
334
- createAgentProfileIfAbsent(profile) {
335
- return this.transaction((store) => {
336
- if (store.getAgentProfile(profile.id) !== null)
337
- return null;
338
- store.saveAgentProfile(profile);
339
- return clone(profile);
340
- });
341
- }
342
- listAgentProfiles() {
343
- return values(this.#state().agentProfiles, "id");
344
- }
345
- getAgentProfile(id) {
346
- return optional(this.#state().agentProfiles[id]);
347
- }
348
- removeAgentProfile(id) {
349
- return this.#remove((state) => state.agentProfiles, id);
350
- }
351
- saveGlobalRole(role) {
352
- const stored = identified(role, CURRENT_GLOBAL_ROLE_SCHEMA_VERSION, "name", role.name, "Global Role");
353
- validateGlobalRole(stored);
354
- const sessions = this.getGlobalRoleSessionSet(stored.name);
355
- if (sessions !== null)
356
- assertSessionsMatchRole(sessions, stored);
357
- this.#mutate((state) => { state.globalRoles[stored.name] = stored; });
358
- }
359
- saveGlobalRoleWithSessionSet(role, sessions) {
360
- const storedRole = identified(role, CURRENT_GLOBAL_ROLE_SCHEMA_VERSION, "name", role.name, "Global Role");
361
- validateGlobalRole(storedRole);
362
- const storedSessions = sessions === null ? null : globalSessions(sessions);
363
- if (storedSessions !== null)
364
- assertSessionsMatchRole(storedSessions, storedRole);
365
- this.transaction(() => {
366
- this.#mutate((state) => {
367
- state.globalRoles[storedRole.name] = storedRole;
368
- if (storedSessions === null)
369
- delete state.globalRoleSessionSets[storedRole.name];
370
- else
371
- state.globalRoleSessionSets[storedRole.name] = storedSessions;
372
- });
373
- });
374
- }
375
- createGlobalRoleIfAbsent(role) {
376
- return this.transaction((store) => {
377
- if (store.getGlobalRole(role.name) !== null)
378
- return null;
379
- store.saveGlobalRole(role);
380
- return clone(role);
381
- });
382
- }
383
- listGlobalRoles() { return values(this.#state().globalRoles, "name"); }
384
- getGlobalRole(name) { return optional(this.#state().globalRoles[name]); }
385
- removeGlobalRole(name) {
386
- return this.transaction(() => {
387
- const removed = this.#remove((state) => state.globalRoles, name);
388
- this.#mutate((state) => { delete state.globalRoleSessionSets[name]; });
389
- return removed;
390
- });
391
- }
392
- getGlobalRoleSessionSet(name) {
393
- return optional(this.#state().globalRoleSessionSets[name]);
394
- }
395
- listGlobalRoleSessionSets() {
396
- return values(this.#state().globalRoleSessionSets, (set) => set.owner.roleName);
397
- }
398
- saveGlobalRoleSessionSet(sessions) {
399
- const stored = globalSessions(sessions);
400
- const role = this.getGlobalRole(stored.owner.roleName);
401
- if (role === null)
402
- throw new StorageRecordError(`Global Role not found: ${stored.owner.roleName}`);
403
- assertSessionsMatchRole(stored, role);
404
- this.#mutate((state) => { state.globalRoleSessionSets[stored.owner.roleName] = stored; });
405
- }
406
- nextTaskId() { return this.#nextGlobalId("task", (state) => Object.keys(state.tasks)); }
407
- saveTask(task) {
408
- const stored = validateTask(identified(task, CURRENT_TASK_SCHEMA_VERSION, "id", task.id, "Task"));
409
- this.#mutate((state) => {
410
- for (const binding of stored.projectBindings) {
411
- if (state.projects[binding.projectId] === undefined) {
412
- throw new StorageRecordError(`Task Project not found: ${binding.projectId}`);
413
- }
414
- }
415
- const aggregate = state.tasks[stored.id] ?? emptyStoredTask(stored);
416
- aggregate.task = stored;
417
- state.tasks[stored.id] = aggregate;
418
- });
419
- }
420
- listTasks() {
421
- // Clone only the Task headers, not the whole stored aggregate (events,
422
- // runs, messages): a scheduler pass lists Tasks per phase, and cloning
423
- // each aggregate's full event history turned every phase into a 32 MiB
424
- // projection. Callers that need a Task's events read them explicitly.
425
- return Object.values(this.#state().tasks)
426
- .map((aggregate) => clone(aggregate.task))
427
- .sort((left, right) => numericCompare(left.id, right.id));
428
- }
429
- listActiveTaskIds() {
430
- // The file rollback backend has no catalog index, so it filters the loaded
431
- // aggregate. Layout-7 Controller hot paths use SQLite's bounded selector.
432
- return this.listTasks()
433
- .filter((task) => task.status === "active" && task.executionGate.state === "enabled")
434
- .map((task) => task.id);
435
- }
436
- getStateRevision() { return this.#state().revision; }
437
- getTask(id) { return optional(this.#state().tasks[id]?.task); }
438
- readNextActionFacts(taskId) {
439
- const aggregate = this.#state().tasks[taskId];
440
- if (aggregate === undefined)
441
- return null;
442
- const events = values(aggregate.events, "id");
443
- const agentRuns = operationalTaskRecords(values(aggregate.agentRuns, "id"), events, "agent-run");
444
- return {
445
- task: {
446
- id: aggregate.task.id,
447
- status: aggregate.task.status,
448
- executionGate: aggregate.task.executionGate,
449
- projectBindings: aggregate.task.projectBindings,
450
- type: aggregate.task.type
451
- },
452
- workItems: values(aggregate.workItems, "id"),
453
- changeSets: values(aggregate.changeSets, "id"),
454
- integrations: values(aggregate.integrationAttempts, "id"),
455
- integrationQueueEntries: values(aggregate.integrationQueue, "id"),
456
- reviewRounds: values(aggregate.reviewRounds, "id"),
457
- taskFinalReviewContractEvents: events
458
- .filter((event) => event.type === TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT),
459
- reviewConfig: this.getReviewConfig(),
460
- openInputRequests: values(aggregate.inputRequests, "id")
461
- .filter((request) => request.status === "open"),
462
- activeRuns: agentRuns.filter((run) => run.status === "active"),
463
- leaderRuns: agentRuns.filter((run) => run.roleName === "leader"),
464
- reviewOutcomeEvidence: {
465
- agentRuns: agentRuns.filter((run) => run.purpose === "review"),
466
- // The rollback file backend has no finding-ledger records.
467
- reviewFindings: [],
468
- events: events.filter((event) => (event.type === "review.completed"))
469
- }
470
- };
471
- }
472
- readCompletionReadinessFacts(taskId) {
473
- const base = this.readNextActionFacts(taskId);
474
- if (base === null)
475
- return null;
476
- const aggregate = this.#state().tasks[taskId];
477
- const ledgerMode = reviewFindingLedgerMode(this.#state().config);
478
- // The file-rollback backend does not persist the finding ledger. Under
479
- // `enforce` the completion gate must fail closed with the same bounded
480
- // diagnosis as `listReviewFindings` instead of silently treating
481
- // "unsupported" as "no open findings".
482
- if (ledgerMode === "enforce") {
483
- throw new StorageRecordError(`Review findings require the SQLite backend (yui.db); migrate this Home with \`yui update\` before using the finding ledger on Task ${taskId}.`);
484
- }
485
- return {
486
- ...base,
487
- agentRuns: operationalTaskRecords(this.listAgentRuns(taskId), values(aggregate.events, "id"), "agent-run"),
488
- managedWorkspaces: values(aggregate.managedWorkspaces, (workspace) => managedWorkspaceKey(workspace.owner)),
489
- durableJobs: values(aggregate.durableJobs, "id"),
490
- integrationQueueEntries: values(aggregate.integrationQueue, "id"),
491
- // The SQLite backend supplies real findings; the file backend never
492
- // reaches this point under `enforce` (throw above).
493
- reviewFindings: [],
494
- reviewFindingLedgerMode: ledgerMode,
495
- events: values(aggregate.events, "id")
496
- };
497
- }
498
- getReviewConfig() {
499
- return optional(this.#state().config.review);
500
- }
501
- getTaskBrief(taskId) {
502
- return optional(this.#state().tasks[taskId]?.brief ?? undefined);
503
- }
504
- saveTaskBrief(taskId, brief) {
505
- const stored = storedTaskBrief(brief);
506
- this.#requireTaskForWrite(taskId);
507
- this.#mutate((state) => { state.tasks[taskId].brief = stored; });
508
- }
509
- clearTaskBrief(taskId) {
510
- this.#requireTaskForWrite(taskId);
511
- this.#mutate((state) => { state.tasks[taskId].brief = null; });
512
- }
513
- nextChangeSetId(taskId) {
514
- return this.#nextTaskRecordId(taskId, "changeSet");
515
- }
516
- saveChangeSet(taskId, changeSet) {
517
- const stored = identifiedChangeSet(changeSet, changeSet.id);
518
- validateChangeSet(stored);
519
- if (stored.taskId !== taskId) {
520
- throw new StorageRecordError(`ChangeSet belongs to another Task: ${stored.taskId}.`);
521
- }
522
- const aggregate = this.#requireTaskForWrite(taskId);
523
- const evidenceRound = Object.values(aggregate.reviewRounds).find(({ evidenceCommit, reviewBaseCommit }) => evidenceCommit !== undefined && evidenceCommit !== reviewBaseCommit && evidenceCommit === stored.headCommit);
524
- if (evidenceRound !== undefined) {
525
- throw new StorageRecordError(`ReviewRound evidence commit ${evidenceRound.id}/${stored.headCommit} cannot become a ChangeSet.`);
526
- }
527
- if (!aggregate.task.projectBindings.some(({ projectId }) => projectId === stored.projectId)) {
528
- throw new StorageRecordError(`ChangeSet Project does not match Task: ${stored.id}.`);
529
- }
530
- if (aggregate.workItems[stored.workItemId] === undefined) {
531
- throw new StorageRecordError(`ChangeSet Work Item not found: ${stored.workItemId}.`);
532
- }
533
- const existing = aggregate.changeSets[stored.id];
534
- if (existing !== undefined && !isDeepStrictEqual(existing, stored)) {
535
- throw new StorageRecordError(`ChangeSet is immutable: ${stored.id}.`);
536
- }
537
- this.#mutate((state) => {
538
- const task = state.tasks[taskId];
539
- observeTaskRecordId(task, "changeSet", stored.id);
540
- task.changeSets[stored.id] = stored;
541
- });
542
- }
543
- listChangeSets(taskId) {
544
- return values(this.#requireTask(taskId).changeSets, "id");
545
- }
546
- getChangeSet(taskId, changeSetId) {
547
- return optional(this.#state().tasks[taskId]?.changeSets[changeSetId]);
548
- }
549
- nextIntegrationAttemptId(taskId) {
550
- return this.#nextTaskRecordId(taskId, "integrationAttempt");
551
- }
552
- saveIntegrationAttempt(taskId, attempt) {
553
- const stored = identified(attempt, CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION, "id", attempt.id, "Integration Attempt");
554
- validateIntegrationAttempt(stored);
555
- if (stored.taskId !== taskId) {
556
- throw new StorageRecordError(`Integration Attempt belongs to another Task: ${stored.taskId}.`);
557
- }
558
- const aggregate = this.#requireTaskForWrite(taskId);
559
- if (!aggregate.task.projectBindings.some(({ projectId }) => projectId === stored.projectId)) {
560
- throw new StorageRecordError(`Integration Project does not match Task: ${stored.id}.`);
561
- }
562
- for (const changeSetId of stored.changeSetIds) {
563
- const changeSet = aggregate.changeSets[changeSetId];
564
- if (changeSet === undefined) {
565
- throw new StorageRecordError(`Integration ChangeSet not found: ${changeSetId}.`);
566
- }
567
- if (changeSet.projectId !== stored.projectId) {
568
- throw new StorageRecordError(`Integration ChangeSet belongs to another Project: ${changeSetId}.`);
569
- }
570
- const evidenceRound = Object.values(aggregate.reviewRounds).find(({ evidenceCommit, reviewBaseCommit }) => evidenceCommit !== undefined && evidenceCommit !== reviewBaseCommit && evidenceCommit === changeSet.headCommit);
571
- if (evidenceRound !== undefined) {
572
- throw new StorageRecordError(`ReviewRound evidence commit ${evidenceRound.id}/${changeSet.headCommit} cannot become an Integration source.`);
573
- }
574
- }
575
- const existing = aggregate.integrationAttempts[stored.id];
576
- if (existing !== undefined) {
577
- if (Date.parse(stored.updatedAt) < Date.parse(existing.updatedAt)) {
578
- throw new StorageRecordError(`Integration Attempt updatedAt cannot move backwards: ${stored.id}.`);
579
- }
580
- if (!validIntegrationTransition(existing, stored)) {
581
- throw new StorageRecordError(`Integration Attempt transition is invalid: ${stored.id}.`);
582
- }
583
- }
584
- this.#mutate((state) => {
585
- const task = state.tasks[taskId];
586
- observeTaskRecordId(task, "integrationAttempt", stored.id);
587
- task.integrationAttempts[stored.id] = stored;
588
- });
589
- }
590
- listIntegrationAttempts(taskId) {
591
- return values(this.#requireTask(taskId).integrationAttempts, "id");
592
- }
593
- getIntegrationAttempt(taskId, integrationId) {
594
- return optional(this.#state().tasks[taskId]?.integrationAttempts[integrationId]);
595
- }
596
- nextIntegrationQueueEntryId(taskId) {
597
- return this.#nextTaskRecordId(taskId, "integrationQueue");
598
- }
599
- saveIntegrationQueueEntry(taskId, entry) {
600
- const stored = identified(entry, CURRENT_INTEGRATION_QUEUE_SCHEMA_VERSION, "id", entry.id, "Integration queue entry");
601
- validateIntegrationQueueEntry(stored);
602
- if (stored.taskId !== taskId) {
603
- throw new StorageRecordError(`Integration queue entry belongs to another Task: ${stored.taskId}.`);
604
- }
605
- const aggregate = this.#requireTaskForWrite(taskId);
606
- if (!aggregate.task.projectBindings.some(({ projectId }) => projectId === stored.projectId)) {
607
- throw new StorageRecordError(`Integration queue Project does not match Task: ${stored.id}.`);
608
- }
609
- const changeSet = aggregate.changeSets[stored.changeSetId];
610
- if (changeSet === undefined) {
611
- throw new StorageRecordError(`Integration queue ChangeSet not found: ${stored.changeSetId}.`);
612
- }
613
- if (changeSet.projectId !== stored.projectId) {
614
- throw new StorageRecordError(`Integration queue ChangeSet belongs to another Project: ${stored.changeSetId}.`);
615
- }
616
- const existing = aggregate.integrationQueue[stored.id];
617
- if (existing !== undefined) {
618
- if (Date.parse(stored.updatedAt) < Date.parse(existing.updatedAt)) {
619
- throw new StorageRecordError(`Integration queue entry updatedAt cannot move backwards: ${stored.id}.`);
620
- }
621
- if (!validIntegrationQueueTransition(existing, stored)) {
622
- throw new StorageRecordError(`Integration queue entry transition is invalid: ${stored.id}.`);
623
- }
624
- }
625
- this.#mutate((state) => {
626
- const task = state.tasks[taskId];
627
- observeTaskRecordId(task, "integrationQueue", stored.id);
628
- task.integrationQueue[stored.id] = stored;
629
- });
630
- }
631
- listIntegrationQueueEntries(taskId) {
632
- return values(this.#requireTask(taskId).integrationQueue, "id");
633
- }
634
- getIntegrationQueueEntry(taskId, entryId) {
635
- return optional(this.#state().tasks[taskId]?.integrationQueue[entryId]);
636
- }
637
- nextDurableJobId(taskId) {
638
- return this.#nextTaskRecordId(taskId, "durableJob");
639
- }
640
- saveDurableJob(taskId, job) {
641
- const stored = identified(job, CURRENT_DURABLE_JOB_SCHEMA_VERSION, "id", job.id, "DurableJob");
642
- validateDurableJob(stored);
643
- if (stored.taskId !== taskId) {
644
- throw new StorageRecordError(`DurableJob belongs to another Task: ${stored.taskId}.`);
645
- }
646
- const aggregate = this.#requireTaskForWrite(taskId);
647
- if (!aggregate.task.projectBindings.some(({ projectId }) => projectId === stored.projectId)) {
648
- throw new StorageRecordError(`DurableJob Project does not match Task: ${stored.id}.`);
649
- }
650
- const existing = aggregate.durableJobs[stored.id];
651
- if (existing !== undefined) {
652
- if (Date.parse(stored.updatedAt) < Date.parse(existing.updatedAt)) {
653
- throw new StorageRecordError(`DurableJob updatedAt cannot move backwards: ${stored.id}.`);
654
- }
655
- if (!validDurableJobTransition(existing, stored)) {
656
- throw new StorageRecordError(`DurableJob transition is invalid: ${stored.id}.`);
657
- }
658
- }
659
- this.#mutate((state) => {
660
- const task = state.tasks[taskId];
661
- observeTaskRecordId(task, "durableJob", stored.id);
662
- task.durableJobs[stored.id] = stored;
663
- });
664
- }
665
- listDurableJobs(taskId) {
666
- return values(this.#requireTask(taskId).durableJobs, "id");
667
- }
668
- getDurableJob(taskId, jobId) {
669
- return optional(this.#state().tasks[taskId]?.durableJobs[jobId]);
670
- }
671
- findDurableJobByIdempotencyKey(taskId, key) {
672
- const aggregate = this.#state().tasks[taskId];
673
- if (aggregate === undefined)
674
- return null;
675
- const found = Object.values(aggregate.durableJobs)
676
- .find((job) => job.idempotencyKey === key);
677
- return optional(found);
678
- }
679
- listActiveDurableJobs() {
680
- // The file rollback backend has no secondary indexes, so it filters its
681
- // already-loaded aggregate here. Layout-7 Controller hot paths use the
682
- // SQLite implementation below, whose status index avoids this history
683
- // scan; the return contract and ordering stay identical across backends.
684
- const all = [];
685
- for (const aggregate of Object.values(this.#state().tasks)) {
686
- for (const job of Object.values(aggregate.durableJobs)) {
687
- if (job.status === "queued" || job.status === "running")
688
- all.push(job);
689
- }
690
- }
691
- return all
692
- .map((job) => clone(job))
693
- .sort((left, right) => numericCompare(`${left.taskId}/${left.id}`, `${right.taskId}/${right.id}`));
694
- }
695
- hasActiveDurableJobs() {
696
- for (const aggregate of Object.values(this.#state().tasks)) {
697
- for (const job of Object.values(aggregate.durableJobs)) {
698
- if (job.status === "queued" || job.status === "running")
699
- return true;
700
- }
701
- }
702
- return false;
703
- }
704
- saveRole(taskId, role) {
705
- const aggregate = this.#requireTaskForWrite(taskId);
706
- const stored = identified(role, CURRENT_TASK_ROLE_SCHEMA_VERSION, "name", role.name, "Task Role");
707
- if (stored.taskId !== taskId)
708
- throw new StorageRecordError(`Task Role belongs to another Task: ${stored.taskId}`);
709
- validateTaskRole(stored);
710
- const sessions = this.getRoleSessionSet(taskId, stored.name);
711
- if (sessions !== null)
712
- assertSessionsMatchRole(sessions, stored);
713
- this.#mutate((state) => { state.tasks[aggregate.task.id].roles[stored.name] = stored; });
714
- }
715
- listRoles(taskId) { return values(this.#requireTask(taskId).roles, "name"); }
716
- getRole(taskId, name) { return optional(this.#state().tasks[taskId]?.roles[name]); }
717
- saveTaskRoleWithSessionSet(role, sessions) {
718
- const storedRole = identified(role, CURRENT_TASK_ROLE_SCHEMA_VERSION, "name", role.name, "Task Role");
719
- validateTaskRole(storedRole);
720
- const storedSessions = taskSessions(sessions);
721
- assertSessionsMatchRole(storedSessions, storedRole);
722
- this.transaction(() => {
723
- this.#requireTaskForWrite(storedRole.taskId);
724
- this.#mutate((state) => {
725
- state.tasks[storedRole.taskId].roles[storedRole.name] = storedRole;
726
- state.tasks[storedRole.taskId].roleSessionSets[storedRole.name] = storedSessions;
727
- });
728
- });
729
- }
730
- removeTaskRole(taskId, name) {
731
- return this.transaction(() => {
732
- const aggregate = this.#requireTask(taskId);
733
- const removed = this.#remove(() => aggregate.roles, name);
734
- this.#mutate((state) => {
735
- delete aggregate.roleSessionSets[name];
736
- delete aggregate.activeRuns[name];
737
- delete state.mailboxes[mailboxTargetKey({ kind: "role", taskId, roleName: name })];
738
- });
739
- return removed;
740
- });
741
- }
742
- saveManagedWorkspace(workspace) {
743
- const stored = versioned(workspace, CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION, "Managed workspace");
744
- validateManagedWorkspace(stored);
745
- const taskId = stored.owner.taskId;
746
- const aggregate = this.#requireTaskForWrite(taskId);
747
- if (stored.owner.type === "work-item"
748
- && aggregate.workItems[stored.owner.workItemId] === undefined) {
749
- throw new StorageRecordError(`Managed workspace WorkItem not found: ${taskId}/${stored.owner.workItemId}.`);
750
- }
751
- if (stored.owner.type === "review-round"
752
- && aggregate.reviewRounds[stored.owner.reviewRoundId] === undefined) {
753
- throw new StorageRecordError(`Managed workspace ReviewRound not found: ${taskId}/${stored.owner.reviewRoundId}.`);
754
- }
755
- if (stored.owner.type === "integration-attempt"
756
- && aggregate.integrationAttempts[stored.owner.integrationAttemptId] === undefined) {
757
- throw new StorageRecordError(`Managed workspace Integration Attempt not found: ${taskId}/${stored.owner.integrationAttemptId}.`);
758
- }
759
- if (stored.owner.type === "execution-lane") {
760
- const groupOwner = stored.owner;
761
- if (groupOwner.purpose === "execution") {
762
- const item = groupOwner.workItemId === undefined
763
- ? undefined
764
- : aggregate.workItems[groupOwner.workItemId];
765
- if (item === undefined
766
- || workItemExecutionGroupById(item, groupOwner.executionGroupId) === undefined
767
- || !workItemExecutionGroupById(item, groupOwner.executionGroupId).lanes.some(({ id }) => id === groupOwner.executionLaneId)) {
768
- throw new StorageRecordError(`Managed execution Lane WorkItem lineage is invalid: ${taskId}/${groupOwner.executionGroupId}/${groupOwner.executionLaneId}.`);
769
- }
770
- }
771
- else {
772
- const round = groupOwner.reviewRoundId === undefined
773
- ? undefined
774
- : aggregate.reviewRounds[groupOwner.reviewRoundId];
775
- if (round === undefined || round.executionGroup?.id !== groupOwner.executionGroupId
776
- || !round.executionGroup.lanes.some(({ id }) => id === groupOwner.executionLaneId)) {
777
- throw new StorageRecordError(`Managed review Lane ReviewRound lineage is invalid: ${taskId}/${groupOwner.executionGroupId}/${groupOwner.executionLaneId}.`);
778
- }
779
- }
780
- }
781
- assertManagedWorkspaceReferences(aggregate, stored, "Managed workspace");
782
- const boundProjects = new Set(aggregate.task.projectBindings.map(({ projectId }) => projectId));
783
- if (stored.entries.some(({ projectId }) => !boundProjects.has(projectId))) {
784
- throw new StorageRecordError(`Managed workspace Project does not match Task: ${taskId}.`);
785
- }
786
- this.#mutate((state) => {
787
- state.tasks[taskId].managedWorkspaces[managedWorkspaceKey(stored.owner)] = stored;
788
- });
789
- }
790
- listManagedWorkspaces(taskId) {
791
- return values(this.#requireTask(taskId).managedWorkspaces, (workspace) => managedWorkspaceKey(workspace.owner));
792
- }
793
- listManagedWorkspace(taskId) {
794
- return this.listManagedWorkspaces(taskId);
795
- }
796
- getTaskWorkspace(taskId) {
797
- return this.getManagedWorkspace({ type: "task", taskId });
798
- }
799
- getWorkItemWorkspace(taskId, workItemId) {
800
- return this.getManagedWorkspace({ type: "work-item", taskId, workItemId });
801
- }
802
- getReviewRoundWorkspace(taskId, reviewRoundId) {
803
- return this.getManagedWorkspace({ type: "review-round", taskId, reviewRoundId });
804
- }
805
- getIntegrationWorkspace(taskId, integrationAttemptId) {
806
- return this.getManagedWorkspace({
807
- type: "integration-attempt",
808
- taskId,
809
- integrationAttemptId
810
- });
811
- }
812
- getManagedWorkspace(owner) {
813
- return optional(this.#state().tasks[owner.taskId]?.managedWorkspaces[managedWorkspaceKey(owner)]);
814
- }
815
- removeManagedWorkspace(owner) {
816
- this.#requireTaskForWrite(owner.taskId);
817
- return this.#remove((state) => state.tasks[owner.taskId].managedWorkspaces, managedWorkspaceKey(owner));
818
- }
819
- getRoleSessionSet(taskId, roleName) {
820
- return optional(this.#state().tasks[taskId]?.roleSessionSets[roleName]);
821
- }
822
- getTaskRoleSessionSet(taskId, roleName) {
823
- return this.getRoleSessionSet(taskId, roleName);
824
- }
825
- listRoleSessionSets(taskId) {
826
- return values(this.#requireTask(taskId).roleSessionSets, (set) => set.owner.roleName);
827
- }
828
- listRuntimeSessionCandidates(query = {}) {
829
- const state = this.#state();
830
- const selectedTaskIds = query.taskIds === undefined
831
- ? undefined
832
- : [...new Set(query.taskIds)].sort(numericCompare);
833
- const taskAggregates = query.scope === "global"
834
- ? []
835
- : selectedTaskIds === undefined
836
- ? Object.values(state.tasks)
837
- : selectedTaskIds.flatMap((taskId) => {
838
- const aggregate = state.tasks[taskId];
839
- return aggregate === undefined ? [] : [aggregate];
840
- });
841
- const candidates = [
842
- ...taskAggregates.flatMap((task) => (Object.values(task.roleSessionSets).flatMap((sessions) => {
843
- const candidate = projectRuntimeSessionCandidate(sessions);
844
- return candidate === null ? [] : [candidate];
845
- }))),
846
- ...(query.scope === "task" || selectedTaskIds !== undefined
847
- ? []
848
- : Object.values(state.globalRoleSessionSets).flatMap((sessions) => {
849
- const candidate = projectRuntimeSessionCandidate(sessions);
850
- return candidate === null ? [] : [candidate];
851
- }))
852
- ];
853
- return candidates
854
- .filter((candidate) => !query.cleanupRequiredOnly || candidate.cleanupRequired)
855
- .sort(compareRuntimeSessionCandidates);
856
- }
857
- saveRoleSessionSet(sessions) {
858
- const stored = taskSessions(sessions);
859
- const taskId = stored.owner.taskId;
860
- this.#requireTaskForWrite(taskId);
861
- const role = this.getRole(taskId, stored.owner.roleName);
862
- if (role === null)
863
- throw new StorageRecordError(`Task Role not found: ${taskId}/${stored.owner.roleName}`);
864
- assertSessionsMatchRole(stored, role);
865
- this.#mutate((state) => { state.tasks[taskId].roleSessionSets[stored.owner.roleName] = stored; });
866
- }
867
- saveTaskRoleSessionSet(sessions) { this.saveRoleSessionSet(sessions); }
868
- getRoleSession(taskId, roleName) {
869
- const set = this.getRoleSessionSet(taskId, roleName);
870
- return set === null ? null : optional(set.sessions[set.activeAgentId]);
871
- }
872
- getJobCallerKeyHash(taskId, roleName, agentId) {
873
- const hashes = this.#state().tasks[taskId]?.jobCallerKeyHashes;
874
- if (hashes === undefined)
875
- return null;
876
- return optional(hashes[jobCallerKeyHashKey(roleName, agentId)]);
877
- }
878
- setJobCallerKeyHash(taskId, roleName, agentId, hash) {
879
- this.#requireTaskForWrite(taskId);
880
- if (!/^[a-f0-9]{64}$/u.test(hash)) {
881
- throw new StorageRecordError(`Job caller key hash is invalid: ${taskId}/${roleName}.`);
882
- }
883
- this.#mutate((state) => {
884
- state.tasks[taskId].jobCallerKeyHashes[jobCallerKeyHashKey(roleName, agentId)] = hash;
885
- });
886
- }
887
- saveSessionOwner(identity) {
888
- this.#sessionOwners().record(identity);
889
- }
890
- getSessionOwner(launchId) {
891
- return this.#sessionOwners().get(launchId);
892
- }
893
- listSessionOwners() {
894
- return this.#sessionOwners().list();
895
- }
896
- listSessionOwnersForOwner(owner) {
897
- return this.#sessionOwners().listForOwner(owner);
898
- }
899
- removeSessionOwner(launchId) {
900
- this.#sessionOwners().remove(launchId);
901
- }
902
- #sessionOwners() {
903
- if (this.#sessionOwnerRegistry === undefined) {
904
- this.#sessionOwnerRegistry = new FileSessionOwnerRegistry(this.rootDir);
905
- }
906
- return this.#sessionOwnerRegistry;
907
- }
908
- nextWorkItemId(taskId) {
909
- return this.#nextTaskRecordId(taskId, "workItem");
910
- }
911
- getWorkItem(taskId, id) { return optional(this.#state().tasks[taskId]?.workItems[id]); }
912
- listWorkItems(taskId) { return values(this.#requireTask(taskId).workItems, "id"); }
913
- saveWorkItem(taskId, item) {
914
- const stored = identified(item, CURRENT_WORK_ITEM_SCHEMA_VERSION, "id", item.id, "Work item");
915
- if (stored.taskId !== taskId)
916
- throw new StorageRecordError(`Work item belongs to another Task: ${stored.taskId}`);
917
- validateWorkItem(stored);
918
- const aggregate = this.#requireTaskForWrite(taskId);
919
- const boundProjects = new Set(aggregate.task.projectBindings.map(({ projectId }) => projectId));
920
- if (stored.writeProjectIds.some((projectId) => !boundProjects.has(projectId))) {
921
- throw new StorageRecordError(`Work Item writable Project does not belong to Task: ${stored.id}.`);
922
- }
923
- const writableProjects = new Set(stored.writeProjectIds);
924
- if (stored.baseRefs?.some(({ projectId }) => !boundProjects.has(projectId))) {
925
- throw new StorageRecordError(`Work Item base-ref Project does not belong to Task: ${stored.id}.`);
926
- }
927
- if (stored.baseRefs?.some(({ projectId }) => !writableProjects.has(projectId))) {
928
- throw new StorageRecordError(`Work Item base-ref Project must be writable: ${stored.id}.`);
929
- }
930
- for (const dependencyId of stored.dependsOn) {
931
- const dependency = this.getWorkItem(taskId, dependencyId);
932
- if (dependency === null) {
933
- throw new StorageRecordError(`Work Item dependency not found: ${dependencyId}.`);
934
- }
935
- }
936
- assertAcyclicWorkItems({
937
- ...aggregate.workItems,
938
- [stored.id]: stored
939
- });
940
- for (const candidate of stored.candidates) {
941
- assertWorkItemCandidateReferences(aggregate, stored, candidate, "Work Item candidate");
942
- }
943
- const existing = this.getWorkItem(taskId, stored.id);
944
- if (existing !== null && !validWorkItemTransition(existing, stored)) {
945
- throw new StorageRecordError(`Work Item transition is invalid: ${stored.id}.`);
946
- }
947
- this.#mutate((state) => {
948
- const task = state.tasks[taskId];
949
- observeTaskRecordId(task, "workItem", stored.id);
950
- task.workItems[stored.id] = stored;
951
- });
952
- }
953
- nextContextSnapshotId(taskId) {
954
- return this.#nextTaskRecordId(taskId, "contextSnapshot");
955
- }
956
- getContextSnapshot(taskId, id) {
957
- return optional(this.#state().tasks[taskId]?.contextSnapshots[id]);
958
- }
959
- listContextSnapshots(taskId) {
960
- return values(this.#requireTask(taskId).contextSnapshots, "id");
961
- }
962
- saveContextSnapshot(snapshot) {
963
- const stored = identified(snapshot, CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION, "id", snapshot.id, "Context Snapshot");
964
- validateContextSnapshot(stored);
965
- const aggregate = this.#requireTaskForWrite(stored.taskId);
966
- const existing = aggregate.contextSnapshots[stored.id];
967
- if (existing !== undefined) {
968
- if (!isDeepStrictEqual(existing, stored)) {
969
- throw new StorageRecordError(`Context Snapshot is immutable: ${stored.id}.`);
970
- }
971
- return;
972
- }
973
- const duplicateSequence = Object.values(aggregate.contextSnapshots).find((candidate) => (candidate.scope === stored.scope
974
- && candidate.scopeRef === stored.scopeRef
975
- && candidate.sequence === stored.sequence));
976
- if (duplicateSequence !== undefined) {
977
- throw new StorageRecordError(`Context Snapshot sequence already exists: ${stored.taskId}/${stored.scope}/${stored.sequence}.`);
978
- }
979
- this.#mutate((state) => {
980
- const task = state.tasks[stored.taskId];
981
- observeTaskRecordId(task, "contextSnapshot", stored.id);
982
- task.contextSnapshots[stored.id] = stored;
983
- });
984
- }
985
- nextAgentRunId(taskId) {
986
- return this.#nextTaskRecordId(taskId, "agentRun");
987
- }
988
- peekNextAgentRunId(taskId) {
989
- return this.#peekTaskRecordId(taskId, "agentRun");
990
- }
991
- getAgentRun(taskId, id) { return optional(this.#state().tasks[taskId]?.agentRuns[id]); }
992
- listAgentRuns(taskId) { return values(this.#requireTask(taskId).agentRuns, "id"); }
993
- saveAgentRun(run) {
994
- const stored = identified(run, CURRENT_AGENT_RUN_SCHEMA_VERSION, "id", run.id, "Agent run");
995
- validateAgentRun(stored);
996
- const aggregate = this.#requireTaskForWrite(stored.taskId);
997
- if (stored.purpose === "review"
998
- && aggregate.task.projectBindings.length > 0
999
- && stored.workspace === undefined) {
1000
- throw new StorageRecordError(`A Project-backed review Agent run requires its ReviewRound workspace: ${stored.id}.`);
1001
- }
1002
- if (stored.reviewRoundId !== undefined) {
1003
- const round = aggregate.reviewRounds[stored.reviewRoundId];
1004
- if (round === undefined) {
1005
- throw new StorageRecordError(`Agent run ReviewRound not found: ${stored.reviewRoundId}.`);
1006
- }
1007
- const roundWorkItemId = round.workItemId ?? round.legacyAnchor?.workItemId;
1008
- const laneRole = round.executionGroup?.lanes.find(({ id }) => id === stored.executionLaneId)?.roleName;
1009
- if (roundWorkItemId !== stored.workItemId
1010
- || (round.reviewerRoleName !== stored.roleName && laneRole !== stored.roleName)) {
1011
- throw new StorageRecordError(`Agent run does not match ReviewRound: ${stored.id}.`);
1012
- }
1013
- }
1014
- assertAgentRunExecutionReferences(aggregate, stored);
1015
- this.#mutate((state) => {
1016
- const task = state.tasks[stored.taskId];
1017
- observeTaskRecordId(task, "agentRun", stored.id);
1018
- task.agentRuns[stored.id] = stored;
1019
- });
1020
- }
1021
- nextReviewRoundId(taskId) {
1022
- return this.#nextTaskRecordId(taskId, "reviewRound");
1023
- }
1024
- getReviewRound(taskId, reviewRoundId) {
1025
- return optional(this.#state().tasks[taskId]?.reviewRounds[reviewRoundId]);
1026
- }
1027
- listReviewRounds(taskId) {
1028
- return values(this.#requireTask(taskId).reviewRounds, "id");
1029
- }
1030
- saveReviewRound(taskId, round) {
1031
- const stored = identified(round, CURRENT_REVIEW_ROUND_SCHEMA_VERSION, "id", round.id, "ReviewRound");
1032
- validateReviewRound(stored);
1033
- if (stored.taskId !== taskId) {
1034
- throw new StorageRecordError(`ReviewRound belongs to another Task: ${stored.taskId}.`);
1035
- }
1036
- const aggregate = this.#requireTaskForWrite(taskId);
1037
- if ((stored.scope ?? "work-item") === "work-item") {
1038
- const item = aggregate.workItems[stored.workItemId];
1039
- if (item === undefined) {
1040
- throw new StorageRecordError(`ReviewRound Work Item not found: ${stored.workItemId}.`);
1041
- }
1042
- const candidate = item.candidates.find(({ id }) => id === stored.candidateId);
1043
- if (candidate === undefined) {
1044
- throw new StorageRecordError(`ReviewRound Candidate not found: ${stored.candidateId}.`);
1045
- }
1046
- assertWorkItemCandidateReferences(aggregate, item, candidate, `ReviewRound candidate ${stored.id}`);
1047
- }
1048
- if (stored.reviewerRunId !== undefined) {
1049
- const reviewerRun = aggregate.agentRuns[stored.reviewerRunId];
1050
- if (reviewerRun !== undefined
1051
- && (reviewerRun.reviewRoundId !== stored.id || reviewerRun.purpose !== "review")) {
1052
- throw new StorageRecordError(`ReviewRound Reviewer Run is invalid: ${stored.reviewerRunId}.`);
1053
- }
1054
- }
1055
- const existing = aggregate.reviewRounds[stored.id];
1056
- if (existing !== undefined && !validReviewRoundTransition(existing, stored)) {
1057
- throw new StorageRecordError(`ReviewRound transition is invalid: ${stored.id}.`);
1058
- }
1059
- this.#mutate((state) => {
1060
- const task = state.tasks[taskId];
1061
- observeTaskRecordId(task, "reviewRound", stored.id);
1062
- task.reviewRounds[stored.id] = stored;
1063
- });
1064
- }
1065
- nextReviewFindingId(taskId) {
1066
- throw new StorageRecordError(`Review findings require the SQLite backend (yui.db); migrate this Home with \`yui update\` before using the finding ledger on Task ${taskId}.`);
1067
- }
1068
- getReviewFinding(taskId, reviewFindingId) {
1069
- throw new StorageRecordError(`Review findings require the SQLite backend (yui.db); migrate this Home with \`yui update\` before using the finding ledger on Task ${taskId}.`);
1070
- }
1071
- listReviewFindings(taskId) {
1072
- throw new StorageRecordError(`Review findings require the SQLite backend (yui.db); migrate this Home with \`yui update\` before using the finding ledger on Task ${taskId}.`);
1073
- }
1074
- saveReviewFinding(taskId, finding) {
1075
- throw new StorageRecordError(`Review findings require the SQLite backend (yui.db); migrate this Home with \`yui update\` before using the finding ledger on Task ${taskId}.`);
1076
- }
1077
- getActiveAgentRun(taskId, roleName) {
1078
- const aggregate = this.#state().tasks[taskId];
1079
- const pointer = aggregate?.activeRuns[roleName];
1080
- if (aggregate === undefined || pointer === undefined)
1081
- return null;
1082
- const run = aggregate.agentRuns[pointer.runId];
1083
- if (run === undefined)
1084
- throw new StorageRecordError(`Active Agent run pointer is dangling: ${taskId}/${roleName}`);
1085
- return clone(run);
1086
- }
1087
- saveActiveAgentRun(run) {
1088
- if (run.executionGroupId !== undefined && run.executionLaneId !== undefined) {
1089
- this.saveActiveExecutionLaneRun(run);
1090
- return;
1091
- }
1092
- if (run.status !== "active")
1093
- throw new StorageRecordError(`Active Agent run must have active status: ${run.id}`);
1094
- this.transaction((store) => {
1095
- const task = store.getTask(run.taskId);
1096
- if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
1097
- throw new StorageRecordError(`Task execution is not enabled: ${run.taskId}.`);
1098
- }
1099
- const current = store.getActiveAgentRun(run.taskId, run.roleName);
1100
- if (current !== null && current.id !== run.id) {
1101
- throw new StorageRecordError(`Role already has an active Agent run: ${run.taskId}/${run.roleName}`);
1102
- }
1103
- const sessions = store.getTaskRoleSessionSet(run.taskId, run.roleName);
1104
- if (sessions !== null && sessions.inFlight !== null && sessions.inFlight.runId !== run.id) {
1105
- throw new StorageRecordError(`Role still has an in-flight Turn: ${run.taskId}/${run.roleName}/${sessions.inFlight.runId}`);
1106
- }
1107
- store.saveAgentRun(run);
1108
- this.#mutate((state) => {
1109
- state.tasks[run.taskId].activeRuns[run.roleName] = {
1110
- schemaVersion: CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION,
1111
- runId: run.id
1112
- };
1113
- });
1114
- });
1115
- }
1116
- clearActiveAgentRun(taskId, roleName) {
1117
- this.#mutate((state) => {
1118
- const task = state.tasks[taskId];
1119
- if (task === undefined)
1120
- return;
1121
- const rolePointer = task.activeRuns[roleName];
1122
- delete task.activeRuns[roleName];
1123
- // Older Controller paths only know the Role key. When that key points
1124
- // at a lane-backed Run, remove the matching lane pointer too; preserve
1125
- // every other lane for the same Role in a multi-lane group.
1126
- if (rolePointer === undefined)
1127
- return;
1128
- for (const [key, pointer] of Object.entries(task.activeRuns)) {
1129
- if (executionLaneActiveRunKeyParts(key) !== null && pointer.runId === rolePointer.runId) {
1130
- delete task.activeRuns[key];
1131
- }
1132
- }
1133
- });
1134
- }
1135
- getActiveExecutionLaneRun(taskId, executionGroupId, executionLaneId) {
1136
- const aggregate = this.#state().tasks[taskId];
1137
- const key = executionLaneActiveRunKey(executionGroupId, executionLaneId);
1138
- const pointer = aggregate?.activeRuns[key];
1139
- if (aggregate === undefined || pointer === undefined)
1140
- return null;
1141
- const run = aggregate.agentRuns[pointer.runId];
1142
- if (run === undefined) {
1143
- throw new StorageRecordError(`Active Agent run pointer is dangling: ${taskId}/${key}`);
1144
- }
1145
- if (run.executionGroupId !== executionGroupId
1146
- || run.executionLaneId !== executionLaneId
1147
- || run.status !== "active") {
1148
- throw new StorageRecordError(`Active Agent run pointer is invalid: ${taskId}/${key}`);
1149
- }
1150
- return clone(run);
1151
- }
1152
- saveActiveExecutionLaneRun(run) {
1153
- if (run.status !== "active") {
1154
- throw new StorageRecordError(`Active Agent run must have active status: ${run.id}`);
1155
- }
1156
- if (run.executionGroupId === undefined || run.executionLaneId === undefined) {
1157
- throw new StorageRecordError(`Lane active Agent run requires execution lineage: ${run.id}`);
1158
- }
1159
- this.transaction((store) => {
1160
- const task = store.getTask(run.taskId);
1161
- if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
1162
- throw new StorageRecordError(`Task execution is not enabled: ${run.taskId}.`);
1163
- }
1164
- const key = executionLaneActiveRunKey(run.executionGroupId, run.executionLaneId);
1165
- const current = store.getActiveExecutionLaneRun(run.taskId, run.executionGroupId, run.executionLaneId);
1166
- if (current !== null && current.id !== run.id) {
1167
- throw new StorageRecordError(`Execution Lane already has an active Agent run: ${run.taskId}/${key}`);
1168
- }
1169
- const sessions = store.getTaskRoleSessionSet(run.taskId, run.roleName);
1170
- if (sessions !== null && sessions.inFlight !== null && sessions.inFlight.runId !== run.id) {
1171
- throw new StorageRecordError(`Role still has an in-flight Turn: ${run.taskId}/${run.roleName}/${sessions.inFlight.runId}`);
1172
- }
1173
- store.saveAgentRun(run);
1174
- this.#mutate((state) => {
1175
- const task = state.tasks[run.taskId];
1176
- task.activeRuns[key] = {
1177
- schemaVersion: CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION,
1178
- runId: run.id
1179
- };
1180
- // Preserve the legacy role pointer for the single-lane delivery path.
1181
- // A second lane for the same Role keeps the first pointer unchanged;
1182
- // lane-aware readers use the exact key above.
1183
- if (task.activeRuns[run.roleName] === undefined) {
1184
- task.activeRuns[run.roleName] = {
1185
- schemaVersion: CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION,
1186
- runId: run.id
1187
- };
1188
- }
1189
- });
1190
- });
1191
- }
1192
- clearActiveExecutionLaneRun(taskId, executionGroupId, executionLaneId) {
1193
- this.#mutate((state) => {
1194
- const task = state.tasks[taskId];
1195
- if (task === undefined)
1196
- return;
1197
- const key = executionLaneActiveRunKey(executionGroupId, executionLaneId);
1198
- const pointer = task.activeRuns[key];
1199
- delete task.activeRuns[key];
1200
- const runId = pointer?.runId;
1201
- for (const [roleName, rolePointer] of Object.entries(task.activeRuns)) {
1202
- if (executionLaneActiveRunKeyParts(roleName) !== null)
1203
- continue;
1204
- const roleRun = task.agentRuns[rolePointer.runId];
1205
- const matches = runId !== undefined
1206
- ? rolePointer.runId === runId
1207
- : roleRun?.executionGroupId === executionGroupId
1208
- && roleRun.executionLaneId === executionLaneId;
1209
- if (matches) {
1210
- delete task.activeRuns[roleName];
1211
- }
1212
- }
1213
- });
1214
- }
1215
- nextMessageId(taskId) {
1216
- return this.#nextTaskRecordId(taskId, "message");
1217
- }
1218
- saveMessage(taskId, message) {
1219
- validateTaskMessage(message);
1220
- if (message.taskId !== taskId) {
1221
- throw new StorageRecordError(`Message belongs to another Task: ${message.taskId}`);
1222
- }
1223
- this.#saveTaskRecord(taskId, "messages", message, "Message");
1224
- }
1225
- listMessages(taskId) { return values(this.#requireTask(taskId).messages, "id"); }
1226
- nextInputRequestId(taskId) {
1227
- return this.#nextTaskRecordId(taskId, "inputRequest");
1228
- }
1229
- saveInputRequest(taskId, request) {
1230
- const stored = validateInputRequest(versioned(request, CURRENT_INPUT_REQUEST_SCHEMA_VERSION, "Input request"));
1231
- if (stored.taskId !== taskId) {
1232
- throw new StorageRecordError(`Input request belongs to another Task: ${stored.taskId}`);
1233
- }
1234
- this.#mutate((state) => {
1235
- const aggregate = requireTaskFromState(state, taskId);
1236
- const existing = aggregate.inputRequests[stored.id];
1237
- if (existing === undefined && stored.status !== "open") {
1238
- throw new StorageRecordError(`Input request must start open: ${stored.id}`);
1239
- }
1240
- if (existing !== undefined && !isValidInputRequestTransition(existing, stored)) {
1241
- throw new StorageRecordError(`Input request cannot be overwritten: ${stored.id}`);
1242
- }
1243
- observeTaskRecordId(aggregate, "inputRequest", stored.id);
1244
- aggregate.inputRequests[stored.id] = stored;
1245
- });
1246
- }
1247
- getInputRequest(taskId, requestId) {
1248
- return optional(this.#state().tasks[taskId]?.inputRequests[requestId]);
1249
- }
1250
- listInputRequests(taskId) {
1251
- return values(this.#requireTask(taskId).inputRequests, "id");
1252
- }
1253
- listOpenInputRequests(taskIds) {
1254
- const state = this.#state();
1255
- const selected = taskIds === undefined
1256
- ? Object.values(state.tasks)
1257
- : [...new Set(taskIds)].sort(numericCompare).flatMap((taskId) => {
1258
- const aggregate = state.tasks[taskId];
1259
- return aggregate === undefined ? [] : [aggregate];
1260
- });
1261
- return selected
1262
- .flatMap((aggregate) => Object.values(aggregate.inputRequests))
1263
- .filter((request) => request.status === "open")
1264
- .map(clone)
1265
- .sort((left, right) => (numericCompare(left.taskId, right.taskId)
1266
- || numericCompare(left.id, right.id)));
1267
- }
1268
- listAllInputRequests() {
1269
- return Object.values(this.#state().tasks)
1270
- .flatMap((aggregate) => Object.values(aggregate.inputRequests).map(clone))
1271
- .sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
1272
- || left.taskId.localeCompare(right.taskId)
1273
- || numericCompare(left.id, right.id)));
1274
- }
1275
- nextDecisionId(taskId) {
1276
- return this.#nextTaskRecordId(taskId, "decision");
1277
- }
1278
- saveDecision(taskId, decision) {
1279
- const stored = storedDecision(decision);
1280
- if (stored.taskId !== taskId) {
1281
- throw new StorageRecordError(`Decision belongs to another Task: ${stored.taskId}`);
1282
- }
1283
- this.#mutate((state) => {
1284
- const aggregate = requireTaskFromState(state, taskId);
1285
- const existing = aggregate.decisions[stored.id];
1286
- if (existing === undefined && stored.status !== "active") {
1287
- throw new StorageRecordError(`Decision must start active: ${stored.id}`);
1288
- }
1289
- if (existing !== undefined && !isValidDecisionSupersession(existing, stored)) {
1290
- throw new StorageRecordError(`Decision cannot be overwritten: ${stored.id}`);
1291
- }
1292
- observeTaskRecordId(aggregate, "decision", stored.id);
1293
- aggregate.decisions[stored.id] = stored;
1294
- });
1295
- }
1296
- listDecisions(taskId) {
1297
- return values(this.#requireTask(taskId).decisions, "id");
1298
- }
1299
- getDecision(taskId, decisionId) {
1300
- return this.#requireTask(taskId).decisions[decisionId] ?? null;
1301
- }
1302
- nextMilestoneId(taskId) {
1303
- return this.#nextTaskRecordId(taskId, "milestone");
1304
- }
1305
- saveMilestone(taskId, milestone) {
1306
- const stored = storedMilestone(milestone);
1307
- if (stored.taskId !== taskId) {
1308
- throw new StorageRecordError(`Milestone belongs to another Task: ${stored.taskId}`);
1309
- }
1310
- this.#mutate((state) => {
1311
- const aggregate = requireTaskFromState(state, taskId);
1312
- if (aggregate.milestones[stored.id] !== undefined) {
1313
- throw new StorageRecordError(`Milestone already exists: ${taskId}/${stored.id}`);
1314
- }
1315
- observeTaskRecordId(aggregate, "milestone", stored.id);
1316
- aggregate.milestones[stored.id] = stored;
1317
- });
1318
- }
1319
- listMilestones(taskId) {
1320
- return values(this.#requireTask(taskId).milestones, "id");
1321
- }
1322
- getMilestone(taskId, milestoneId) {
1323
- return this.#requireTask(taskId).milestones[milestoneId] ?? null;
1324
- }
1325
- nextEventId(taskId) {
1326
- return this.#nextTaskRecordId(taskId, "event");
1327
- }
1328
- saveEvent(taskId, event) {
1329
- const stored = storedTaskEvent(event);
1330
- if (stored.taskId !== taskId) {
1331
- throw new StorageRecordError(`Task event belongs to another Task: ${stored.taskId}`);
1332
- }
1333
- this.#mutate((state) => {
1334
- const aggregate = requireTaskFromState(state, taskId);
1335
- if (aggregate.events[stored.id] !== undefined) {
1336
- throw new StorageRecordError(`Task event already exists: ${taskId}/${stored.id}`);
1337
- }
1338
- observeTaskRecordId(aggregate, "event", stored.id);
1339
- aggregate.events[stored.id] = stored;
1340
- });
1341
- }
1342
- listEvents(taskId) { return values(this.#requireTask(taskId).events, "id"); }
1343
- nextTaskWakeId(taskId) {
1344
- return this.#nextTaskRecordId(taskId, "taskWake");
1345
- }
1346
- peekNextTaskWakeId(taskId) {
1347
- return this.#peekTaskRecordId(taskId, "taskWake");
1348
- }
1349
- saveTaskWake(taskId, wake) {
1350
- if (wake.taskId !== taskId) {
1351
- throw new StorageRecordError(`Task wake belongs to another Task: ${wake.taskId}`);
1352
- }
1353
- this.#mutate((state) => {
1354
- const aggregate = requireTaskFromState(state, taskId);
1355
- observeTaskRecordId(aggregate, "taskWake", wake.id);
1356
- aggregate.wakes[wake.id] = wake;
1357
- });
1358
- }
1359
- getTaskWake(taskId, wakeId) {
1360
- return this.#state().tasks[taskId]?.wakes[wakeId] ?? null;
1361
- }
1362
- listTaskWakes(taskId) {
1363
- return values(this.#requireTask(taskId).wakes, "id");
1364
- }
1365
- removeEvents(taskId, eventIds) {
1366
- if (eventIds.length === 0)
1367
- return 0;
1368
- let removed = 0;
1369
- this.#mutate((state) => {
1370
- const aggregate = requireTaskFromState(state, taskId);
1371
- for (const id of eventIds) {
1372
- if (aggregate.events[id] !== undefined) {
1373
- delete aggregate.events[id];
1374
- removed++;
1375
- }
1376
- }
1377
- });
1378
- return removed;
1379
- }
1380
- nextCapabilityGrantId(taskId) {
1381
- return this.#nextTaskRecordId(taskId, "capabilityGrant");
1382
- }
1383
- saveCapabilityGrant(taskId, grant) {
1384
- const stored = storedCapabilityGrant(grant);
1385
- if (stored.taskId !== taskId) {
1386
- throw new StorageRecordError(`Capability grant belongs to another Task: ${stored.taskId}`);
1387
- }
1388
- this.#mutate((state) => {
1389
- const aggregate = requireTaskFromState(state, taskId);
1390
- const existing = aggregate.capabilityGrants[stored.id];
1391
- if (existing === undefined) {
1392
- if (stored.revokedAt !== undefined) {
1393
- throw new StorageRecordError(`Capability grant must start unrevoked: ${stored.id}`);
1394
- }
1395
- if (stored.usesUsed !== 0) {
1396
- throw new StorageRecordError(`Capability grant must start unused: ${stored.id}`);
1397
- }
1398
- }
1399
- else if (!isValidCapabilityGrantTransition(existing, stored)) {
1400
- throw new StorageRecordError(`Capability grant cannot be overwritten: ${taskId}/${stored.id}`);
1401
- }
1402
- observeTaskRecordId(aggregate, "capabilityGrant", stored.id);
1403
- aggregate.capabilityGrants[stored.id] = stored;
1404
- });
1405
- }
1406
- listCapabilityGrants(taskId) {
1407
- return values(this.#requireTask(taskId).capabilityGrants, "id");
1408
- }
1409
- getCapabilityGrant(taskId, grantId) {
1410
- return this.#requireTask(taskId).capabilityGrants[grantId] ?? null;
1411
- }
1412
- nextReleaseWorkflowId(taskId) {
1413
- return this.#nextTaskRecordId(taskId, "releaseWorkflow");
1414
- }
1415
- saveReleaseWorkflow(taskId, workflow) {
1416
- const stored = storedReleaseWorkflow(workflow);
1417
- if (stored.taskId !== taskId) {
1418
- throw new StorageRecordError(`Release workflow belongs to another Task: ${stored.taskId}`);
1419
- }
1420
- this.#mutate((state) => {
1421
- const aggregate = requireTaskFromState(state, taskId);
1422
- const existing = aggregate.releaseWorkflows[stored.id];
1423
- if (existing !== undefined && !isValidReleaseWorkflowTransition(existing, stored)) {
1424
- throw new StorageRecordError(`Release workflow cannot be overwritten: ${taskId}/${stored.id}`);
1425
- }
1426
- observeTaskRecordId(aggregate, "releaseWorkflow", stored.id);
1427
- aggregate.releaseWorkflows[stored.id] = stored;
1428
- });
1429
- }
1430
- listReleaseWorkflows(taskId) {
1431
- return values(this.#requireTask(taskId).releaseWorkflows, "id");
1432
- }
1433
- getReleaseWorkflow(taskId, workflowId) {
1434
- return this.#requireTask(taskId).releaseWorkflows[workflowId] ?? null;
1435
- }
1436
- nextPublicationReferenceId(taskId) {
1437
- return this.#nextTaskRecordId(taskId, "publicationReference");
1438
- }
1439
- savePublicationReference(taskId, reference) {
1440
- const stored = storedPublicationReference(reference);
1441
- if (stored.taskId !== taskId) {
1442
- throw new StorageRecordError(`Publication reference belongs to another Task: ${stored.taskId}`);
1443
- }
1444
- this.#mutate((state) => {
1445
- const aggregate = requireTaskFromState(state, taskId);
1446
- const existing = aggregate.publicationReferences[stored.id];
1447
- if (existing !== undefined) {
1448
- if (!isDeepStrictEqual(existing, stored)) {
1449
- throw new StorageRecordError(`Publication reference cannot be overwritten: ${taskId}/${stored.id}`);
1450
- }
1451
- }
1452
- else {
1453
- const key = publicationExternalKey(stored);
1454
- const sameExternal = [];
1455
- for (const [otherTaskId, otherAggregate] of Object.entries(state.tasks)) {
1456
- for (const candidate of Object.values(otherAggregate.publicationReferences)) {
1457
- if (publicationExternalKey(candidate) === key) {
1458
- if (otherTaskId !== taskId) {
1459
- throw new StorageRecordError(`Publication external identity already belongs to Task ${otherTaskId}: ${key}.`);
1460
- }
1461
- sameExternal.push(candidate);
1462
- }
1463
- }
1464
- }
1465
- if (sameExternal.length > 0) {
1466
- const latest = latestPublicationReference(sameExternal);
1467
- if (stored.supersedes !== latest.id) {
1468
- throw new StorageRecordError(`Publication external identity conflicts with ${latest.id}; `
1469
- + `explicitly supersede that record: ${key}.`);
1470
- }
1471
- }
1472
- else if (stored.supersedes !== undefined) {
1473
- throw new StorageRecordError(`Publication supersedes target has a different external identity: ${stored.supersedes}.`);
1474
- }
1475
- observeTaskRecordId(aggregate, "publicationReference", stored.id);
1476
- }
1477
- aggregate.publicationReferences[stored.id] = stored;
1478
- });
1479
- }
1480
- listPublicationReferences(taskId) {
1481
- return values(this.#requireTask(taskId).publicationReferences, "id");
1482
- }
1483
- getPublicationReference(taskId, referenceId) {
1484
- return this.#requireTask(taskId).publicationReferences[referenceId] ?? null;
1485
- }
1486
- findPublicationReferenceByExternalKey(externalKey) {
1487
- const matches = Object.values(this.#state().tasks)
1488
- .flatMap((aggregate) => Object.values(aggregate.publicationReferences))
1489
- .filter((reference) => publicationExternalKey(reference) === externalKey);
1490
- return matches.length === 0 ? null : latestPublicationReference(matches);
1491
- }
1492
- // -- Gate artifacts (Issue 08) ---------------------------------------------
1493
- // FileTaskStore delegates to the file-backed artifact namespace under
1494
- // `<home>/artifacts/gates/`. This is the transitional path for Homes that
1495
- // have not yet migrated to SQLite; the SqliteTaskStore stores the same
1496
- // records and logs in `gate_artifacts` / `gate_artifact_logs`.
1497
- #gateArtifactRecordPath(projectId, key) {
1498
- return join(this.rootDir, "artifacts", "gates", projectId, `${key}.json`);
1499
- }
1500
- #gateArtifactLogsRoot(projectId, key) {
1501
- return join(this.rootDir, "artifacts", "gates", projectId, key);
1502
- }
1503
- saveGateArtifact(artifact, logs) {
1504
- validateGateArtifact(artifact);
1505
- const recordPath = this.#gateArtifactRecordPath(artifact.projectId, artifact.key);
1506
- const logsRoot = this.#gateArtifactLogsRoot(artifact.projectId, artifact.key);
1507
- this.#mutate(() => {
1508
- writeTextFileAtomically(recordPath, `${JSON.stringify(artifact, null, 2)}\n`);
1509
- mkdirSync(logsRoot, { recursive: true, mode: 0o700 });
1510
- for (const [stepName, content] of logs) {
1511
- const logPath = join(logsRoot, stepName);
1512
- const tmpPath = `${logPath}.tmp-${process.pid}`;
1513
- writeFileSync(tmpPath, content, { mode: 0o600 });
1514
- renameSync(tmpPath, logPath);
1515
- }
1516
- });
1517
- }
1518
- touchGateArtifact(artifact) {
1519
- validateGateArtifact(artifact);
1520
- const recordPath = this.#gateArtifactRecordPath(artifact.projectId, artifact.key);
1521
- if (!existsSync(recordPath)) {
1522
- throw new StorageRecordError(`Gate artifact not found for touch: ${artifact.key}`);
1523
- }
1524
- writeTextFileAtomically(recordPath, `${JSON.stringify(artifact, null, 2)}\n`);
1525
- }
1526
- getGateArtifact(projectId, key) {
1527
- const path = this.#gateArtifactRecordPath(projectId, key);
1528
- if (!existsSync(path))
1529
- return null;
1530
- const parsed = JSON.parse(readFileSync(path, "utf8"));
1531
- return validateGateArtifact(parsed);
1532
- }
1533
- findGateArtifactByIdentity(identity) {
1534
- return this.getGateArtifact(identity.projectId, gateArtifactKey(identity));
1535
- }
1536
- findL2GateArtifactsForCommit(query) {
1537
- const root = join(this.rootDir, "artifacts", "gates", query.projectId);
1538
- if (!existsSync(root))
1539
- return [];
1540
- const results = [];
1541
- for (const entry of readdirSync(root)) {
1542
- if (!entry.endsWith(".json"))
1543
- continue;
1544
- let artifact;
1545
- try {
1546
- artifact = this.getGateArtifact(query.projectId, entry.slice(0, -".json".length));
1547
- }
1548
- catch {
1549
- continue;
1550
- }
1551
- if (artifact === null
1552
- || artifact.level !== "L2"
1553
- || artifact.status !== "complete"
1554
- || artifact.outcome !== "succeeded"
1555
- || artifact.commit !== query.commit
1556
- || artifact.planDigest !== query.planDigest
1557
- || artifact.toolchainDigest !== query.toolchainDigest
1558
- || artifact.boundary?.targetRef !== query.targetRef) {
1559
- continue;
1560
- }
1561
- results.push(artifact);
1562
- }
1563
- return results;
1564
- }
1565
- getGateArtifactLogs(artifactKey) {
1566
- // The artifact key is content-addressed; we need the projectId to build
1567
- // the path. Callers that have the artifact should use
1568
- // getGateArtifactLogsForArtifact; this fallback scans all projects.
1569
- const gatesRoot = join(this.rootDir, "artifacts", "gates");
1570
- if (!existsSync(gatesRoot))
1571
- return new Map();
1572
- for (const projectId of readdirSync(gatesRoot)) {
1573
- const logsRoot = join(gatesRoot, projectId, artifactKey);
1574
- if (existsSync(logsRoot) && statSync(logsRoot).isDirectory()) {
1575
- return this.#readGateArtifactLogs(logsRoot);
1576
- }
1577
- }
1578
- return new Map();
1579
- }
1580
- #readGateArtifactLogs(logsRoot) {
1581
- const logs = new Map();
1582
- if (!existsSync(logsRoot))
1583
- return logs;
1584
- for (const entry of readdirSync(logsRoot)) {
1585
- const fullPath = join(logsRoot, entry);
1586
- if (statSync(fullPath).isFile()) {
1587
- logs.set(entry, readFileSync(fullPath));
1588
- }
1589
- }
1590
- return logs;
1591
- }
1592
- pruneGateArtifacts(projectId, options) {
1593
- const root = join(this.rootDir, "artifacts", "gates", projectId);
1594
- if (!existsSync(root))
1595
- return Object.freeze({ retained: 0, deleted: 0 });
1596
- let retained = 0;
1597
- let deleted = 0;
1598
- for (const entry of readdirSync(root)) {
1599
- if (!entry.endsWith(".json"))
1600
- continue;
1601
- const key = entry.slice(0, -".json".length);
1602
- let artifact;
1603
- try {
1604
- const loaded = this.getGateArtifact(projectId, key);
1605
- if (loaded === null)
1606
- continue;
1607
- artifact = loaded;
1608
- }
1609
- catch {
1610
- retained += 1;
1611
- continue;
1612
- }
1613
- const age = options.now.getTime() - Date.parse(artifact.lastUsedAt);
1614
- if (options.isReferenced(key) || age < options.ttlMs) {
1615
- retained += 1;
1616
- continue;
1617
- }
1618
- rmSync(this.#gateArtifactRecordPath(projectId, key), { force: true });
1619
- rmSync(this.#gateArtifactLogsRoot(projectId, key), { recursive: true, force: true });
1620
- deleted += 1;
1621
- }
1622
- return Object.freeze({ retained, deleted });
1623
- }
1624
- getWorkMailbox(target) {
1625
- return optional(this.#state().mailboxes[mailboxTargetKey(target)]);
1626
- }
1627
- listWorkMailboxes() {
1628
- return Object.entries(this.#state().mailboxes)
1629
- .sort(([left], [right]) => left.localeCompare(right))
1630
- .map(([, mailbox]) => clone(mailbox));
1631
- }
1632
- listReadyWorkMailboxes() {
1633
- // The file rollback backend has no secondary indexes. It filters the
1634
- // in-memory aggregate while preserving the indexed SQLite contract's
1635
- // target-key order; production layout 7 uses the bounded SQLite query.
1636
- return Object.entries(this.#state().mailboxes)
1637
- .filter(([, mailbox]) => mailboxHasWork(mailbox))
1638
- .sort(([left], [right]) => left.localeCompare(right))
1639
- .map(([, mailbox]) => clone(mailbox));
1640
- }
1641
- saveWorkMailbox(value) {
1642
- let mailbox;
1643
- try {
1644
- mailbox = validateWorkMailbox(versioned(value, CURRENT_WORK_MAILBOX_SCHEMA_VERSION, "WorkMailbox"));
1645
- }
1646
- catch (error) {
1647
- throw new StorageRecordError(error instanceof Error ? error.message : String(error));
1648
- }
1649
- this.#mutate((state) => {
1650
- validateMailboxReferences(state, mailbox);
1651
- state.mailboxes[mailboxTargetKey(mailbox.target)] = clone(mailbox);
1652
- });
1653
- }
1654
- removeWorkMailbox(target) {
1655
- return this.#remove((state) => state.mailboxes, mailboxTargetKey(target));
1656
- }
1657
- getPendingWakeup(taskId) {
1658
- return pendingWakeupProjection(this.getWorkMailbox({ kind: "role", taskId, roleName: "leader" }));
1659
- }
1660
- listPendingWakeups() {
1661
- return this.listReadyWorkMailboxes()
1662
- .flatMap((mailbox) => {
1663
- const wakeup = pendingWakeupProjection(mailbox);
1664
- return wakeup === null ? [] : [wakeup];
1665
- })
1666
- .sort((a, b) => numericCompare(a.taskId, b.taskId));
1667
- }
1668
- savePendingWakeup(value) {
1669
- const wakeup = identified(value, CURRENT_PENDING_WAKEUP_SCHEMA_VERSION, "taskId", value.taskId, "Pending wakeup");
1670
- const target = { kind: "role", taskId: wakeup.taskId, roleName: "leader" };
1671
- this.transaction(() => {
1672
- const existing = this.getWorkMailbox(target);
1673
- const existingPending = existing === null ? null : pendingLane(existing, "normal");
1674
- if (existingPending !== null
1675
- && wakeup.requestCount <= existingPending.requestCount) {
1676
- throw new StorageRecordError(`Pending wakeup is stale: ${wakeup.taskId}`);
1677
- }
1678
- const fromSequence = existingPending?.fromSequence ?? existing?.nextSequence ?? 1;
1679
- const toSequence = fromSequence + wakeup.requestCount - 1;
1680
- this.saveWorkMailbox({
1681
- schemaVersion: CURRENT_WORK_MAILBOX_SCHEMA_VERSION,
1682
- target,
1683
- nextSequence: Math.max(existing?.nextSequence ?? 1, toSequence + 1),
1684
- processing: existing?.processing ?? null,
1685
- inputDelivery: existing?.inputDelivery ?? null,
1686
- pending: {
1687
- normal: {
1688
- fromSequence,
1689
- toSequence,
1690
- reasons: [...wakeup.reasons],
1691
- refs: existingPending?.refs ?? [],
1692
- requestCount: wakeup.requestCount,
1693
- firstQueuedAt: wakeup.firstRequestedAt,
1694
- lastQueuedAt: wakeup.lastRequestedAt,
1695
- sources: existingPending?.sources ?? ["pending-wakeup-projection"],
1696
- dedupeKeys: existingPending?.dedupeKeys ?? [
1697
- `pending-wakeup:${wakeup.taskId}:${fromSequence}-${toSequence}`
1698
- ],
1699
- deliveryModes: existingPending?.deliveryModes ?? ["followup"]
1700
- },
1701
- userCorrection: existing?.pending.userCorrection ?? null,
1702
- cursors: existing?.pending.cursors ?? { normal: 0, userCorrection: 0 },
1703
- recentDedupeKeys: existing?.pending.recentDedupeKeys ?? []
1704
- }
1705
- });
1706
- });
1707
- }
1708
- clearPendingWakeup(taskId) {
1709
- const target = { kind: "role", taskId, roleName: "leader" };
1710
- const mailbox = this.getWorkMailbox(target);
1711
- if (mailbox === null || mailbox.pending.normal === null)
1712
- return;
1713
- this.saveWorkMailbox(consumePendingBatch(mailbox, "normal"));
1714
- }
1715
- getLeaderFailure(taskId) { return optional(this.#state().tasks[taskId]?.leaderFailure ?? undefined); }
1716
- saveLeaderFailure(value) {
1717
- const stored = identified(value, CURRENT_LEADER_FAILURE_SCHEMA_VERSION, "taskId", value.taskId, "Leader failure");
1718
- this.#requireTaskForWrite(value.taskId);
1719
- this.#mutate((state) => { state.tasks[value.taskId].leaderFailure = stored; });
1720
- }
1721
- clearLeaderFailure(taskId) {
1722
- this.#mutate((state) => {
1723
- if (state.tasks[taskId] !== undefined)
1724
- state.tasks[taskId].leaderFailure = null;
1725
- });
1726
- }
1727
- #saveTaskRecord(taskId, key, value, label) {
1728
- const record = versioned(value, CURRENT_MESSAGE_SCHEMA_VERSION, label);
1729
- this.#requireTaskForWrite(taskId);
1730
- this.#mutate((state) => {
1731
- if (state.tasks[taskId][key][record.id] !== undefined) {
1732
- throw new StorageRecordError(`${label} already exists: ${taskId}/${record.id}`);
1733
- }
1734
- observeTaskRecordId(state.tasks[taskId], "message", record.id);
1735
- state.tasks[taskId][key][record.id] = clone(value);
1736
- });
1737
- }
1738
- #requireTask(taskId) {
1739
- const aggregate = this.#state().tasks[taskId];
1740
- if (aggregate === undefined)
1741
- throw new StorageRecordError(`Task not found: ${taskId}`);
1742
- return aggregate;
1743
- }
1744
- #requireTaskForWrite(taskId) { return this.#requireTask(taskId); }
1745
- #state() { return this.#transaction?.state ?? this.#readCachedState(); }
1746
- #mutate(execute) { this.#mutateResult((state) => { execute(state); }); }
1747
- #mutateResult(execute) {
1748
- if (this.#transaction !== null) {
1749
- const result = execute(this.#transaction.state);
1750
- this.#transaction.dirty = true;
1751
- return result;
1752
- }
1753
- return this.#withWriteLock(() => {
1754
- const state = this.#readCachedState();
1755
- try {
1756
- const result = execute(state);
1757
- this.#commit(state, state.revision);
1758
- return result;
1759
- }
1760
- catch (error) {
1761
- this.#readCache = null;
1762
- throw error;
1763
- }
1764
- });
1765
- }
1766
- #remove(select, id) {
1767
- return this.#mutateResult((state) => {
1768
- const records = select(state);
1769
- if (records[id] === undefined)
1770
- return false;
1771
- delete records[id];
1772
- return true;
1773
- });
1774
- }
1775
- #nextGlobalId(prefix, collect) {
1776
- const pattern = new RegExp(`^${prefix}-(\\d+)$`);
1777
- const maximum = collect(this.#state()).reduce((max, id) => {
1778
- const match = pattern.exec(id);
1779
- return match === null ? max : Math.max(max, Number.parseInt(match[1], 10));
1780
- }, 0);
1781
- return `${prefix}-${maximum + 1}`;
1782
- }
1783
- #nextTaskRecordId(taskId, kind) {
1784
- return this.#mutateResult((state) => {
1785
- const aggregate = requireTaskFromState(state, taskId);
1786
- const next = nextTaskRecordSequence(aggregate, taskId, kind);
1787
- aggregate.idHighWaterMarks[kind] = next;
1788
- return `${TASK_RECORD_ID_PREFIXES[kind]}-${next}`;
1789
- });
1790
- }
1791
- #peekTaskRecordId(taskId, kind) {
1792
- const aggregate = this.#requireTask(taskId);
1793
- return `${TASK_RECORD_ID_PREFIXES[kind]}-${nextTaskRecordSequence(aggregate, taskId, kind)}`;
1794
- }
1795
- #readState() {
1796
- this.#requireReadableSchema();
1797
- const path = join(this.rootDir, STORAGE_STATE_FILE);
1798
- if (!existsSync(path))
1799
- return emptyState();
1800
- return this.#parseState(readFileSync(path, "utf8"));
1801
- }
1802
- #readCachedState() {
1803
- this.#requireReadableSchema();
1804
- const path = join(this.rootDir, STORAGE_STATE_FILE);
1805
- if (!existsSync(path)) {
1806
- if (this.#readCache?.fingerprint === "missing")
1807
- return this.#readCache.state;
1808
- const state = emptyState();
1809
- this.#readCache = { fingerprint: "missing", state };
1810
- return state;
1811
- }
1812
- const fingerprint = stateFileFingerprint(path);
1813
- if (this.#readCache?.fingerprint === fingerprint)
1814
- return this.#readCache.state;
1815
- const state = this.#parseState(readFileSync(path, "utf8"));
1816
- this.#readCache = { fingerprint, state };
1817
- return state;
1818
- }
1819
- #commit(state, expectedRevision) {
1820
- // The upgrade admission fence is honored at the single write moment, so both
1821
- // baseline CLI writers and the Controller (which mutate through this same
1822
- // store) refuse to persist while an upgrade owns the Home. Reads and
1823
- // read-only transactions never reach here, and the fencing process itself is
1824
- // exempt so it can re-pin the revision under the lock.
1825
- assertHomeWritable(this.rootDir);
1826
- const current = this.#readState();
1827
- if (current.revision !== expectedRevision) {
1828
- throw new StorageConflictError(`Storage changed concurrently (expected revision ${expectedRevision}, found ${current.revision}).`);
1829
- }
1830
- state.revision = expectedRevision + 1;
1831
- const content = `${JSON.stringify(state, null, 2)}\n`;
1832
- parseState(content);
1833
- writeTextFileAtomically(join(this.rootDir, STORAGE_STATE_FILE), content);
1834
- if (this.#normalizeState !== undefined) {
1835
- // A compatible read never rewrites the Home. Its first actual mutation
1836
- // emits current-only state, then advances the durable manifest to the
1837
- // same current family versions under the existing storage write lock.
1838
- // A crash between the two atomic files is detected as manifest/state
1839
- // inconsistency on the next open; it is never silently accepted.
1840
- writeCurrentStorageManifest(this.rootDir);
1841
- this.#normalizeState = undefined;
1842
- }
1843
- // Keep the state we just wrote as the warm read cache. The atomic write
1844
- // produced a new fingerprint, so a concurrent external writer is still
1845
- // detected on the next read; our own mutations no longer re-parse the
1846
- // whole Home to observe state they already held under the write lock.
1847
- this.#readCache = {
1848
- fingerprint: stateFileFingerprint(join(this.rootDir, STORAGE_STATE_FILE)),
1849
- state
1850
- };
1851
- }
1852
- #parseState(raw) {
1853
- return parseState(this.#normalizeState?.(raw) ?? raw);
1854
- }
1855
- #withWriteLock(execute) {
1856
- const release = acquireStorageLock(this.rootDir);
1857
- try {
1858
- return execute();
1859
- }
1860
- finally {
1861
- release();
1862
- }
1863
- }
1864
- #requireReadableSchema() {
1865
- if (this.#normalizeState === undefined)
1866
- requireStorageSchema(this.rootDir);
1867
- else
1868
- requireCompatibleStorageSchema(this.rootDir);
1869
- }
1870
- }
1871
- /**
1872
- * The on-disk identity of a `state.json` the store's read cache is keyed on.
1873
- * The one-snapshot current-Home open fences its single read with the same
1874
- * fingerprint, so a writer that changes the file between the fence and the
1875
- * store's next read is detected exactly like a normal cache invalidation.
1876
- */
1877
- export function stateFileFingerprint(path) {
1878
- const stat = statSync(path, { bigint: true });
1879
- return [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(":");
1880
- }
1881
71
  export class StorageRecordError extends Error {
1882
72
  constructor(message) { super(message); this.name = "StorageRecordError"; }
1883
73
  }
@@ -1929,539 +119,51 @@ function canonicalizeYuiHome(value) {
1929
119
  }
1930
120
  const parent = dirname(current);
1931
121
  if (parent === current)
1932
- return absolute;
1933
- trailing.unshift(basename(current));
1934
- current = parent;
1935
- continue;
1936
- }
1937
- throw unstableYuiHome(absolute, realpathError);
1938
- }
1939
- }
1940
- }
1941
- function unstableYuiHome(path, cause) {
1942
- return new Error(`YUI_HOME cannot be resolved to a stable physical path: ${path}.`, { cause });
1943
- }
1944
- function isErrno(error, code) {
1945
- return error instanceof Error && "code" in error && error.code === code;
1946
- }
1947
- function emptyState() {
1948
- return {
1949
- schemaVersion: CURRENT_STORAGE_STATE_SCHEMA_VERSION,
1950
- revision: 0,
1951
- homeIdentity: generateHomeIdentity(new Date()),
1952
- config: { schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION },
1953
- configuredAgents: {},
1954
- projects: {},
1955
- agentProfiles: {},
1956
- globalRoles: {},
1957
- globalRoleSessionSets: {},
1958
- tasks: {},
1959
- mailboxes: {}
1960
- };
1961
- }
1962
- /** rr13: Durable map key for a Session's job caller key hash. */
1963
- function jobCallerKeyHashKey(roleName, agentId) {
1964
- return `${roleName}\0${agentId}`;
1965
- }
1966
- function emptyStoredTask(task) {
1967
- return {
1968
- schemaVersion: CURRENT_STORED_TASK_SCHEMA_VERSION,
1969
- task,
1970
- idHighWaterMarks: emptyTaskIdHighWaterMarks(),
1971
- brief: null,
1972
- changeSets: {},
1973
- integrationAttempts: {},
1974
- integrationQueue: {},
1975
- durableJobs: {},
1976
- roles: {},
1977
- managedWorkspaces: {},
1978
- roleSessionSets: {},
1979
- jobCallerKeyHashes: {},
1980
- workItems: {},
1981
- contextSnapshots: {},
1982
- agentRuns: {},
1983
- reviewRounds: {},
1984
- activeRuns: {},
1985
- messages: {},
1986
- inputRequests: {},
1987
- decisions: {},
1988
- milestones: {},
1989
- events: {},
1990
- wakes: {},
1991
- capabilityGrants: {},
1992
- releaseWorkflows: {},
1993
- publicationReferences: {},
1994
- leaderFailure: null
1995
- };
1996
- }
1997
- function emptyTaskIdHighWaterMarks() {
1998
- return {
1999
- workItem: 0,
2000
- contextSnapshot: 0,
2001
- agentRun: 0,
2002
- reviewRound: 0,
2003
- reviewFinding: 0,
2004
- changeSet: 0,
2005
- integrationAttempt: 0,
2006
- integrationQueue: 0,
2007
- durableJob: 0,
2008
- message: 0,
2009
- inputRequest: 0,
2010
- decision: 0,
2011
- milestone: 0,
2012
- event: 0,
2013
- taskWake: 0,
2014
- capabilityGrant: 0,
2015
- releaseWorkflow: 0,
2016
- publicationReference: 0
2017
- };
2018
- }
2019
- function nextTaskRecordSequence(aggregate, taskId, kind) {
2020
- const next = aggregate.idHighWaterMarks[kind] + 1;
2021
- if (!Number.isSafeInteger(next)) {
2022
- throw new StorageRecordError(`Task ${kind} id space is exhausted: ${taskId}.`);
2023
- }
2024
- return next;
2025
- }
2026
- function validateTaskIdHighWaterMarks(value, taskId) {
2027
- const marks = object(value, `Task id high-water marks ${taskId}`);
2028
- const kinds = Object.keys(TASK_RECORD_ID_PREFIXES);
2029
- exact(marks, kinds, `Task id high-water marks ${taskId}`);
2030
- for (const kind of kinds) {
2031
- if (!Number.isSafeInteger(marks[kind]) || marks[kind] < 0) {
2032
- throw new StorageRecordError(`Task id high-water mark is invalid: ${taskId}/${kind}.`);
2033
- }
2034
- }
2035
- }
2036
- function parseState(raw) {
2037
- let parsed;
2038
- try {
2039
- parsed = JSON.parse(raw);
2040
- }
2041
- catch (error) {
2042
- throw new StorageRecordError(`Invalid ${STORAGE_STATE_FILE}: ${error instanceof Error ? error.message : String(error)}`);
2043
- }
2044
- const state = object(parsed, "Storage state");
2045
- exact(state, [
2046
- "schemaVersion",
2047
- "revision",
2048
- "homeIdentity",
2049
- "config",
2050
- "configuredAgents",
2051
- "projects",
2052
- "agentProfiles",
2053
- "globalRoles",
2054
- "globalRoleSessionSets",
2055
- "tasks",
2056
- "mailboxes"
2057
- ], "Storage state");
2058
- if (state.schemaVersion !== CURRENT_STORAGE_STATE_SCHEMA_VERSION || !Number.isInteger(state.revision) || state.revision < 0)
2059
- throw new StorageRecordError("Storage state schemaVersion/revision is invalid.");
2060
- const result = clone(state);
2061
- try {
2062
- result.homeIdentity = validateHomeIdentity(versioned(result.homeIdentity, CURRENT_HOME_IDENTITY_SCHEMA_VERSION, "Home identity"));
2063
- }
2064
- catch (error) {
2065
- throw new StorageRecordError(error instanceof Error ? error.message : String(error));
2066
- }
2067
- result.config = versioned(result.config, CURRENT_CONFIG_SCHEMA_VERSION, "Yui config");
2068
- validateYuiConfig(result.config);
2069
- parseMap(result.configuredAgents, (value, key) => {
2070
- const agent = identified(value, CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION, "id", key, "Configured Agent");
2071
- validateConfiguredAgent(agent);
2072
- return agent;
2073
- }, "configuredAgents");
2074
- parseMap(result.projects, (value, key) => {
2075
- const project = identified(value, CURRENT_PROJECT_SCHEMA_VERSION, "id", key, "Project");
2076
- validateProject(project);
2077
- return project;
2078
- }, "projects");
2079
- try {
2080
- assertProjectCatalog(Object.values(result.projects));
2081
- }
2082
- catch (error) {
2083
- throw new StorageRecordError(error instanceof Error ? error.message : String(error));
2084
- }
2085
- parseMap(result.agentProfiles, (value, key) => {
2086
- const profile = identified(value, CURRENT_AGENT_PROFILE_SCHEMA_VERSION, "id", key, "Agent Profile");
2087
- validateAgentProfile(profile);
2088
- return profile;
2089
- }, "agentProfiles");
2090
- parseMap(result.globalRoles, (value, key) => {
2091
- const role = identified(value, CURRENT_GLOBAL_ROLE_SCHEMA_VERSION, "name", key, "Global Role");
2092
- validateGlobalRole(role);
2093
- return role;
2094
- }, "globalRoles");
2095
- parseMap(result.globalRoleSessionSets, (value, key) => { const set = globalSessions(value); if (set.owner.roleName !== key)
2096
- throw new StorageRecordError(`Global Role session set identity is inconsistent: ${key}`); return set; }, "globalRoleSessionSets");
2097
- parseMap(result.tasks, (value, key) => parseStoredTask(value, key), "tasks");
2098
- parseMap(result.mailboxes, (value, key) => {
2099
- let mailbox;
2100
- try {
2101
- mailbox = validateWorkMailbox(versioned(value, CURRENT_WORK_MAILBOX_SCHEMA_VERSION, "WorkMailbox"));
2102
- }
2103
- catch (error) {
2104
- throw new StorageRecordError(error instanceof Error ? error.message : String(error));
2105
- }
2106
- if (mailboxTargetKey(mailbox.target) !== key) {
2107
- throw new StorageRecordError(`WorkMailbox identity is inconsistent: ${key}`);
2108
- }
2109
- return mailbox;
2110
- }, "mailboxes");
2111
- for (const [name, role] of Object.entries(result.globalRoles)) {
2112
- const sessions = result.globalRoleSessionSets[name];
2113
- if (sessions !== undefined)
2114
- assertSessionsMatchRole(sessions, role);
2115
- }
2116
- for (const name of Object.keys(result.globalRoleSessionSets)) {
2117
- if (result.globalRoles[name] === undefined) {
2118
- throw new StorageRecordError(`Global Role session set has no Role: ${name}`);
2119
- }
2120
- }
2121
- for (const aggregate of Object.values(result.tasks)) {
2122
- for (const binding of aggregate.task.projectBindings) {
2123
- if (result.projects[binding.projectId] === undefined) {
2124
- throw new StorageRecordError(`Task Project not found: ${aggregate.task.id}/${binding.projectId}`);
2125
- }
2126
- }
2127
- for (const [name, role] of Object.entries(aggregate.roles)) {
2128
- const sessions = aggregate.roleSessionSets[name];
2129
- if (sessions !== undefined)
2130
- assertSessionsMatchRole(sessions, role);
2131
- }
2132
- for (const name of Object.keys(aggregate.roleSessionSets)) {
2133
- if (aggregate.roles[name] === undefined) {
2134
- throw new StorageRecordError(`Task Role session set has no Role: ${aggregate.task.id}/${name}`);
2135
- }
2136
- }
2137
- for (const [key, workspace] of Object.entries(aggregate.managedWorkspaces)) {
2138
- if (workspace.owner.taskId !== aggregate.task.id) {
2139
- throw new StorageRecordError(`Managed workspace belongs to another Task: ${key}`);
2140
- }
2141
- const boundProjects = new Set(aggregate.task.projectBindings.map(({ projectId }) => projectId));
2142
- if (workspace.entries.some(({ projectId }) => !boundProjects.has(projectId))) {
2143
- throw new StorageRecordError(`Managed workspace Project does not match Task: ${aggregate.task.id}/${key}`);
2144
- }
2145
- if (workspace.owner.type === "work-item"
2146
- && aggregate.workItems[workspace.owner.workItemId] === undefined) {
2147
- throw new StorageRecordError(`Managed workspace WorkItem not found: ${aggregate.task.id}/${workspace.owner.workItemId}`);
2148
- }
2149
- if (workspace.owner.type === "review-round"
2150
- && aggregate.reviewRounds[workspace.owner.reviewRoundId] === undefined) {
2151
- throw new StorageRecordError(`Managed workspace ReviewRound not found: ${aggregate.task.id}/${workspace.owner.reviewRoundId}`);
2152
- }
2153
- if (workspace.owner.type === "integration-attempt"
2154
- && aggregate.integrationAttempts[workspace.owner.integrationAttemptId] === undefined) {
2155
- throw new StorageRecordError(`Managed workspace Integration Attempt not found: ${aggregate.task.id}/${workspace.owner.integrationAttemptId}`);
2156
- }
2157
- if (workspace.owner.type === "execution-lane") {
2158
- const laneOwner = workspace.owner;
2159
- const laneItem = laneOwner.purpose === "execution"
2160
- ? aggregate.workItems[laneOwner.workItemId ?? ""]
2161
- : undefined;
2162
- const group = laneOwner.purpose === "execution"
2163
- ? (laneItem === undefined
2164
- ? undefined
2165
- : workItemExecutionGroupById(laneItem, laneOwner.executionGroupId))
2166
- : aggregate.reviewRounds[laneOwner.reviewRoundId ?? ""]?.executionGroup;
2167
- if (group?.id !== laneOwner.executionGroupId
2168
- || !group.lanes.some(({ id }) => id === laneOwner.executionLaneId)) {
2169
- throw new StorageRecordError(`Managed workspace Execution Lane not found: ${aggregate.task.id}/${key}`);
2170
- }
2171
- }
2172
- }
2173
- validateCanonicalTaskReferences(result, aggregate);
2174
- }
2175
- for (const mailbox of Object.values(result.mailboxes))
2176
- validateMailboxReferences(result, mailbox);
2177
- return result;
2178
- }
2179
- /** Strict current-model gate used by the compatible loader before any writer opens. */
2180
- export function validateCurrentStorageStateSnapshot(value) {
2181
- parseState(`${JSON.stringify(value)}\n`);
2182
- }
2183
- function parseStoredTask(value, taskId) {
2184
- const aggregate = object(value, `Task aggregate ${taskId}`);
2185
- exact(aggregate, [
2186
- "schemaVersion",
2187
- "task",
2188
- "idHighWaterMarks",
2189
- "brief",
2190
- "changeSets",
2191
- "integrationAttempts",
2192
- "integrationQueue",
2193
- "durableJobs",
2194
- "roles",
2195
- "managedWorkspaces",
2196
- "roleSessionSets",
2197
- "jobCallerKeyHashes",
2198
- "workItems",
2199
- "contextSnapshots",
2200
- "agentRuns",
2201
- "reviewRounds",
2202
- "activeRuns",
2203
- "messages",
2204
- "inputRequests",
2205
- "decisions",
2206
- "milestones",
2207
- "events",
2208
- "wakes",
2209
- "capabilityGrants",
2210
- "releaseWorkflows",
2211
- "publicationReferences",
2212
- "leaderFailure"
2213
- ], `Task aggregate ${taskId}`);
2214
- parseMap(aggregate.changeSets, (record, key) => {
2215
- const changeSet = identifiedChangeSet(record, key);
2216
- if (changeSet.taskId !== taskId) {
2217
- throw new StorageRecordError(`ChangeSet belongs to another Task: ${changeSet.taskId}.`);
2218
- }
2219
- validateChangeSet(changeSet);
2220
- return changeSet;
2221
- }, "changeSets");
2222
- parseMap(aggregate.integrationAttempts, (record, key) => {
2223
- const attempt = identified(record, CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION, "id", key, "Integration Attempt");
2224
- if (attempt.taskId !== taskId) {
2225
- throw new StorageRecordError(`Integration Attempt belongs to another Task: ${attempt.taskId}.`);
2226
- }
2227
- validateIntegrationAttempt(attempt);
2228
- return attempt;
2229
- }, "integrationAttempts");
2230
- parseMap(aggregate.integrationQueue, (record, key) => {
2231
- const entry = identified(record, CURRENT_INTEGRATION_QUEUE_SCHEMA_VERSION, "id", key, "Integration queue entry");
2232
- if (entry.taskId !== taskId) {
2233
- throw new StorageRecordError(`Integration queue entry belongs to another Task: ${entry.taskId}.`);
2234
- }
2235
- if (aggregate.changeSets[entry.changeSetId] === undefined) {
2236
- throw new StorageRecordError(`Integration queue entry ChangeSet not found: ${entry.changeSetId}.`);
2237
- }
2238
- validateIntegrationQueueEntry(entry);
2239
- return entry;
2240
- }, "integrationQueue");
2241
- parseMap(aggregate.durableJobs, (record, key) => {
2242
- const job = identified(record, CURRENT_DURABLE_JOB_SCHEMA_VERSION, "id", key, "DurableJob");
2243
- if (job.taskId !== taskId) {
2244
- throw new StorageRecordError(`DurableJob belongs to another Task: ${job.taskId}.`);
2245
- }
2246
- validateDurableJob(job);
2247
- return job;
2248
- }, "durableJobs");
2249
- versioned(aggregate, CURRENT_STORED_TASK_SCHEMA_VERSION, `Task aggregate ${taskId}`);
2250
- validateTaskIdHighWaterMarks(aggregate.idHighWaterMarks, taskId);
2251
- validateTask(identified(aggregate.task, CURRENT_TASK_SCHEMA_VERSION, "id", taskId, "Task"));
2252
- if (aggregate.brief !== null)
2253
- storedTaskBrief(aggregate.brief);
2254
- parseMap(aggregate.roles, (record, key) => {
2255
- const role = identified(record, CURRENT_TASK_ROLE_SCHEMA_VERSION, "name", key, "Task Role");
2256
- if (role.taskId !== taskId)
2257
- throw new StorageRecordError(`Task Role belongs to another Task: ${role.taskId}`);
2258
- validateTaskRole(role);
2259
- return role;
2260
- }, "roles");
2261
- parseMap(aggregate.managedWorkspaces, (record, key) => {
2262
- const workspace = versioned(record, CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION, "Managed workspace");
2263
- validateManagedWorkspace(workspace);
2264
- if (workspace.owner.taskId !== taskId) {
2265
- throw new StorageRecordError(`Managed workspace belongs to another Task: ${workspace.owner.taskId}`);
2266
- }
2267
- if (managedWorkspaceKey(workspace.owner) !== key) {
2268
- throw new StorageRecordError(`Managed workspace identity is inconsistent: ${taskId}/${key}`);
2269
- }
2270
- return workspace;
2271
- }, "managedWorkspaces");
2272
- parseMap(aggregate.roleSessionSets, (record, key) => { const set = taskSessions(record); if (set.owner.taskId !== taskId || set.owner.roleName !== key)
2273
- throw new StorageRecordError(`Task Role session set identity is inconsistent: ${taskId}/${key}`); return set; }, "roleSessionSets");
2274
- parseMap(aggregate.jobCallerKeyHashes, (hash, key) => {
2275
- if (typeof hash !== "string" || !/^[a-f0-9]{64}$/u.test(hash)) {
2276
- throw new StorageRecordError(`Job caller key hash is invalid: ${taskId}/${key}.`);
2277
- }
2278
- return hash;
2279
- }, "jobCallerKeyHashes");
2280
- parseMap(aggregate.workItems, (record, key) => {
2281
- const item = identified(record, CURRENT_WORK_ITEM_SCHEMA_VERSION, "id", key, "Work item");
2282
- if (item.taskId !== taskId) {
2283
- throw new StorageRecordError(`Work item belongs to another Task: ${item.taskId}`);
2284
- }
2285
- validateWorkItem(item);
2286
- return item;
2287
- }, "workItems");
2288
- parseMap(aggregate.contextSnapshots, (record, key) => {
2289
- const snapshot = identified(record, CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION, "id", key, "Context Snapshot");
2290
- if (snapshot.taskId !== taskId) {
2291
- throw new StorageRecordError(`Context Snapshot belongs to another Task: ${snapshot.taskId}.`);
2292
- }
2293
- validateContextSnapshot(snapshot);
2294
- return snapshot;
2295
- }, "contextSnapshots");
2296
- parseMap(aggregate.agentRuns, (record, key) => {
2297
- const run = identified(record, CURRENT_AGENT_RUN_SCHEMA_VERSION, "id", key, "Agent run");
2298
- if (run.taskId !== taskId) {
2299
- throw new StorageRecordError(`Agent run belongs to another Task: ${run.taskId}`);
2300
- }
2301
- validateAgentRun(run);
2302
- return run;
2303
- }, "agentRuns");
2304
- parseMap(aggregate.reviewRounds, (record, key) => {
2305
- const round = identified(record, CURRENT_REVIEW_ROUND_SCHEMA_VERSION, "id", key, "ReviewRound");
2306
- if (round.taskId !== taskId) {
2307
- throw new StorageRecordError(`ReviewRound belongs to another Task: ${round.taskId}.`);
2308
- }
2309
- validateReviewRound(round);
2310
- return round;
2311
- }, "reviewRounds");
2312
- parseMap(aggregate.activeRuns, (record, key) => {
2313
- const pointer = versioned(record, CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION, `Active run ${key}`);
2314
- const run = typeof pointer.runId === "string" ? aggregate.agentRuns[pointer.runId] : undefined;
2315
- const laneMatch = executionLaneActiveRunKeyParts(key);
2316
- const validLanePointer = laneMatch !== null
2317
- && run !== undefined
2318
- && run.executionGroupId === laneMatch.executionGroupId
2319
- && run.executionLaneId === laneMatch.executionLaneId;
2320
- const validRolePointer = laneMatch === null
2321
- && run !== undefined
2322
- && run.roleName === key;
2323
- if (run === undefined || run.status !== "active"
2324
- || (!validLanePointer && !validRolePointer)) {
2325
- throw new StorageRecordError(`Active run pointer is invalid: ${taskId}/${key}`);
2326
- }
2327
- return pointer;
2328
- }, "activeRuns");
2329
- parseMap(aggregate.messages, (record, key) => {
2330
- const message = identified(record, CURRENT_MESSAGE_SCHEMA_VERSION, "id", key, "Message");
2331
- if (message.taskId !== taskId) {
2332
- throw new StorageRecordError(`Message belongs to another Task: ${message.taskId}`);
2333
- }
2334
- validateTaskMessage(message);
2335
- return message;
2336
- }, "messages");
2337
- parseMap(aggregate.inputRequests, (record, key) => {
2338
- const request = validateInputRequest(versioned(record, CURRENT_INPUT_REQUEST_SCHEMA_VERSION, "Input request"));
2339
- if (request.id !== key) {
2340
- throw new StorageRecordError(`Input request identity is inconsistent: ${key}.`);
2341
- }
2342
- if (request.taskId !== taskId) {
2343
- throw new StorageRecordError(`Input request belongs to another Task: ${request.taskId}`);
2344
- }
2345
- return request;
2346
- }, "inputRequests");
2347
- parseMap(aggregate.decisions, (record, key) => {
2348
- const decision = storedDecision(record);
2349
- if (decision.id !== key)
2350
- throw new StorageRecordError(`Decision identity is inconsistent: ${key}.`);
2351
- if (decision.taskId !== taskId) {
2352
- throw new StorageRecordError(`Decision belongs to another Task: ${decision.taskId}`);
2353
- }
2354
- return decision;
2355
- }, "decisions");
2356
- parseMap(aggregate.milestones, (record, key) => {
2357
- const milestone = storedMilestone(record);
2358
- if (milestone.id !== key)
2359
- throw new StorageRecordError(`Milestone identity is inconsistent: ${key}.`);
2360
- if (milestone.taskId !== taskId) {
2361
- throw new StorageRecordError(`Milestone belongs to another Task: ${milestone.taskId}`);
2362
- }
2363
- return milestone;
2364
- }, "milestones");
2365
- parseMap(aggregate.events, (record, key) => {
2366
- const event = storedTaskEvent(record);
2367
- if (event.id !== key)
2368
- throw new StorageRecordError(`Task event identity is inconsistent: ${key}.`);
2369
- if (event.taskId !== taskId) {
2370
- throw new StorageRecordError(`Task event belongs to another Task: ${event.taskId}`);
2371
- }
2372
- return event;
2373
- }, "events");
2374
- parseMap(aggregate.wakes, (record, key) => {
2375
- const wake = identified(record, CURRENT_TASK_WAKE_SCHEMA_VERSION, "id", key, "Task wake");
2376
- if (wake.taskId !== taskId) {
2377
- throw new StorageRecordError(`Task wake belongs to another Task: ${wake.taskId}`);
2378
- }
2379
- validateTaskWake(wake);
2380
- return wake;
2381
- }, "wakes");
2382
- parseMap(aggregate.capabilityGrants, (record, key) => {
2383
- const grant = storedCapabilityGrant(record);
2384
- if (grant.id !== key) {
2385
- throw new StorageRecordError(`Capability grant identity is inconsistent: ${key}.`);
2386
- }
2387
- if (grant.taskId !== taskId) {
2388
- throw new StorageRecordError(`Capability grant belongs to another Task: ${grant.taskId}`);
2389
- }
2390
- return grant;
2391
- }, "capabilityGrants");
2392
- parseMap(aggregate.releaseWorkflows, (record, key) => {
2393
- const workflow = storedReleaseWorkflow(record);
2394
- if (workflow.id !== key) {
2395
- throw new StorageRecordError(`Release workflow identity is inconsistent: ${key}.`);
2396
- }
2397
- if (workflow.taskId !== taskId) {
2398
- throw new StorageRecordError(`Release workflow belongs to another Task: ${workflow.taskId}`);
2399
- }
2400
- return workflow;
2401
- }, "releaseWorkflows");
2402
- parseMap(aggregate.publicationReferences, (record, key) => {
2403
- const reference = storedPublicationReference(record);
2404
- if (reference.id !== key) {
2405
- throw new StorageRecordError(`Publication reference identity is inconsistent: ${key}.`);
2406
- }
2407
- if (reference.taskId !== taskId) {
2408
- throw new StorageRecordError(`Publication reference belongs to another Task: ${reference.taskId}`);
2409
- }
2410
- return reference;
2411
- }, "publicationReferences");
2412
- if (aggregate.leaderFailure !== null) {
2413
- identified(aggregate.leaderFailure, CURRENT_LEADER_FAILURE_SCHEMA_VERSION, "taskId", taskId, "Leader failure");
2414
- }
2415
- validateTaskIdHighWaterCoverage(aggregate, taskId);
2416
- return aggregate;
2417
- }
2418
- function validateTaskIdHighWaterCoverage(aggregate, taskId) {
2419
- const records = {
2420
- workItem: aggregate.workItems,
2421
- contextSnapshot: aggregate.contextSnapshots,
2422
- agentRun: aggregate.agentRuns,
2423
- reviewRound: aggregate.reviewRounds,
2424
- // Issue 06 dbonly: review findings are SQLite-native; the file aggregate
2425
- // never carries them, so coverage is trivially empty.
2426
- reviewFinding: {},
2427
- changeSet: aggregate.changeSets,
2428
- integrationAttempt: aggregate.integrationAttempts,
2429
- integrationQueue: aggregate.integrationQueue,
2430
- durableJob: aggregate.durableJobs,
2431
- message: aggregate.messages,
2432
- inputRequest: aggregate.inputRequests,
2433
- decision: aggregate.decisions,
2434
- milestone: aggregate.milestones,
2435
- event: aggregate.events,
2436
- taskWake: aggregate.wakes,
2437
- capabilityGrant: aggregate.capabilityGrants,
2438
- releaseWorkflow: aggregate.releaseWorkflows,
2439
- publicationReference: aggregate.publicationReferences
2440
- };
2441
- for (const kind of Object.keys(TASK_RECORD_ID_PREFIXES)) {
2442
- const pattern = new RegExp(`^${TASK_RECORD_ID_PREFIXES[kind]}-(\\d+)$`);
2443
- for (const id of Object.keys(records[kind])) {
2444
- const match = pattern.exec(id);
2445
- if (match === null) {
2446
- throw new StorageRecordError(`Task-local ${kind} id is invalid: ${taskId}/${id}.`);
2447
- }
2448
- const sequence = Number.parseInt(match[1], 10);
2449
- if (sequence > aggregate.idHighWaterMarks[kind]) {
2450
- throw new StorageRecordError(`Task id high-water mark is behind ${taskId}/${kind}: ${id}.`);
122
+ return absolute;
123
+ trailing.unshift(basename(current));
124
+ current = parent;
125
+ continue;
2451
126
  }
127
+ throw unstableYuiHome(absolute, realpathError);
2452
128
  }
2453
129
  }
2454
130
  }
2455
- function observeTaskRecordId(aggregate, kind, id) {
2456
- validateTaskRecordReference({ taskId: aggregate.task.id, localId: id }, kind);
2457
- const match = new RegExp(`^${TASK_RECORD_ID_PREFIXES[kind]}-(\\d+)$`).exec(id);
2458
- if (match === null)
2459
- throw new StorageRecordError(`Task-local ${kind} id is invalid: ${id}.`);
2460
- const sequence = Number.parseInt(match[1], 10);
2461
- aggregate.idHighWaterMarks[kind] = Math.max(aggregate.idHighWaterMarks[kind], sequence);
131
+ function unstableYuiHome(path, cause) {
132
+ return new Error(`YUI_HOME cannot be resolved to a stable physical path: ${path}.`, { cause });
133
+ }
134
+ function isErrno(error, code) {
135
+ return error instanceof Error && "code" in error && error.code === code;
2462
136
  }
2463
137
  export function validateYuiConfig(config) {
2464
138
  try {
139
+ const optionalFields = [
140
+ "defaultAgent",
141
+ "defaultWorkspace",
142
+ "timeZone",
143
+ "currentTaskId",
144
+ "lastTaskId",
145
+ "reconciliationIntervalSeconds",
146
+ "resourcesGcMode",
147
+ "resourcesGcAutoQuarantine",
148
+ "review",
149
+ "leaderNextActionMode",
150
+ "runtimeHealth",
151
+ "controllerTaskConcurrency",
152
+ "agentLaunchInactivityTimeoutSeconds",
153
+ "deliveryTimeoutSeconds",
154
+ "leaderSemanticBudgetTurns",
155
+ "resourcesQuarantineTtlHours",
156
+ "tmuxBin",
157
+ "tmuxHistoryLimit",
158
+ "telemetryEnabled",
159
+ "telemetryTerminalKeep",
160
+ "telemetryTurnCap",
161
+ "completionInstallations"
162
+ ];
163
+ exact(config, [
164
+ "schemaVersion",
165
+ ...optionalFields.filter((field) => config[field] !== undefined)
166
+ ], "Yui config");
2465
167
  if (config.schemaVersion !== CURRENT_CONFIG_SCHEMA_VERSION) {
2466
168
  throw new TypeError(`Yui config must use schemaVersion ${CURRENT_CONFIG_SCHEMA_VERSION}.`);
2467
169
  }
@@ -2470,7 +172,6 @@ export function validateYuiConfig(config) {
2470
172
  if (config.review !== undefined)
2471
173
  validateReviewConfig(config.review);
2472
174
  resolveLeaderNextActionMode(config.leaderNextActionMode);
2473
- resolveContextBudget(config.contextBudget);
2474
175
  resolveResourcesGcMode(config.resourcesGcMode);
2475
176
  resolveResourcesGcAutoQuarantine(config.resourcesGcAutoQuarantine);
2476
177
  resolveResourcesQuarantineTtlHours(config.resourcesQuarantineTtlHours);
@@ -2483,161 +184,23 @@ export function validateYuiConfig(config) {
2483
184
  resolveTmuxHistoryLimit(config.tmuxHistoryLimit);
2484
185
  resolveTelemetryEnabled(config.telemetryEnabled);
2485
186
  resolveTelemetryTerminalKeep(config.telemetryTerminalKeep);
2486
- resolveTelemetryRunCap(config.telemetryRunCap);
187
+ resolveTelemetryTurnCap(config.telemetryTurnCap);
2487
188
  }
2488
189
  catch (error) {
2489
190
  throw new StorageRecordError(error instanceof Error ? error.message : "Yui reconciliation interval is invalid.");
2490
191
  }
2491
192
  }
2492
- function globalSessions(value) {
2493
- const set = versioned(value, CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION, "Global Role session set");
2494
- if (set.owner?.scope !== "global" || typeof set.owner.roleName !== "string")
2495
- throw new StorageRecordError("Global Role session owner is invalid.");
2496
- validateSessions(set.sessions);
2497
- validateRoleSessionSet(set);
2498
- return set;
2499
- }
2500
- function taskSessions(value) {
2501
- const set = versioned(value, CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION, "Task Role session set");
2502
- if (set.owner?.scope !== "task" || typeof set.owner.taskId !== "string" || typeof set.owner.roleName !== "string")
2503
- throw new StorageRecordError("Task Role session owner is invalid.");
2504
- validateSessions(set.sessions);
2505
- validateRoleSessionSet(set);
2506
- return set;
2507
- }
2508
- function validateSessions(sessions) {
2509
- parseMap(sessions, (record, key) => identified(record, CURRENT_ROLE_AGENT_SESSION_SCHEMA_VERSION, "agentId", key, "Role Agent session"), "sessions");
2510
- }
2511
- function assertSessionsMatchRole(sessions, role) {
2512
- validateRoleSessionSet(sessions);
2513
- if (sessions.owner.roleName !== role.name) {
2514
- throw new StorageRecordError(`Role session set does not match Role: ${role.name}`);
2515
- }
2516
- if ("taskId" in role && (sessions.owner.scope !== "task" || sessions.owner.taskId !== role.taskId)) {
2517
- throw new StorageRecordError(`Task Role session owner is inconsistent: ${role.taskId}/${role.name}`);
2518
- }
2519
- if (!("taskId" in role) && sessions.owner.scope !== "global") {
2520
- throw new StorageRecordError(`Global Role session owner is inconsistent: ${role.name}`);
2521
- }
2522
- const ownedSessions = [
2523
- ...Object.entries(sessions.sessions),
2524
- ...(sessions.owner.scope === "global"
2525
- ? Object.entries(sessions.history ?? {})
2526
- : [
2527
- ...(sessions.history ?? []).map((session, index) => [`history-${index}`, session])
2528
- ])
2529
- ];
2530
- for (const [, session] of ownedSessions) {
2531
- const agentId = session.agentId;
2532
- const binding = role.agentBindings[agentId];
2533
- if (binding === undefined || binding.adapterId !== session.adapterId) {
2534
- throw new StorageRecordError(`Role Agent session has no matching binding: ${role.name}/${agentId}`);
2535
- }
2536
- }
2537
- }
2538
- function storedTaskBrief(value) {
2539
- const brief = versioned(value, CURRENT_TASK_BRIEF_SCHEMA_VERSION, "Task Brief");
2540
- exact(brief, [
2541
- "schemaVersion",
2542
- "objective",
2543
- "boundaries",
2544
- "technicalApproach",
2545
- "currentFocus",
2546
- "leaderSummary",
2547
- "updatedAt",
2548
- "updatedBy"
2549
- ], "Task Brief");
2550
- requireNormalizedText(brief.objective, "Task Brief objective");
2551
- requireOptionalNormalizedText(brief.technicalApproach, "Task Brief technical approach");
2552
- requireNormalizedText(brief.currentFocus, "Task Brief current focus");
2553
- requireNormalizedText(brief.leaderSummary, "Task Brief leader summary");
2554
- requireNormalizedText(brief.updatedBy, "Task Brief updatedBy");
2555
- if (!Array.isArray(brief.boundaries)) {
2556
- throw new StorageRecordError("Task Brief boundaries must be an array.");
2557
- }
2558
- const boundaries = brief.boundaries.map((boundary) => (requireNormalizedText(boundary, "Task Brief boundary")));
2559
- if (new Set(boundaries).size !== boundaries.length) {
2560
- throw new StorageRecordError("Task Brief boundaries must be unique.");
2561
- }
2562
- requireTimestamp(brief.updatedAt, "Task Brief updatedAt");
2563
- return brief;
2564
- }
2565
- function storedDecision(value) {
2566
- const decision = versioned(value, CURRENT_DECISION_SCHEMA_VERSION, "Decision");
2567
- const baseFields = [
2568
- "schemaVersion", "id", "taskId", "title", "rationale", "status", "createdAt", "updatedAt"
2569
- ];
2570
- if (decision.status === "active") {
2571
- exact(decision, baseFields, "Decision");
2572
- }
2573
- else if (decision.status === "superseded") {
2574
- exact(decision, [...baseFields, "supersededReason", "supersededAt"], "Decision");
2575
- requireNormalizedText(decision.supersededReason, "Decision supersededReason");
2576
- requireTimestamp(decision.supersededAt, "Decision supersededAt");
2577
- if (decision.supersededAt !== decision.updatedAt) {
2578
- throw new StorageRecordError("Decision supersededAt must match updatedAt.");
2579
- }
2580
- }
2581
- else {
2582
- throw new StorageRecordError(`Decision status is invalid: ${String(decision.status)}.`);
2583
- }
2584
- requireRecordIdentity(decision.id, "Decision id");
2585
- requireRecordIdentity(decision.taskId, "Decision Task id");
2586
- validateTaskRecordReference({ taskId: decision.taskId, localId: decision.id }, "decision");
2587
- requireNormalizedText(decision.title, "Decision title");
2588
- requireNormalizedText(decision.rationale, "Decision rationale");
2589
- requireTimestamp(decision.createdAt, "Decision createdAt");
2590
- requireTimestamp(decision.updatedAt, "Decision updatedAt");
2591
- if (Date.parse(decision.updatedAt) < Date.parse(decision.createdAt)) {
2592
- throw new StorageRecordError("Decision updatedAt cannot precede createdAt.");
2593
- }
2594
- return decision;
2595
- }
2596
- function storedMilestone(value) {
2597
- const milestone = versioned(value, CURRENT_MILESTONE_SCHEMA_VERSION, "Milestone");
2598
- exact(milestone, ["schemaVersion", "id", "taskId", "title", "summary", "createdBy", "createdAt"], "Milestone");
2599
- requireRecordIdentity(milestone.id, "Milestone id");
2600
- requireRecordIdentity(milestone.taskId, "Milestone Task id");
2601
- validateTaskRecordReference({ taskId: milestone.taskId, localId: milestone.id }, "milestone");
2602
- requireNormalizedText(milestone.title, "Milestone title");
2603
- requireNormalizedText(milestone.summary, "Milestone summary");
2604
- if (milestone.createdBy !== "user"
2605
- && milestone.createdBy !== "operator"
2606
- && milestone.createdBy !== "leader") {
2607
- throw new StorageRecordError("Milestone createdBy is invalid.");
2608
- }
2609
- requireTimestamp(milestone.createdAt, "Milestone createdAt");
2610
- return milestone;
2611
- }
2612
- function storedTaskEvent(value) {
2613
- const event = versioned(value, CURRENT_EVENT_SCHEMA_VERSION, "Task event");
2614
- exact(event, ["schemaVersion", "id", "taskId", "type", "payload", "createdAt"], "Task event");
2615
- requireRecordIdentity(event.id, "Task event id");
2616
- requireRecordIdentity(event.taskId, "Task event Task id");
2617
- validateTaskRecordReference({ taskId: event.taskId, localId: event.id }, "event");
2618
- requireNormalizedText(event.type, "Task event type");
2619
- const payload = object(event.payload, "Task event payload");
2620
- for (const [key, payloadValue] of Object.entries(payload)) {
2621
- requireRecordIdentity(key, "Task event payload key");
2622
- if (typeof payloadValue !== "string" || payloadValue.includes("\0")) {
2623
- throw new StorageRecordError(`Task event payload value is invalid: ${key}.`);
2624
- }
2625
- }
2626
- requireTimestamp(event.createdAt, "Task event createdAt");
2627
- return event;
2628
- }
2629
193
  export function storedCapabilityGrant(value) {
2630
194
  const grant = versioned(value, CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION, "Capability grant");
2631
195
  const fields = [
2632
196
  "schemaVersion", "id", "taskId", "granter", "scope", "actions",
2633
- "parameterBounds", "usesUsed", "irreversibilityCeiling", "createdAt", "updatedAt"
197
+ "parameterBounds", "usesUsed", "useReservations", "irreversibilityCeiling",
198
+ "createdAt", "updatedAt"
2634
199
  ];
2635
200
  if (grant.expiresAt !== undefined)
2636
201
  fields.push("expiresAt");
2637
202
  if (grant.maxUses !== undefined)
2638
203
  fields.push("maxUses");
2639
- if (grant.useReservations !== undefined)
2640
- fields.push("useReservations");
2641
204
  if (grant.revokedAt !== undefined)
2642
205
  fields.push("revokedAt");
2643
206
  if (grant.revokedBy !== undefined)
@@ -2788,9 +351,9 @@ export function isValidCapabilityGrantTransition(existing, candidate) {
2788
351
  && Date.parse(candidate.updatedAt) >= Date.parse(existing.updatedAt);
2789
352
  }
2790
353
  function reservationsAppendOnly(existing, candidate) {
2791
- if (existing === undefined || existing.length === 0)
354
+ if (existing.length === 0)
2792
355
  return true;
2793
- if (candidate === undefined || candidate.length < existing.length)
356
+ if (candidate.length < existing.length)
2794
357
  return false;
2795
358
  return existing.every((key, index) => candidate[index] === key);
2796
359
  }
@@ -2935,28 +498,6 @@ export function storedPublicationReference(value) {
2935
498
  throw new StorageRecordError(error instanceof Error ? error.message : String(error));
2936
499
  }
2937
500
  }
2938
- function latestPublicationReference(references) {
2939
- const byId = new Map(references.map((reference) => [reference.id, reference]));
2940
- const superseded = new Set(references
2941
- .map((reference) => reference.supersedes)
2942
- .filter((id) => id !== undefined));
2943
- const roots = references.filter((reference) => !superseded.has(reference.id));
2944
- const latest = roots.length === 1 ? roots[0] : [...byId.values()].sort((left, right) => (right.id.localeCompare(left.id, undefined, { numeric: true })))[0];
2945
- let current = latest;
2946
- const seen = new Set();
2947
- while (current.supersedes !== undefined) {
2948
- if (seen.has(current.id)) {
2949
- throw new StorageRecordError(`Publication supersession cycle at ${current.id}.`);
2950
- }
2951
- seen.add(current.id);
2952
- const previous = byId.get(current.supersedes);
2953
- if (previous === undefined) {
2954
- throw new StorageRecordError(`Publication supersession target not found: ${current.id}/${current.supersedes}.`);
2955
- }
2956
- current = previous;
2957
- }
2958
- return latest;
2959
- }
2960
501
  function storedReleaseWorkflowSource(source) {
2961
502
  const value = object(source, "Release workflow source");
2962
503
  const sourceFields = ["repository", "commit"];
@@ -3087,35 +628,6 @@ function isLegalStepTransition(from, to) {
3087
628
  return true;
3088
629
  return LEGAL_STEP_TRANSITIONS[from]?.includes(to) ?? false;
3089
630
  }
3090
- function isValidDecisionSupersession(existing, candidate) {
3091
- return existing.status === "active"
3092
- && candidate.status === "superseded"
3093
- && candidate.id === existing.id
3094
- && candidate.taskId === existing.taskId
3095
- && candidate.title === existing.title
3096
- && candidate.rationale === existing.rationale
3097
- && candidate.createdAt === existing.createdAt
3098
- && Date.parse(candidate.updatedAt) >= Date.parse(existing.updatedAt);
3099
- }
3100
- function isValidInputRequestTransition(existing, candidate) {
3101
- if (existing.status !== "open" || candidate.status === "open")
3102
- return false;
3103
- return candidate.id === existing.id
3104
- && candidate.taskId === existing.taskId
3105
- && isDeepStrictEqual(candidate.requester, existing.requester)
3106
- && candidate.question === existing.question
3107
- && isDeepStrictEqual(candidate.choices, existing.choices)
3108
- && isDeepStrictEqual(candidate.blockedRefs, existing.blockedRefs)
3109
- && isDeepStrictEqual(candidate.policy, existing.policy)
3110
- && candidate.createdAt === existing.createdAt
3111
- && Date.parse(candidate.updatedAt) >= Date.parse(existing.updatedAt);
3112
- }
3113
- function requireTaskFromState(state, taskId) {
3114
- const aggregate = state.tasks[taskId];
3115
- if (aggregate === undefined)
3116
- throw new StorageRecordError(`Task not found: ${taskId}`);
3117
- return aggregate;
3118
- }
3119
631
  function requireRecordIdentity(value, label) {
3120
632
  const normalized = requireNormalizedText(value, label);
3121
633
  if (["__proto__", "prototype", "constructor", ".", ".."].includes(normalized)
@@ -3135,26 +647,12 @@ function requireNormalizedText(value, label) {
3135
647
  throw new StorageRecordError(`${label} must be normalized.`);
3136
648
  return normalized;
3137
649
  }
3138
- function requireOptionalNormalizedText(value, label) {
3139
- if (typeof value !== "string" || value.includes("\0")) {
3140
- throw new StorageRecordError(`${label} is invalid.`);
3141
- }
3142
- const normalized = value.trim();
3143
- if (normalized !== value)
3144
- throw new StorageRecordError(`${label} must be normalized.`);
3145
- return normalized;
3146
- }
3147
650
  function requireTimestamp(value, label) {
3148
651
  if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
3149
652
  throw new StorageRecordError(`${label} is invalid.`);
3150
653
  }
3151
654
  return value;
3152
655
  }
3153
- function parseMap(value, parse, label) {
3154
- const records = object(value, label);
3155
- for (const [key, entry] of Object.entries(records))
3156
- parse(entry, key);
3157
- }
3158
656
  function versioned(value, schemaVersion, label) {
3159
657
  assertJsonValue(value, label);
3160
658
  const record = object(value, label);
@@ -3162,15 +660,6 @@ function versioned(value, schemaVersion, label) {
3162
660
  throw new StorageRecordError(`${label} must use schemaVersion ${schemaVersion}.`);
3163
661
  return clone(record);
3164
662
  }
3165
- function identified(value, schemaVersion, key, expected, label) {
3166
- const record = versioned(value, schemaVersion, label);
3167
- if (record[key] !== expected)
3168
- throw new StorageRecordError(`${label} identity is inconsistent: ${expected}.`);
3169
- return record;
3170
- }
3171
- function identifiedChangeSet(value, expectedId) {
3172
- return identified(value, CURRENT_CHANGE_SET_SCHEMA_VERSION, "id", expectedId, "ChangeSet");
3173
- }
3174
663
  function object(value, label) {
3175
664
  if (typeof value !== "object" || value === null || Array.isArray(value))
3176
665
  throw new StorageRecordError(`${label} must be an object.`);
@@ -3207,13 +696,10 @@ function assertJsonValue(value, label, seen = new Set()) {
3207
696
  seen.delete(value);
3208
697
  }
3209
698
  function clone(value) { assertJsonValue(value, "Stored value"); return JSON.parse(JSON.stringify(value)); }
3210
- function optional(value) { return value === undefined ? null : clone(value); }
3211
- function values(records, identity) {
3212
- return Object.values(records).map(clone).sort((left, right) => numericCompare(typeof identity === "function" ? identity(left) : String(left[identity]), typeof identity === "function" ? identity(right) : String(right[identity])));
3213
- }
3214
- function numericCompare(left, right) { return left.localeCompare(right, undefined, { numeric: true }); }
3215
699
  export function pendingWakeupProjection(mailbox) {
3216
- const pending = mailbox === null ? null : pendingLane(mailbox, "normal");
700
+ const pending = mailbox?.processing?.owner.startsWith("leader-steer:") === true
701
+ ? mailbox.processing.batch
702
+ : mailbox?.pending ?? null;
3217
703
  if (mailbox === null || mailbox.target.kind !== "role" || mailbox.target.roleName !== "leader"
3218
704
  || pending === null) {
3219
705
  return null;
@@ -3227,841 +713,3 @@ export function pendingWakeupProjection(mailbox) {
3227
713
  lastRequestedAt: pending.lastQueuedAt
3228
714
  };
3229
715
  }
3230
- function validateMailboxReferences(state, mailbox) {
3231
- if (mailbox.target.kind === "task"
3232
- || mailbox.target.kind === "role"
3233
- || mailbox.target.kind === "role-runtime") {
3234
- const aggregate = state.tasks[mailbox.target.taskId];
3235
- if (aggregate === undefined) {
3236
- throw new StorageRecordError(`WorkMailbox target Task not found: ${mailbox.target.taskId}`);
3237
- }
3238
- if (mailbox.target.kind === "role" && aggregate.roles[mailbox.target.roleName] === undefined) {
3239
- throw new StorageRecordError(`WorkMailbox target Role not found: ${mailbox.target.taskId}/${mailbox.target.roleName}`);
3240
- }
3241
- }
3242
- const refs = [];
3243
- if (mailbox.processing !== null) {
3244
- if (mailbox.processing.executionRef !== undefined)
3245
- refs.push(mailbox.processing.executionRef);
3246
- }
3247
- for (const batch of mailboxBatches(mailbox))
3248
- refs.push(...batch.refs);
3249
- for (const ref of refs) {
3250
- if (!mailboxReferenceExists(state, ref)) {
3251
- const identity = "taskId" in ref ? `${ref.taskId}/${ref.id}` : ref.id;
3252
- throw new StorageRecordError(`WorkMailbox reference does not exist: ${ref.type}/${identity}`);
3253
- }
3254
- }
3255
- }
3256
- function validateCanonicalTaskReferences(state, aggregate) {
3257
- const taskId = aggregate.task.id;
3258
- if (aggregate.task.replacementTaskId !== undefined) {
3259
- const replacement = state.tasks[aggregate.task.replacementTaskId];
3260
- if (replacement === undefined || replacement.task.id === taskId) {
3261
- throw new StorageRecordError(`Replacement Task reference is invalid: ${taskId}/${aggregate.task.replacementTaskId}.`);
3262
- }
3263
- }
3264
- const boundProjects = new Set(aggregate.task.projectBindings.map(({ projectId }) => projectId));
3265
- assertAcyclicWorkItems(aggregate.workItems);
3266
- for (const item of Object.values(aggregate.workItems)) {
3267
- for (const dependencyId of item.dependsOn) {
3268
- if (aggregate.workItems[dependencyId] === undefined) {
3269
- throw new StorageRecordError(`Work Item dependency not found: ${taskId}/${dependencyId}.`);
3270
- }
3271
- }
3272
- if (item.writeProjectIds.some((projectId) => !boundProjects.has(projectId))) {
3273
- throw new StorageRecordError(`Work Item writable Project does not belong to Task: ${taskId}/${item.id}.`);
3274
- }
3275
- const writableProjects = new Set(item.writeProjectIds);
3276
- if (item.baseRefs?.some(({ projectId }) => !boundProjects.has(projectId))) {
3277
- throw new StorageRecordError(`Work Item base-ref Project does not belong to Task: ${taskId}/${item.id}.`);
3278
- }
3279
- if (item.baseRefs?.some(({ projectId }) => !writableProjects.has(projectId))) {
3280
- throw new StorageRecordError(`Work Item base-ref Project must be writable: ${taskId}/${item.id}.`);
3281
- }
3282
- const replacementWorkItemId = item.disposition?.replacementWorkItemId;
3283
- if (replacementWorkItemId !== undefined) {
3284
- if (replacementWorkItemId === item.id) {
3285
- throw new StorageRecordError(`Work Item cannot replace itself: ${taskId}/${item.id}.`);
3286
- }
3287
- const replacement = aggregate.workItems[replacementWorkItemId];
3288
- if (replacement === undefined || replacement.taskId !== taskId) {
3289
- throw new StorageRecordError(`Replacement Work Item must belong to the same Task: ${taskId}/${replacementWorkItemId}.`);
3290
- }
3291
- }
3292
- for (const candidate of item.candidates) {
3293
- assertWorkItemCandidateReferences(aggregate, item, candidate, `Work Item candidate ${item.id}`);
3294
- }
3295
- }
3296
- for (const workspace of Object.values(aggregate.managedWorkspaces)) {
3297
- if (workspace.owner.type === "work-item"
3298
- && aggregate.workItems[workspace.owner.workItemId] === undefined) {
3299
- throw new StorageRecordError(`Managed workspace Work Item not found: ${taskId}/${workspace.owner.workItemId}.`);
3300
- }
3301
- if (workspace.owner.type === "review-round") {
3302
- const round = aggregate.reviewRounds[workspace.owner.reviewRoundId];
3303
- if (round === undefined) {
3304
- throw new StorageRecordError(`Managed workspace ReviewRound is invalid: ${taskId}/${workspace.owner.reviewRoundId}.`);
3305
- }
3306
- }
3307
- }
3308
- for (const [roleName, sessions] of Object.entries(aggregate.roleSessionSets)) {
3309
- if (sessions.inFlight !== null) {
3310
- const run = aggregate.agentRuns[sessions.inFlight.runId];
3311
- if (run === undefined || run.roleName !== roleName
3312
- || sessions.inFlight.receiptId !== agentRunDeliveryReceiptId(run)) {
3313
- throw new StorageRecordError(`Task Role in-flight Run is invalid: ${taskId}/${roleName}.`);
3314
- }
3315
- }
3316
- }
3317
- for (const run of Object.values(aggregate.agentRuns)) {
3318
- if (run.workItemId !== undefined && aggregate.workItems[run.workItemId] === undefined) {
3319
- throw new StorageRecordError(`Agent Run Work Item not found: ${taskId}/${run.id}.`);
3320
- }
3321
- if (run.reviewRoundId !== undefined
3322
- && aggregate.reviewRounds[run.reviewRoundId] === undefined) {
3323
- throw new StorageRecordError(`Agent Run ReviewRound not found: ${taskId}/${run.id}.`);
3324
- }
3325
- assertAgentRunExecutionReferences(aggregate, run);
3326
- }
3327
- for (const message of Object.values(aggregate.messages)) {
3328
- if (message.runId !== undefined && aggregate.agentRuns[message.runId] === undefined) {
3329
- throw new StorageRecordError(`Message Run not found: ${taskId}/${message.id}.`);
3330
- }
3331
- if (message.workItemId !== undefined
3332
- && aggregate.workItems[message.workItemId] === undefined) {
3333
- throw new StorageRecordError(`Message Work Item not found: ${taskId}/${message.id}.`);
3334
- }
3335
- }
3336
- for (const request of Object.values(aggregate.inputRequests)) {
3337
- if (aggregate.agentRuns[request.requester.runId] === undefined) {
3338
- throw new StorageRecordError(`Input requester Run not found: ${taskId}/${request.id}.`);
3339
- }
3340
- for (const reference of request.blockedRefs) {
3341
- const found = reference.type === "run"
3342
- ? aggregate.agentRuns[reference.id]
3343
- : aggregate.workItems[reference.id];
3344
- if (found === undefined) {
3345
- throw new StorageRecordError(`Input blocked ${reference.type} not found: ${taskId}/${request.id}/${reference.id}.`);
3346
- }
3347
- }
3348
- }
3349
- for (const round of Object.values(aggregate.reviewRounds)) {
3350
- if ((round.scope ?? "work-item") === "task") {
3351
- const frozenProjects = round.taskCandidate?.projects ?? [];
3352
- if (frozenProjects.length !== boundProjects.size
3353
- || frozenProjects.some(({ projectId }) => !boundProjects.has(projectId))) {
3354
- throw new StorageRecordError(`Task ReviewRound Projects do not match Task scope: ${round.id}.`);
3355
- }
3356
- }
3357
- else {
3358
- const item = aggregate.workItems[round.workItemId];
3359
- if (item === undefined) {
3360
- throw new StorageRecordError(`ReviewRound references are invalid: ${round.id}.`);
3361
- }
3362
- const candidate = item.candidates.find(({ id }) => id === round.candidateId);
3363
- if (candidate === undefined) {
3364
- throw new StorageRecordError(`ReviewRound Candidate not found: ${round.candidateId}.`);
3365
- }
3366
- assertWorkItemCandidateReferences(aggregate, item, candidate, `ReviewRound candidate ${round.id}`);
3367
- }
3368
- if (round.reviewerRunId !== undefined) {
3369
- const reviewerRun = aggregate.agentRuns[round.reviewerRunId];
3370
- if (reviewerRun === undefined
3371
- || reviewerRun.reviewRoundId !== round.id
3372
- || reviewerRun.purpose !== "review") {
3373
- throw new StorageRecordError(`ReviewRound Reviewer Run is invalid: ${round.id}.`);
3374
- }
3375
- }
3376
- }
3377
- for (const changeSet of Object.values(aggregate.changeSets)) {
3378
- if (aggregate.workItems[changeSet.workItemId] === undefined) {
3379
- throw new StorageRecordError(`ChangeSet Work Item not found: ${changeSet.id}.`);
3380
- }
3381
- if (!aggregate.task.projectBindings.some(({ projectId }) => projectId === changeSet.projectId)) {
3382
- throw new StorageRecordError(`ChangeSet Project does not match Task: ${changeSet.id}.`);
3383
- }
3384
- const evidenceRound = Object.values(aggregate.reviewRounds).find(({ evidenceCommit, reviewBaseCommit }) => evidenceCommit !== undefined && evidenceCommit !== reviewBaseCommit && evidenceCommit === changeSet.headCommit);
3385
- if (evidenceRound !== undefined) {
3386
- throw new StorageRecordError(`ReviewRound evidence commit ${evidenceRound.id}/${changeSet.headCommit} cannot become a ChangeSet.`);
3387
- }
3388
- }
3389
- for (const integration of Object.values(aggregate.integrationAttempts)) {
3390
- if (!boundProjects.has(integration.projectId)) {
3391
- throw new StorageRecordError(`Integration Project does not match Task: ${integration.id}.`);
3392
- }
3393
- for (const changeSetId of integration.changeSetIds) {
3394
- const changeSet = aggregate.changeSets[changeSetId];
3395
- if (changeSet === undefined) {
3396
- throw new StorageRecordError(`Integration ChangeSet not found: ${integration.id}/${changeSetId}.`);
3397
- }
3398
- if (changeSet.projectId !== integration.projectId) {
3399
- throw new StorageRecordError(`Integration ChangeSet belongs to another Project: ${integration.id}/${changeSetId}.`);
3400
- }
3401
- const evidenceRound = Object.values(aggregate.reviewRounds).find(({ evidenceCommit, reviewBaseCommit }) => evidenceCommit !== undefined && evidenceCommit !== reviewBaseCommit && evidenceCommit === changeSet.headCommit);
3402
- if (evidenceRound !== undefined) {
3403
- throw new StorageRecordError(`ReviewRound evidence commit ${evidenceRound.id}/${changeSet.headCommit} cannot become an Integration source.`);
3404
- }
3405
- }
3406
- }
3407
- for (const reference of Object.values(aggregate.publicationReferences)) {
3408
- if (!boundProjects.has(reference.projectId)) {
3409
- throw new StorageRecordError(`Publication reference Project does not match Task: ${reference.id}.`);
3410
- }
3411
- if (reference.supersedes !== undefined) {
3412
- const target = aggregate.publicationReferences[reference.supersedes];
3413
- if (target === undefined) {
3414
- throw new StorageRecordError(`Publication reference supersedes target not found: ${reference.id}/${reference.supersedes}.`);
3415
- }
3416
- if (publicationExternalKey(target) !== publicationExternalKey(reference)) {
3417
- throw new StorageRecordError(`Publication reference supersedes a different external identity: ${reference.id}/${reference.supersedes}.`);
3418
- }
3419
- }
3420
- }
3421
- assertPublicationExternalIdentityUniqueness(state);
3422
- for (const workspace of Object.values(aggregate.managedWorkspaces)) {
3423
- assertManagedWorkspaceReferences(aggregate, workspace, "Managed workspace");
3424
- }
3425
- }
3426
- function assertPublicationExternalIdentityUniqueness(state) {
3427
- const owners = new Map();
3428
- for (const aggregate of Object.values(state.tasks)) {
3429
- for (const reference of Object.values(aggregate.publicationReferences)) {
3430
- const key = publicationExternalKey(reference);
3431
- const owner = owners.get(key);
3432
- if (owner !== undefined && owner !== aggregate.task.id) {
3433
- throw new StorageRecordError(`Publication external identity belongs to multiple Tasks: ${key}.`);
3434
- }
3435
- owners.set(key, aggregate.task.id);
3436
- }
3437
- }
3438
- }
3439
- /**
3440
- * Enforce the owner-specific scope at the storage boundary. The map key and
3441
- * foreign-record checks prevent dangling identities; these checks prevent a
3442
- * valid owner from being paired with another lifecycle's Project scope.
3443
- */
3444
- function assertManagedWorkspaceReferences(aggregate, workspace, label, workItemOverride) {
3445
- const taskId = aggregate.task.id;
3446
- const boundProjects = aggregate.task.projectBindings.map(({ projectId }) => projectId).sort();
3447
- const actualProjects = workspace.entries.map(({ projectId }) => projectId).sort();
3448
- const requireVisibleTaskScope = () => {
3449
- // Project bindings are persisted before the physical workspace is
3450
- // reconciled. During that bounded hand-off a workspace may be missing a
3451
- // newly-bound Project, but it must never expose a Project outside the
3452
- // current Task. Launch preparation closes the temporary subset before
3453
- // the workspace can be used.
3454
- if (actualProjects.some((projectId) => !boundProjects.includes(projectId))) {
3455
- throw new StorageRecordError(`${label} Project scope does not match Task: ${taskId}.`);
3456
- }
3457
- };
3458
- switch (workspace.owner.type) {
3459
- case "task":
3460
- requireVisibleTaskScope();
3461
- if (workspace.entries.some(({ access }) => access !== "write")) {
3462
- throw new StorageRecordError(`${label} Task workspace must be writable: ${taskId}.`);
3463
- }
3464
- return;
3465
- case "work-item": {
3466
- const item = workItemOverride?.id === workspace.owner.workItemId
3467
- ? workItemOverride
3468
- : aggregate.workItems[workspace.owner.workItemId];
3469
- if (item === undefined) {
3470
- throw new StorageRecordError(`${label} WorkItem not found: ${taskId}/${workspace.owner.workItemId}.`);
3471
- }
3472
- requireVisibleTaskScope();
3473
- const writable = workspace.entries
3474
- .filter(({ access }) => access === "write")
3475
- .map(({ projectId }) => projectId)
3476
- .sort();
3477
- const expectedWritable = [...item.writeProjectIds].sort();
3478
- // Write scope expansion is persisted before the physical WorkItem
3479
- // workspace is reconciled. The stored workspace may therefore expose
3480
- // a temporary subset, but it may never grant write access outside the
3481
- // current WorkItem authorization.
3482
- if (writable.some((projectId) => !expectedWritable.includes(projectId))) {
3483
- throw new StorageRecordError(`${label} WorkItem write scope does not match: ${taskId}/${item.id}.`);
3484
- }
3485
- return;
3486
- }
3487
- case "review-round": {
3488
- const round = aggregate.reviewRounds[workspace.owner.reviewRoundId];
3489
- if (round === undefined) {
3490
- throw new StorageRecordError(`${label} ReviewRound not found: ${taskId}/${workspace.owner.reviewRoundId}.`);
3491
- }
3492
- requireVisibleTaskScope();
3493
- if (workspace.entries.some(({ access }) => access !== "write")) {
3494
- throw new StorageRecordError(`${label} ReviewRound workspace must be writable: ${taskId}/${round.id}.`);
3495
- }
3496
- const item = round.workItemId === undefined
3497
- ? undefined
3498
- : aggregate.workItems[round.workItemId];
3499
- const candidate = item?.candidates.find(({ id }) => id === round.candidateId);
3500
- const frozenProjects = round.scope === "task"
3501
- ? round.taskCandidate?.projects
3502
- : candidate?.gitSnapshot?.projects;
3503
- if (frozenProjects !== undefined) {
3504
- const expected = [...frozenProjects]
3505
- .map(({ projectId, commit }) => ({ projectId, commit }))
3506
- .sort((left, right) => left.projectId.localeCompare(right.projectId));
3507
- const actual = workspace.entries
3508
- .map(({ projectId, baseCommit }) => ({ projectId, commit: baseCommit }))
3509
- .sort((left, right) => left.projectId.localeCompare(right.projectId));
3510
- const matches = expected.length === actual.length
3511
- && expected.every((frozen, index) => {
3512
- const reviewEntry = actual[index];
3513
- return reviewEntry?.projectId === frozen.projectId
3514
- && reviewEntry.commit === frozen.commit;
3515
- });
3516
- if (!matches) {
3517
- const provenance = round.scope === "task"
3518
- ? "Task frozen project set"
3519
- : "Candidate frozen commit";
3520
- throw new StorageRecordError(`${label} ReviewRound does not use the ${provenance}: ${taskId}/${round.id}.`);
3521
- }
3522
- }
3523
- return;
3524
- }
3525
- case "integration-attempt": {
3526
- const attempt = aggregate.integrationAttempts[workspace.owner.integrationAttemptId];
3527
- if (attempt === undefined) {
3528
- throw new StorageRecordError(`${label} Integration Attempt not found: ${taskId}/${workspace.owner.integrationAttemptId}.`);
3529
- }
3530
- if (workspace.entries.length !== 1
3531
- || workspace.entries[0].projectId !== attempt.projectId
3532
- || workspace.entries[0].access !== "write") {
3533
- throw new StorageRecordError(`${label} Integration Attempt scope is invalid: ${taskId}/${attempt.id}.`);
3534
- }
3535
- return;
3536
- }
3537
- case "execution-lane": {
3538
- const owner = workspace.owner;
3539
- const laneItem = owner.purpose === "execution"
3540
- ? aggregate.workItems[owner.workItemId ?? ""]
3541
- : undefined;
3542
- const group = owner.purpose === "execution"
3543
- ? (laneItem === undefined
3544
- ? undefined
3545
- : workItemExecutionGroupById(laneItem, owner.executionGroupId))
3546
- : aggregate.reviewRounds[owner.reviewRoundId ?? ""]?.executionGroup;
3547
- if (group === undefined || group.id !== owner.executionGroupId
3548
- || !group.lanes.some(({ id }) => id === owner.executionLaneId)) {
3549
- throw new StorageRecordError(`${label} Execution Lane lineage is invalid: ${taskId}/${owner.executionGroupId}/${owner.executionLaneId}.`);
3550
- }
3551
- requireVisibleTaskScope();
3552
- const writable = workspace.entries
3553
- .filter(({ access }) => access === "write")
3554
- .map(({ projectId }) => projectId)
3555
- .sort();
3556
- const expectedWritable = owner.purpose === "execution"
3557
- ? [...(aggregate.workItems[owner.workItemId ?? ""]?.writeProjectIds ?? [])].sort()
3558
- : boundProjects;
3559
- if (!isDeepStrictEqual(writable, expectedWritable)) {
3560
- throw new StorageRecordError(`${label} Execution Lane write scope does not match: ${taskId}/${owner.executionLaneId}.`);
3561
- }
3562
- return;
3563
- }
3564
- }
3565
- }
3566
- function validIntegrationTransition(before, after) {
3567
- if (before.id !== after.id
3568
- || before.taskId !== after.taskId
3569
- || before.projectId !== after.projectId
3570
- || before.targetRef !== after.targetRef
3571
- || before.expectedHead !== after.expectedHead
3572
- || !isDeepStrictEqual(before.changeSetIds, after.changeSetIds)
3573
- || !isDeepStrictEqual(before.checkCommands, after.checkCommands)
3574
- || before.createdAt !== after.createdAt)
3575
- return false;
3576
- const allowed = {
3577
- running: ["running", "blocked", "validating", "failed"],
3578
- blocked: ["blocked", "validating", "failed"],
3579
- validating: ["validating", "committed", "failed"],
3580
- committed: ["committed", "superseded"],
3581
- superseded: ["superseded"],
3582
- failed: ["failed"]
3583
- };
3584
- return allowed[before.status].includes(after.status);
3585
- }
3586
- /**
3587
- * Storage-level defence in depth for the integration queue state machine: the
3588
- * identity fields and the check/evidence lists are immutable once written, and
3589
- * a status may only move along the queue's legal transitions. The service
3590
- * owns the CAS claim; this rejects a stale or forged write that slipped past it.
3591
- */
3592
- function validIntegrationQueueTransition(before, after) {
3593
- if (before.id !== after.id
3594
- || before.taskId !== after.taskId
3595
- || before.projectId !== after.projectId
3596
- || before.changeSetId !== after.changeSetId
3597
- || before.targetRef !== after.targetRef
3598
- || !isDeepStrictEqual(before.checkCommands, after.checkCommands)
3599
- || !isDeepStrictEqual(before.evidenceRefs, after.evidenceRefs)
3600
- || before.createdAt !== after.createdAt)
3601
- return false;
3602
- const allowed = {
3603
- queued: ["queued", "running", "validated", "superseded"],
3604
- running: ["running", "conflicted", "committed"],
3605
- conflicted: ["conflicted", "running", "committed", "queued", "superseded"],
3606
- validated: ["validated", "running", "queued", "superseded"],
3607
- committed: ["committed"],
3608
- superseded: ["superseded"]
3609
- };
3610
- return allowed[before.status].includes(after.status);
3611
- }
3612
- function assertAcyclicWorkItems(items) {
3613
- const visiting = new Set();
3614
- const visited = new Set();
3615
- const visit = (id) => {
3616
- if (visited.has(id))
3617
- return;
3618
- if (visiting.has(id))
3619
- throw new StorageRecordError(`Work Item dependency cycle detected: ${id}.`);
3620
- visiting.add(id);
3621
- const item = items[id];
3622
- if (item !== undefined) {
3623
- for (const dependencyId of item.dependsOn)
3624
- visit(dependencyId);
3625
- }
3626
- visiting.delete(id);
3627
- visited.add(id);
3628
- };
3629
- for (const id of Object.keys(items))
3630
- visit(id);
3631
- }
3632
- function mailboxReferenceExists(state, ref) {
3633
- if ("taskId" in ref) {
3634
- const aggregate = state.tasks[ref.taskId];
3635
- if (aggregate === undefined)
3636
- return false;
3637
- switch (ref.type) {
3638
- case "run": return aggregate.agentRuns[ref.id] !== undefined;
3639
- case "work-item": return aggregate.workItems[ref.id] !== undefined;
3640
- case "input": return aggregate.inputRequests[ref.id] !== undefined;
3641
- case "message": return aggregate.messages[ref.id] !== undefined;
3642
- case "event": return aggregate.events[ref.id] !== undefined;
3643
- }
3644
- }
3645
- switch (ref.type) {
3646
- case "task": return state.tasks[ref.id] !== undefined;
3647
- case "session":
3648
- return [
3649
- ...Object.values(state.globalRoleSessionSets),
3650
- ...Object.values(state.tasks).flatMap((task) => Object.values(task.roleSessionSets))
3651
- ].some((set) => Object.values(set.sessions).some((session) => session.nativeSessionId === ref.id));
3652
- default: return false;
3653
- }
3654
- }
3655
- function validWorkItemTransition(existing, candidate) {
3656
- if (isDeepStrictEqual(existing, candidate))
3657
- return true;
3658
- if (existing.id !== candidate.id
3659
- || existing.taskId !== candidate.taskId
3660
- || existing.assignee !== candidate.assignee
3661
- || existing.createdAt !== candidate.createdAt
3662
- || !isDeepStrictEqual(existing.baseRefs, candidate.baseRefs)
3663
- || candidate.revision !== existing.revision + 1
3664
- || Date.parse(candidate.updatedAt) < Date.parse(existing.updatedAt))
3665
- return false;
3666
- if (!compatibleWorkItemExecutionGroups(existing, candidate))
3667
- return false;
3668
- if (existing.status !== candidate.status
3669
- && (existing.title !== candidate.title
3670
- || existing.objective !== candidate.objective
3671
- || !isDeepStrictEqual(existing.acceptance, candidate.acceptance)
3672
- || !isDeepStrictEqual(existing.dependsOn, candidate.dependsOn)
3673
- || !isDeepStrictEqual(existing.writeProjectIds, candidate.writeProjectIds)))
3674
- return false;
3675
- const candidateProjects = new Set(candidate.writeProjectIds);
3676
- if (existing.writeProjectIds.some((projectId) => !candidateProjects.has(projectId))) {
3677
- return false;
3678
- }
3679
- const candidatesChanged = !isDeepStrictEqual(existing.candidates, candidate.candidates);
3680
- const submittedCandidate = existing.status === "running"
3681
- && candidate.status === "awaiting_acceptance"
3682
- && candidate.candidates.length === existing.candidates.length + 1
3683
- && isDeepStrictEqual(candidate.candidates.slice(0, existing.candidates.length), existing.candidates);
3684
- if (candidatesChanged && !submittedCandidate) {
3685
- return false;
3686
- }
3687
- const allowed = {
3688
- pending: ["pending", "running", "retired"],
3689
- running: [
3690
- "running",
3691
- "awaiting_acceptance",
3692
- "completed",
3693
- "failed",
3694
- "retired"
3695
- ],
3696
- awaiting_acceptance: [
3697
- "awaiting_acceptance",
3698
- "completed",
3699
- "failed",
3700
- "retired"
3701
- ],
3702
- completed: ["completed"],
3703
- failed: ["failed", "running", "retired"],
3704
- retired: ["retired"]
3705
- };
3706
- return allowed[existing.status].includes(candidate.status);
3707
- }
3708
- function compatibleWorkItemExecutionGroups(existing, candidate) {
3709
- if (candidate.executionGroups.length < existing.executionGroups.length)
3710
- return false;
3711
- for (const [index, historical] of existing.executionGroups.entries()) {
3712
- const next = candidate.executionGroups[index];
3713
- if (next === undefined || next.id !== historical.id)
3714
- return false;
3715
- const isCurrent = existing.currentExecutionGroupId === historical.id;
3716
- const mutableCurrent = isCurrent && historical.resolution === undefined;
3717
- if (!mutableCurrent && !isDeepStrictEqual(historical, next))
3718
- return false;
3719
- if (mutableCurrent && !compatibleExecutionGroups(historical, next))
3720
- return false;
3721
- }
3722
- const appended = candidate.executionGroups.slice(existing.executionGroups.length);
3723
- if (appended.length > 1)
3724
- return false;
3725
- if (appended.length === 1) {
3726
- const priorCurrent = existing.currentExecutionGroupId === undefined
3727
- ? undefined
3728
- : workItemExecutionGroupById(existing, existing.currentExecutionGroupId);
3729
- if (priorCurrent !== undefined && priorCurrent.resolution === undefined)
3730
- return false;
3731
- if (candidate.currentExecutionGroupId !== appended[0].id)
3732
- return false;
3733
- }
3734
- else if (candidate.currentExecutionGroupId !== existing.currentExecutionGroupId) {
3735
- const priorCurrent = existing.currentExecutionGroupId === undefined
3736
- ? undefined
3737
- : workItemExecutionGroupById(existing, existing.currentExecutionGroupId);
3738
- const clearingResolvedRetry = existing.status === "failed"
3739
- && candidate.status === "running"
3740
- && candidate.currentExecutionGroupId === undefined
3741
- && priorCurrent?.resolution !== undefined;
3742
- if (!clearingResolvedRetry)
3743
- return false;
3744
- }
3745
- if (candidate.currentExecutionGroupId !== undefined
3746
- && workItemExecutionGroupById(candidate, candidate.currentExecutionGroupId) === undefined) {
3747
- return false;
3748
- }
3749
- return true;
3750
- }
3751
- function assertWorkItemCandidateReferences(aggregate, item, candidate, label) {
3752
- if (candidate.workItemRevision > item.revision) {
3753
- throw new StorageRecordError(`${label} revision is invalid.`);
3754
- }
3755
- if ((candidate.executionGroupId === undefined) !== (candidate.executionLaneId === undefined)) {
3756
- throw new StorageRecordError(`${label} execution lineage is incomplete.`);
3757
- }
3758
- if (candidate.executionGroupId !== undefined) {
3759
- const group = workItemExecutionGroupById(item, candidate.executionGroupId);
3760
- const lane = group?.lanes.find(({ id }) => id === candidate.executionLaneId);
3761
- if (group === undefined
3762
- || group.id !== candidate.executionGroupId
3763
- || lane === undefined) {
3764
- throw new StorageRecordError(`${label} execution lineage is invalid: candidate=${candidate.executionGroupId}/${candidate.executionLaneId}; `
3765
- + `item=${group?.id ?? "none"}/${lane?.id ?? "none"}.`);
3766
- }
3767
- }
3768
- if (candidate.workspace !== undefined) {
3769
- if (candidate.workspace.owner.taskId !== item.taskId) {
3770
- throw new StorageRecordError(`${label} workspace belongs to another Task.`);
3771
- }
3772
- if (candidate.workspace.owner.type === "work-item"
3773
- && candidate.workspace.owner.workItemId !== item.id) {
3774
- throw new StorageRecordError(`${label} workspace belongs to another Work Item.`);
3775
- }
3776
- if (candidate.workspace.owner.type !== "work-item") {
3777
- throw new StorageRecordError(`${label} must use the WorkItem-owned Develop workspace.`);
3778
- }
3779
- assertManagedWorkspaceReferences(aggregate, candidate.workspace, label, item);
3780
- }
3781
- if (candidate.source.type === "direct") {
3782
- if (item.assignee !== undefined) {
3783
- throw new StorageRecordError(`${label} cannot be direct for an assigned Work Item.`);
3784
- }
3785
- if (candidate.workspace !== undefined
3786
- && (candidate.workspace.owner.type !== "work-item"
3787
- || candidate.workspace.owner.taskId !== item.taskId
3788
- || candidate.workspace.owner.workItemId !== item.id)) {
3789
- throw new StorageRecordError(`${label} must use the WorkItem-owned Develop workspace.`);
3790
- }
3791
- if (candidate.taskMainSnapshot !== undefined) {
3792
- const expectedProjects = [...item.writeProjectIds].sort();
3793
- const actualProjects = candidate.taskMainSnapshot.projects
3794
- .map(({ projectId }) => projectId)
3795
- .sort();
3796
- if (!isDeepStrictEqual(actualProjects, expectedProjects)) {
3797
- throw new StorageRecordError(`${label} Task-main snapshot scope is stale.`);
3798
- }
3799
- for (const project of candidate.taskMainSnapshot.projects) {
3800
- const binding = aggregate.task.projectBindings.find(({ projectId }) => projectId === project.projectId);
3801
- if (binding === undefined || binding.directory !== project.directory) {
3802
- throw new StorageRecordError(`${label} Task-main snapshot Project is not bound: ${project.projectId}.`);
3803
- }
3804
- }
3805
- }
3806
- return;
3807
- }
3808
- const run = aggregate.agentRuns[candidate.source.runId];
3809
- const resolvedGroupSummary = candidate.executionGroupId === undefined
3810
- ? undefined
3811
- : workItemExecutionGroupById(item, candidate.executionGroupId)?.resolution?.summary;
3812
- if (run === undefined
3813
- || run.workItemId !== item.id
3814
- || run.purpose !== "execution"
3815
- || run.status !== "yielded"
3816
- || (run.summary !== candidate.summary && resolvedGroupSummary !== candidate.summary)) {
3817
- throw new StorageRecordError(`${label} Run is invalid: ${candidate.source.runId}.`);
3818
- }
3819
- if (candidate.executionGroupId !== run.executionGroupId
3820
- || candidate.executionLaneId !== run.executionLaneId) {
3821
- throw new StorageRecordError(`${label} execution lineage does not match its source Run.`);
3822
- }
3823
- // A Gitless execution Run still carries its durable Task-owned empty view
3824
- // for runtime fencing, while its Candidate intentionally has no Develop
3825
- // workspace or Git snapshot. This is the only source/run workspace
3826
- // mismatch permitted at the storage boundary.
3827
- const gitlessRunWorkspace = run.workspace?.owner.type === "task"
3828
- && run.workspace.owner.taskId === item.taskId
3829
- && aggregate.task.projectBindings.length === 0
3830
- && run.workspace.entries.length === 0
3831
- && (() => {
3832
- const durable = aggregate.managedWorkspaces[managedWorkspaceKey(run.workspace.owner)];
3833
- return durable !== undefined && isDeepStrictEqual(durable, run.workspace);
3834
- })();
3835
- if (!gitlessRunWorkspace
3836
- && (candidate.workspace === undefined) !== (run.workspace === undefined)) {
3837
- throw new StorageRecordError(`${label} workspace does not match its source Run.`);
3838
- }
3839
- if (!gitlessRunWorkspace && candidate.workspace !== undefined && run.workspace !== undefined) {
3840
- assertCandidateWorkspaceMatchesRun(candidate.workspace, run.workspace, label);
3841
- }
3842
- if (candidate.workspace !== undefined && (candidate.workspace.owner.type !== "work-item"
3843
- || candidate.workspace.owner.taskId !== item.taskId
3844
- || candidate.workspace.owner.workItemId !== item.id)) {
3845
- throw new StorageRecordError(`${label} must use the WorkItem-owned Develop workspace.`);
3846
- }
3847
- }
3848
- function assertAgentRunExecutionReferences(aggregate, run) {
3849
- if ((run.executionGroupId === undefined) !== (run.executionLaneId === undefined)) {
3850
- throw new StorageRecordError(`Agent Run execution lineage is incomplete: ${run.id}.`);
3851
- }
3852
- if (run.executionGroupId === undefined)
3853
- return;
3854
- const ownerGroup = run.purpose === "review"
3855
- ? (run.reviewRoundId === undefined
3856
- ? undefined
3857
- : aggregate.reviewRounds[run.reviewRoundId]?.executionGroup)
3858
- : (run.workItemId === undefined || run.executionGroupId === undefined
3859
- ? undefined
3860
- : (() => {
3861
- const item = aggregate.workItems[run.workItemId];
3862
- return item === undefined
3863
- ? undefined
3864
- : workItemExecutionGroupById(item, run.executionGroupId);
3865
- })());
3866
- if (ownerGroup === undefined) {
3867
- throw new StorageRecordError(`Agent Run ExecutionGroup not found: ${run.id}.`);
3868
- }
3869
- validateExecutionGroup(ownerGroup);
3870
- const lane = ownerGroup.lanes.find(({ id }) => id === run.executionLaneId);
3871
- if (ownerGroup.id !== run.executionGroupId || lane === undefined) {
3872
- throw new StorageRecordError(`Agent Run ExecutionLane does not match its owner: ${run.id}.`);
3873
- }
3874
- if (lane.roleName !== run.roleName) {
3875
- throw new StorageRecordError(`Agent Run Role does not match its ExecutionLane: ${run.id}.`);
3876
- }
3877
- }
3878
- function compatibleExecutionGroups(existing, candidate) {
3879
- if (existing === undefined)
3880
- return true;
3881
- if (candidate === undefined)
3882
- return false;
3883
- return isExecutionGroupTransition(existing, candidate);
3884
- }
3885
- /** Candidate freezes Git commits at yield time, so timestamp/baseCommit fields
3886
- * may differ from the Run's dispatch snapshot while workspace identity and
3887
- * execution scope must remain exact. */
3888
- function assertCandidateWorkspaceMatchesRun(candidate, run, label) {
3889
- if (candidate.owner.type === "work-item" && run.owner.type === "execution-lane") {
3890
- if (candidate.owner.taskId !== run.owner.taskId
3891
- || run.owner.purpose !== "execution"
3892
- || candidate.owner.workItemId !== run.owner.workItemId
3893
- || candidate.entries.length !== run.entries.length) {
3894
- throw new StorageRecordError(`${label} workspace lineage does not match its source Lane.`);
3895
- }
3896
- for (const source of run.entries) {
3897
- const target = candidate.entries.find(({ projectId }) => projectId === source.projectId);
3898
- if (target === undefined || target.directory !== source.directory || target.access !== source.access) {
3899
- throw new StorageRecordError(`${label} workspace Project scope does not match its source Lane.`);
3900
- }
3901
- }
3902
- return;
3903
- }
3904
- if (candidate.owner.type !== run.owner.type
3905
- || candidate.owner.taskId !== run.owner.taskId
3906
- || candidate.root !== run.root
3907
- || candidate.entries.length !== run.entries.length) {
3908
- throw new StorageRecordError(`${label} workspace does not match its source Run.`);
3909
- }
3910
- for (const source of run.entries) {
3911
- const frozen = candidate.entries.find(({ projectId }) => projectId === source.projectId);
3912
- if (frozen === undefined
3913
- || frozen.directory !== source.directory
3914
- || frozen.access !== source.access
3915
- || frozen.path !== source.path
3916
- || frozen.branch !== source.branch
3917
- || frozen.baseRef !== source.baseRef) {
3918
- throw new StorageRecordError(`${label} workspace scope does not match its source Run.`);
3919
- }
3920
- }
3921
- }
3922
- function validReviewRoundTransition(existing, candidate) {
3923
- if (isDeepStrictEqual(existing, candidate))
3924
- return true;
3925
- if (existing.id !== candidate.id
3926
- || existing.taskId !== candidate.taskId
3927
- || existing.workItemId !== candidate.workItemId
3928
- || existing.candidateId !== candidate.candidateId
3929
- || existing.reviewerRoleName !== candidate.reviewerRoleName
3930
- || existing.reviewBaseCommit !== candidate.reviewBaseCommit
3931
- || (existing.scope ?? "work-item") !== (candidate.scope ?? "work-item")
3932
- || !isDeepStrictEqual(existing.taskCandidate, candidate.taskCandidate)
3933
- || !sameTaskFinalReviewContract(existing.taskFinalReviewContract, candidate.taskFinalReviewContract)
3934
- || !compatibleExecutionGroups(existing.executionGroup, candidate.executionGroup)
3935
- // Issue 06: an explicit Task-control retry resets a failed Task-final
3936
- // Round to pending, so requestedBy may change from the original actor.
3937
- || (existing.requestedBy !== candidate.requestedBy
3938
- && !(existing.status === "failed"
3939
- && candidate.status === "pending"
3940
- && (candidate.scope ?? "work-item") === "task"))
3941
- || existing.createdAt !== candidate.createdAt)
3942
- return false;
3943
- if (existing.status === "pending") {
3944
- if (candidate.status === "pending") {
3945
- return (existing.workspace === undefined
3946
- && candidate.workspace !== undefined
3947
- || existing.executionGroup === undefined
3948
- && candidate.executionGroup !== undefined
3949
- || existing.executionGroup !== undefined
3950
- && candidate.executionGroup !== undefined
3951
- && !isDeepStrictEqual(existing.executionGroup, candidate.executionGroup))
3952
- && candidate.reviewerRunId === undefined
3953
- && candidate.summary === undefined
3954
- && candidate.checks === undefined
3955
- && candidate.evidenceCommit === undefined
3956
- && candidate.endedAt === undefined
3957
- && candidate.workspaceDisposition === undefined;
3958
- }
3959
- return ["running", "failed"].includes(candidate.status)
3960
- && (existing.workspace === undefined
3961
- || isDeepStrictEqual(existing.workspace, candidate.workspace));
3962
- }
3963
- if (existing.status === "running") {
3964
- if (candidate.status === "running") {
3965
- return existing.reviewerRunId === candidate.reviewerRunId
3966
- && isDeepStrictEqual(existing.workspace, candidate.workspace)
3967
- && existing.executionGroup !== undefined
3968
- && candidate.executionGroup !== undefined
3969
- && !isDeepStrictEqual(existing.executionGroup, candidate.executionGroup)
3970
- && candidate.summary === undefined
3971
- && candidate.report === undefined
3972
- && candidate.checks === undefined
3973
- && candidate.evidenceCommit === undefined
3974
- && candidate.endedAt === undefined
3975
- && candidate.workspaceDisposition === undefined;
3976
- }
3977
- return ["completed", "failed"].includes(candidate.status)
3978
- && existing.reviewerRunId === candidate.reviewerRunId
3979
- && isDeepStrictEqual(existing.workspace, candidate.workspace);
3980
- }
3981
- // Issue 06: a failed Task-final execution attempt may be reset to pending
3982
- // under the same semantic Round ID. AgentRun history remains the attempt
3983
- // trail; terminal Review metadata is cleared by retryTaskReviewRound.
3984
- if (existing.status === "failed"
3985
- && candidate.status === "pending"
3986
- && (candidate.scope ?? "work-item") === "task") {
3987
- return candidate.reviewerRunId === undefined
3988
- && candidate.summary === undefined
3989
- && candidate.report === undefined
3990
- && candidate.checks === undefined
3991
- && candidate.evidenceCommit === undefined
3992
- && candidate.endedAt === undefined
3993
- && candidate.workspaceDisposition === undefined
3994
- && (existing.workspace === undefined
3995
- || candidate.workspace === undefined
3996
- || isDeepStrictEqual(existing.workspace, candidate.workspace))
3997
- && compatibleExecutionGroups(existing.executionGroup, candidate.executionGroup);
3998
- }
3999
- if (existing.status === candidate.status
4000
- && (existing.status === "completed" || existing.status === "failed")) {
4001
- const { workspaceDisposition: _existingDisposition, ...existingResult } = existing;
4002
- const { workspaceDisposition: _candidateDisposition, ...candidateResult } = candidate;
4003
- return isDeepStrictEqual(existingResult, candidateResult)
4004
- && existing.workspaceDisposition?.kind !== "removed"
4005
- && candidate.workspaceDisposition !== undefined;
4006
- }
4007
- return false;
4008
- }
4009
- function synchronousResult(value) { if (typeof value === "object" && value !== null && "then" in value)
4010
- throw new StorageRecordError("FileTaskStore transactions must be synchronous."); return value; }
4011
- function acquireStorageLock(rootDir) {
4012
- ensureYuiHome(rootDir);
4013
- const lock = join(rootDir, STORAGE_LOCK_DIRECTORY);
4014
- const deadline = Date.now() + LOCK_TIMEOUT_MS;
4015
- while (true) {
4016
- try {
4017
- mkdirSync(lock, { mode: 0o700 });
4018
- writeFileSync(join(lock, "owner"), `${process.pid}\n`, { mode: 0o600 });
4019
- return () => { rmSync(lock, { recursive: true, force: true }); };
4020
- }
4021
- catch (error) {
4022
- if (!(error instanceof Error && "code" in error && error.code === "EEXIST"))
4023
- throw error;
4024
- reclaimDeadLock(lock);
4025
- if (Date.now() >= deadline)
4026
- throw new StorageConflictError(`Timed out waiting for storage lock: ${lock}`);
4027
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_RETRY_MS);
4028
- }
4029
- }
4030
- }
4031
- /**
4032
- * Run `execute` while holding the same storage write lock the store uses, without
4033
- * the version-gated {@link FileTaskStore} constructor. The upgrade orchestrator
4034
- * uses this to re-pin the committed revision under the lock against a source Home
4035
- * whose schema is not the current version (so a store cannot be constructed yet).
4036
- */
4037
- export function withStorageWriteLock(rootDir, execute) {
4038
- const release = acquireStorageLock(rootDir);
4039
- try {
4040
- return execute();
4041
- }
4042
- finally {
4043
- release();
4044
- }
4045
- }
4046
- function reclaimDeadLock(lock) {
4047
- try {
4048
- const age = Date.now() - statSync(lock).mtimeMs;
4049
- if (age < 1_000)
4050
- return;
4051
- const pid = Number.parseInt(readFileSync(join(lock, "owner"), "utf8"), 10);
4052
- if (Number.isInteger(pid) && processIsAlive(pid))
4053
- return;
4054
- rmSync(lock, { recursive: true, force: true });
4055
- }
4056
- catch (error) {
4057
- if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
4058
- return;
4059
- }
4060
- }
4061
- function processIsAlive(pid) { try {
4062
- process.kill(pid, 0);
4063
- return true;
4064
- }
4065
- catch (error) {
4066
- return error instanceof Error && "code" in error && error.code === "EPERM";
4067
- } }