@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.
- package/ARCHITECTURE.md +69 -428
- package/README.md +108 -298
- package/dist/agent/managedRuntimeEnvironment.js +2 -2
- package/dist/cli/commandCatalog.js +39 -77
- package/dist/cli/interactionCandidates.js +4 -4
- package/dist/cli/interactionPolicy.js +7 -10
- package/dist/cli/invocationRouter.js +1 -1
- package/dist/cli/updateCommand.js +6 -27
- package/dist/cli/updateOrchestrator.js +63 -484
- package/dist/cli/updatePorts.js +15 -333
- package/dist/cli/upgradeCommand.js +8 -65
- package/dist/cli.js +57 -242
- package/dist/commands/configCommands.js +11 -62
- package/dist/commands/executionAuditCommands.js +23 -23
- package/dist/commands/globalRoleCommands.js +2 -7
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/projectCommands.js +5 -5
- package/dist/commands/releaseCommands.js +3 -10
- package/dist/commands/resourcesCommands.js +2 -2
- package/dist/commands/sessionCommands.js +2 -2
- package/dist/commands/taskActor.js +31 -39
- package/dist/commands/taskChangeSetCommands.js +5 -12
- package/dist/commands/taskCommands.js +464 -1249
- package/dist/commands/taskContextCommand.js +35 -36
- package/dist/commands/taskExecutionCommands.js +33 -46
- package/dist/commands/taskInputCommands.js +20 -42
- package/dist/commands/taskNextActionCommand.js +9 -9
- package/dist/commands/taskOverviewCommand.js +56 -95
- package/dist/commands/taskRoleRuntimeStatus.js +77 -107
- package/dist/commands/taskUpstreamCommands.js +3 -3
- package/dist/commands/taskWorkspaceCommands.js +7 -79
- package/dist/commands/telemetryCommands.js +32 -124
- package/dist/commands/workflowCommands.js +3 -3
- package/dist/completion/fileCompletionManager.js +1 -1
- package/dist/config/configCatalog.js +4 -6
- package/dist/config/yuiConfig.js +6 -43
- package/dist/context/dispatchContext.js +14 -30
- package/dist/context/roleSessionContext.js +2 -2
- package/dist/context/sessionBootstrapManifest.js +2 -2
- package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
- package/dist/context/turnInputContract.js +204 -0
- package/dist/context/wakeNotification.js +2 -2
- package/dist/controller/agentRuntimeObserver.js +18 -18
- package/dist/controller/clientRuntime.js +13 -20
- package/dist/controller/controller.js +89 -116
- package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
- package/dist/controller/jobControl.js +17 -17
- package/dist/controller/resourceInventory.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +38 -59
- package/dist/controller/resourceInventoryRpc.js +3 -3
- package/dist/controller/resourceInventoryWorker.js +1 -1
- package/dist/controller/runtime.js +46 -53
- package/dist/controller/runtimeEventInbox.js +23 -31
- package/dist/controller/runtimeEventProcessor.js +15 -15
- package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
- package/dist/controller/runtimeLaunchCoordinator.js +3 -3
- package/dist/controller/runtimeObservationHook.js +11 -8
- package/dist/controller/sessionNotify.js +11 -11
- package/dist/controller/structuredProviderObservation.js +89 -16
- package/dist/coordination/workMailbox.js +309 -579
- package/dist/coordination/workMailboxQueue.js +5 -6
- package/dist/core/controllerClient.js +1 -91
- package/dist/core/controllerServer.js +3 -6
- package/dist/doctor/doctor.js +51 -314
- package/dist/execution/candidateConvergence.js +3 -3
- package/dist/execution/executionGroup.js +32 -33
- package/dist/execution/executionHealth.js +36 -38
- package/dist/execution/resourceBroker.js +12 -12
- package/dist/executor/agentExecutor.js +30 -265
- package/dist/executor/effectiveLaunch.js +9 -19
- package/dist/executor/executorRegistry.js +43 -17
- package/dist/executor/fileRoleLaunchPlanner.js +56 -56
- package/dist/executor/launchPlan.js +3 -3
- package/dist/executor/workspacePreflightClassification.js +16 -16
- package/dist/grant/capabilityGrant.js +20 -25
- package/dist/input/inputRequest.js +12 -12
- package/dist/integration/changeSet.js +9 -13
- package/dist/integration/changeSetManifest.js +1 -1
- package/dist/integration/gitIntegrationService.js +2 -2
- package/dist/integration/integrationQueueService.js +5 -21
- package/dist/integration/overlapDiagnostics.js +2 -2
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/job/jobRunner.js +1 -1
- package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
- package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
- package/dist/message/message.js +6 -6
- package/dist/observability/executionAudit.js +74 -86
- package/dist/observability/faultClassification.js +7 -24
- package/dist/observability/orchestrationMetrics.js +5 -5
- package/dist/observability/runtimeIdentity.js +34 -101
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +3 -3
- package/dist/release/releaseWorkflowEngine.js +6 -10
- package/dist/release/releaseWorkflowPorts.js +2 -2
- package/dist/release/workflowFileLock.js +1 -1
- package/dist/repository/checkoutSwap.js +1 -1
- package/dist/repository/gitWorkspace.js +0 -13
- package/dist/repository/project.js +1 -1
- package/dist/repository/projectMaintenanceLock.js +1 -2
- package/dist/repository/taskBaseFreshness.js +13 -13
- package/dist/repository/taskWorkspaceCoordinator.js +21 -25
- package/dist/repository/taskWorkspaceIdentity.js +8 -21
- package/dist/repository/taskWorkspacePreparer.js +63 -620
- package/dist/resources/autoResourceGc.js +3 -3
- package/dist/resources/liveReferences.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/reviewDecision.js +7 -7
- package/dist/review/reviewFindingLedger.js +1 -1
- package/dist/review/reviewOutcomeClassifier.js +35 -50
- package/dist/review/reviewRound.js +14 -25
- package/dist/review/reviewerAvailability.js +4 -4
- package/dist/review/taskFinalReviewContractResolution.js +41 -0
- package/dist/role/role.js +6 -3
- package/dist/runtime/agentDriver.js +4 -0
- package/dist/runtime/agentHost.js +151 -36
- package/dist/runtime/builtinAgentDrivers.js +79 -2
- package/dist/runtime/codexAppServerRuntime.js +122 -0
- package/dist/runtime/continuationManager.js +3 -5
- package/dist/runtime/exactControlPlane.js +17 -42
- package/dist/runtime/firstProgressAdvisory.js +1 -1
- package/dist/runtime/index.js +2 -2
- package/dist/runtime/launchDiagnostics.js +2 -2
- package/dist/runtime/lifecycleReservation.js +4 -4
- package/dist/runtime/processExitObservation.js +1 -1
- package/dist/runtime/processExitOutbox.js +0 -32
- package/dist/runtime/promptEnvelope.js +9 -9
- package/dist/runtime/providerContinuation.js +2 -2
- package/dist/runtime/providerContinuationReconciliationService.js +2 -2
- package/dist/runtime/providerRuntimeIdentity.js +76 -24
- package/dist/runtime/recentTurnIds.js +38 -0
- package/dist/runtime/runtimeContinuationProjection.js +2 -2
- package/dist/runtime/runtimeHealthPolicy.js +1 -1
- package/dist/runtime/runtimeObservation.js +58 -21
- package/dist/runtime/runtimeProjection.js +19 -27
- package/dist/runtime/sessionLaunchRequest.js +2 -2
- package/dist/runtime/structuredProviderHost.js +177 -12
- package/dist/runtime/tmuxAdapters.js +46 -11
- package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
- package/dist/scheduler/leaderWakeupProcessor.js +147 -445
- package/dist/scheduler/operatorEvent.js +4 -6
- package/dist/scheduler/resourceQueueProjection.js +71 -0
- package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
- package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
- package/dist/scheduler/taskExecutionProjection.js +99 -99
- package/dist/scheduler/taskObservabilityProjection.js +10 -10
- package/dist/scheduler/taskWake.js +4 -4
- package/dist/scheduler/wakeReason.js +6 -10
- package/dist/scheduler/wakeupQueue.js +0 -9
- package/dist/setup/setupCommand.js +2 -2
- package/dist/storage/currentTaskStore.js +46 -0
- package/dist/storage/persistenceWorker.js +1 -1
- package/dist/storage/sqliteSchema.js +81 -485
- package/dist/storage/sqliteStore.js +206 -365
- package/dist/storage/storageSchema.js +1 -15
- package/dist/storage/storageVersions.js +8 -9
- package/dist/storage/storeRpc.js +10 -34
- package/dist/storage/taskStore.js +63 -3415
- package/dist/storage/upgrade/recordVersions.js +55 -149
- package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
- package/dist/task/completionReadiness.js +5 -5
- package/dist/task/deliveryGuard.js +10 -10
- package/dist/task/nextAction.js +74 -77
- package/dist/task/task.js +2 -4
- package/dist/task/taskRecordReference.js +3 -3
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/telemetry/sqliteTelemetryStore.js +53 -53
- package/dist/telemetry/telemetryConfig.js +14 -14
- package/dist/telemetry/telemetryWiring.js +8 -14
- package/dist/turn/turn.js +356 -0
- package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
- package/dist/verification/gateArtifactStore.js +2 -2
- package/dist/web/assets/client/app.js +1 -1
- package/dist/web/assets/client/components.js +29 -54
- package/dist/web/assets/client/i18n.js +17 -37
- package/dist/web/assets/client/view.js +37 -37
- package/dist/web/assets/shell.js +1 -1
- package/dist/web/assets/styles/cards.js +5 -5
- package/dist/web/assets/styles/widgets.js +1 -2
- package/dist/web/webSnapshot.js +65 -60
- package/dist/workItem/workItem.js +11 -11
- package/dist/workspace/workItemChangeSetManager.js +1 -1
- package/docs/task-local-identity.md +5 -5
- package/i18n/README.zh-CN.md +41 -54
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +85 -83
- package/skills/yui-operator/SKILL.md +16 -16
- package/skills/yui-reviewer/SKILL.md +10 -10
- package/skills/yui-runtime/SKILL.md +17 -18
- package/skills/yui-worker/SKILL.md +16 -18
- package/dist/context/runContextContract.js +0 -162
- package/dist/executor/turnCompletion.js +0 -126
- package/dist/review/taskFinalReviewContractEvent.js +0 -1
- package/dist/review/taskFinalReviewContractRebind.js +0 -367
- package/dist/run/agentRun.js +0 -356
- package/dist/run/rejectedYieldAttempt.js +0 -221
- package/dist/run/runControlRequest.js +0 -50
- package/dist/run/yieldReceipt.js +0 -65
- package/dist/runtime/turnCompletion.js +0 -3
- package/dist/scheduler/actionability.js +0 -323
- package/dist/scheduler/activeRoleRunDelivery.js +0 -868
- package/dist/scheduler/activeTaskProgress.js +0 -126
- package/dist/storage/compatibleTaskStore.js +0 -248
- package/dist/storage/migration/baseline.js +0 -78
- package/dist/storage/migration/classifier.js +0 -51
- package/dist/storage/migration/compatibleCodec.js +0 -53
- package/dist/storage/migration/engine.js +0 -147
- package/dist/storage/migration/index.js +0 -33
- package/dist/storage/migration/planner.js +0 -154
- package/dist/storage/migration/productionRegistry.js +0 -3931
- package/dist/storage/migration/registry.js +0 -169
- package/dist/storage/migration/report.js +0 -54
- package/dist/storage/migration/types.js +0 -31
- package/dist/storage/upgrade/homeClassification.js +0 -353
- package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
- package/dist/storage/upgrade/migrationReceipt.js +0 -67
- package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
- package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
- package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
- package/dist/storage/upgrade/recordVersionScan.js +0 -176
- package/dist/storage/upgrade/sqliteFileSet.js +0 -50
- package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
- package/dist/storage/upgrade/sqliteStateMigration.js +0 -918
- package/dist/storage/upgrade/switchProgress.js +0 -80
- package/dist/storage/upgrade/upgradeReceipt.js +0 -161
- package/dist/storage/upgradeCoordination.js +0 -187
- package/dist/storage/upgradeFence.js +0 -366
- package/dist/telemetry/telemetryCompaction.js +0 -252
|
@@ -1,60 +1,16 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
9
|
-
import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency,
|
|
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 {
|
|
22
|
-
|
|
23
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
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
|
-
|
|
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", "
|
|
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
|
|
354
|
+
if (existing.length === 0)
|
|
2792
355
|
return true;
|
|
2793
|
-
if (candidate
|
|
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
|
|
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
|
-
} }
|