@zq-silk/yui 0.2.0 → 0.4.2
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 +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { scanControllerResourceInventory } from "../../controller/resourceInventoryLinux.js";
|
|
4
|
+
import { STORAGE_STATE_FILE } from "../taskStore.js";
|
|
5
|
+
/** Pure blocker policy shared by synthetic tests and the real read-only scan. */
|
|
6
|
+
export function classifyOfflineUpgradeFacts(facts) {
|
|
7
|
+
const blockers = [];
|
|
8
|
+
for (const run of facts.runs) {
|
|
9
|
+
if (run.status !== "active")
|
|
10
|
+
continue;
|
|
11
|
+
blockers.push(identity(run, "active-run"));
|
|
12
|
+
}
|
|
13
|
+
for (const session of facts.sessions) {
|
|
14
|
+
if (session.processState === "live") {
|
|
15
|
+
blockers.push(identity(session, "native-session-live"));
|
|
16
|
+
}
|
|
17
|
+
else if (session.processState === "unknown") {
|
|
18
|
+
blockers.push(identity(session, "native-session-unknown"));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
for (const inFlight of facts.inFlight) {
|
|
22
|
+
blockers.push(identity(inFlight, "in-flight-run"));
|
|
23
|
+
}
|
|
24
|
+
for (const completion of facts.pendingCompletions) {
|
|
25
|
+
blockers.push(identity(completion, "pending-completion"));
|
|
26
|
+
}
|
|
27
|
+
for (const lifecycle of facts.lifecycle) {
|
|
28
|
+
blockers.push(identity(lifecycle, lifecycle.reason));
|
|
29
|
+
}
|
|
30
|
+
for (const unknown of facts.unknownRuntime ?? []) {
|
|
31
|
+
blockers.push(identity(unknown, "native-session-unknown"));
|
|
32
|
+
}
|
|
33
|
+
return { total: blockers.length, blockers };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Re-read durable Run/RoleSessionSet/lifecycle facts and the native process
|
|
37
|
+
* inventory without mutating, stopping, acknowledging, or quarantining them.
|
|
38
|
+
*/
|
|
39
|
+
export async function inspectOfflineUpgradeInventory(home, environment = process.env) {
|
|
40
|
+
const inventory = await scanControllerResourceInventory({
|
|
41
|
+
currentHome: home,
|
|
42
|
+
scope: "current",
|
|
43
|
+
environment
|
|
44
|
+
});
|
|
45
|
+
const raw = readRawFacts(home, inventory);
|
|
46
|
+
return classifyOfflineUpgradeFacts(raw);
|
|
47
|
+
}
|
|
48
|
+
function readRawFacts(home, inventory) {
|
|
49
|
+
const runs = [];
|
|
50
|
+
const sessions = [];
|
|
51
|
+
const inFlight = [];
|
|
52
|
+
const pendingCompletions = [];
|
|
53
|
+
const lifecycle = [];
|
|
54
|
+
const unknownRuntime = [];
|
|
55
|
+
const state = readStateObject(home);
|
|
56
|
+
// Failure to enumerate processes, panes, or sockets means absence cannot be
|
|
57
|
+
// proven. Ownership-load warnings are expected for an old record shape and
|
|
58
|
+
// are handled by the raw durable scan plus unowned live-pane check below.
|
|
59
|
+
if (inventory.warnings.some(isUndeterminableNativeInventoryWarning)) {
|
|
60
|
+
unknownRuntime.push({});
|
|
61
|
+
}
|
|
62
|
+
for (const [taskId, aggregateValue] of Object.entries(record(state.tasks))) {
|
|
63
|
+
const aggregate = record(aggregateValue);
|
|
64
|
+
for (const runValue of Object.values(record(aggregate.agentRuns))) {
|
|
65
|
+
const run = record(runValue);
|
|
66
|
+
const roleName = text(run.roleName);
|
|
67
|
+
const runId = text(run.id);
|
|
68
|
+
if (roleName === undefined || runId === undefined)
|
|
69
|
+
continue;
|
|
70
|
+
runs.push({ taskId, roleName, runId, status: text(run.status) ?? "unknown" });
|
|
71
|
+
}
|
|
72
|
+
for (const [roleName, setValue] of Object.entries(record(aggregate.roleSessionSets))) {
|
|
73
|
+
collectSessionSet({
|
|
74
|
+
taskId,
|
|
75
|
+
roleName,
|
|
76
|
+
set: record(setValue),
|
|
77
|
+
inventory,
|
|
78
|
+
sessions,
|
|
79
|
+
inFlight,
|
|
80
|
+
pendingCompletions
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const [roleName, setValue] of Object.entries(record(state.globalRoleSessionSets))) {
|
|
85
|
+
collectSessionSet({
|
|
86
|
+
roleName,
|
|
87
|
+
set: record(setValue),
|
|
88
|
+
inventory,
|
|
89
|
+
sessions,
|
|
90
|
+
inFlight,
|
|
91
|
+
pendingCompletions
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
for (const mailboxValue of Object.values(record(state.mailboxes))) {
|
|
95
|
+
const mailbox = record(mailboxValue);
|
|
96
|
+
const target = record(mailbox.target);
|
|
97
|
+
const kind = text(target.kind);
|
|
98
|
+
if (kind !== "role-runtime" && kind !== "global-role-runtime")
|
|
99
|
+
continue;
|
|
100
|
+
if (mailbox.pending === null && mailbox.processing === null)
|
|
101
|
+
continue;
|
|
102
|
+
lifecycle.push({
|
|
103
|
+
...(text(target.taskId) === undefined ? {} : { taskId: text(target.taskId) }),
|
|
104
|
+
...(text(target.roleName) === undefined ? {} : { roleName: text(target.roleName) }),
|
|
105
|
+
reason: "pending-mailbox"
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
for (let index = 0; index < countPendingInbox(home); index += 1) {
|
|
109
|
+
lifecycle.push({ reason: "pending-inbox" });
|
|
110
|
+
}
|
|
111
|
+
// Any live native pane/process the durable session sets could not identify is
|
|
112
|
+
// still a blocker: its exact owner cannot be proven stopped.
|
|
113
|
+
for (const resource of inventory.resources) {
|
|
114
|
+
if (resource.kind !== "agent-session"
|
|
115
|
+
|| (resource.processes.length === 0 && resource.paneDead !== false))
|
|
116
|
+
continue;
|
|
117
|
+
const owner = resource.owner;
|
|
118
|
+
const known = sessions.some((session) => resourceMatchesSession(resource, session));
|
|
119
|
+
if (known)
|
|
120
|
+
continue;
|
|
121
|
+
const pane = parseManagedPaneTarget(resource.target);
|
|
122
|
+
sessions.push({
|
|
123
|
+
...(owner.kind === "task-role"
|
|
124
|
+
? { taskId: owner.taskId }
|
|
125
|
+
: pane?.taskId === undefined ? {} : { taskId: pane.taskId }),
|
|
126
|
+
roleName: owner.kind === "task-role" || owner.kind === "global-role"
|
|
127
|
+
? owner.roleName
|
|
128
|
+
: pane?.roleName ?? "unknown",
|
|
129
|
+
...(owner.kind === "task-role" || owner.kind === "global-role"
|
|
130
|
+
? {
|
|
131
|
+
...(owner.nativeSessionId === undefined
|
|
132
|
+
? {}
|
|
133
|
+
: { nativeSessionId: owner.nativeSessionId }),
|
|
134
|
+
...(owner.launchId === undefined ? {} : { launchId: owner.launchId })
|
|
135
|
+
}
|
|
136
|
+
: {}),
|
|
137
|
+
status: "unknown",
|
|
138
|
+
processState: "unknown",
|
|
139
|
+
history: false
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
for (let index = 0; index < runs.length; index += 1) {
|
|
143
|
+
const run = runs[index];
|
|
144
|
+
const session = activeCurrentSession(sessions, run.taskId, run.roleName);
|
|
145
|
+
if (session !== undefined) {
|
|
146
|
+
runs[index] = {
|
|
147
|
+
...run,
|
|
148
|
+
...(session.nativeSessionId === undefined
|
|
149
|
+
? {}
|
|
150
|
+
: { nativeSessionId: session.nativeSessionId }),
|
|
151
|
+
...(session.launchId === undefined ? {} : { launchId: session.launchId })
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { runs, sessions, inFlight, pendingCompletions, lifecycle, unknownRuntime };
|
|
156
|
+
}
|
|
157
|
+
function parseManagedPaneTarget(target) {
|
|
158
|
+
if (target === undefined)
|
|
159
|
+
return undefined;
|
|
160
|
+
const match = /^yui-[a-f0-9]{12}-(.+):([^:]+)$/u.exec(target);
|
|
161
|
+
if (match === null)
|
|
162
|
+
return undefined;
|
|
163
|
+
return match[1] === "operator"
|
|
164
|
+
? { roleName: match[2] }
|
|
165
|
+
: { taskId: match[1], roleName: match[2] };
|
|
166
|
+
}
|
|
167
|
+
function isUndeterminableNativeInventoryWarning(warning) {
|
|
168
|
+
return warning.startsWith("Cannot enumerate Linux processes")
|
|
169
|
+
|| warning.startsWith("Cannot inspect tmux")
|
|
170
|
+
|| warning.startsWith("Cannot inspect Unix sockets");
|
|
171
|
+
}
|
|
172
|
+
function collectSessionSet(options) {
|
|
173
|
+
const { taskId, roleName, set, inventory } = options;
|
|
174
|
+
const currentSessions = Object.entries(record(set.sessions));
|
|
175
|
+
const activeAgentId = text(set.activeAgentId);
|
|
176
|
+
const historyValue = set.history;
|
|
177
|
+
const historySessions = Array.isArray(historyValue)
|
|
178
|
+
? historyValue
|
|
179
|
+
: Object.values(record(historyValue));
|
|
180
|
+
for (const { history, value, active } of [
|
|
181
|
+
...currentSessions.map(([agentId, session]) => ({
|
|
182
|
+
history: false,
|
|
183
|
+
value: session,
|
|
184
|
+
active: activeAgentId !== undefined && agentId === activeAgentId
|
|
185
|
+
})),
|
|
186
|
+
...historySessions.map((session) => ({ history: true, value: session, active: false }))
|
|
187
|
+
]) {
|
|
188
|
+
const session = record(value);
|
|
189
|
+
const nativeSessionId = text(session.nativeSessionId);
|
|
190
|
+
const launchId = text(session.launchId);
|
|
191
|
+
const base = {
|
|
192
|
+
...(taskId === undefined ? {} : { taskId }),
|
|
193
|
+
roleName,
|
|
194
|
+
...(nativeSessionId === undefined ? {} : { nativeSessionId }),
|
|
195
|
+
...(launchId === undefined ? {} : { launchId }),
|
|
196
|
+
status: text(session.status) ?? "unknown",
|
|
197
|
+
history,
|
|
198
|
+
active
|
|
199
|
+
};
|
|
200
|
+
options.sessions.push({
|
|
201
|
+
...base,
|
|
202
|
+
processState: processState(inventory, base)
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const flight = record(set.inFlight);
|
|
206
|
+
const runId = text(flight.runId);
|
|
207
|
+
if (taskId !== undefined && runId !== undefined) {
|
|
208
|
+
const session = activeCurrentSession(options.sessions, taskId, roleName);
|
|
209
|
+
options.inFlight.push({
|
|
210
|
+
taskId,
|
|
211
|
+
roleName,
|
|
212
|
+
runId,
|
|
213
|
+
...(session?.nativeSessionId === undefined
|
|
214
|
+
? {}
|
|
215
|
+
: { nativeSessionId: session.nativeSessionId }),
|
|
216
|
+
...(session?.launchId === undefined ? {} : { launchId: session.launchId })
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const completion = record(set.pendingTurnCompletion);
|
|
220
|
+
const completionTask = text(completion.taskId) ?? taskId;
|
|
221
|
+
if (completionTask !== undefined && Object.keys(completion).length > 0) {
|
|
222
|
+
const completionRole = text(completion.roleName) ?? roleName;
|
|
223
|
+
const completionNativeSessionId = text(completion.nativeSessionId);
|
|
224
|
+
const completionSession = options.sessions.find((candidate) => (candidate.taskId === completionTask
|
|
225
|
+
&& candidate.roleName === completionRole
|
|
226
|
+
&& completionNativeSessionId !== undefined
|
|
227
|
+
&& candidate.nativeSessionId === completionNativeSessionId));
|
|
228
|
+
options.pendingCompletions.push({
|
|
229
|
+
taskId: completionTask,
|
|
230
|
+
roleName: completionRole,
|
|
231
|
+
...(text(completion.runId) === undefined ? {} : { runId: text(completion.runId) }),
|
|
232
|
+
...(completionNativeSessionId === undefined
|
|
233
|
+
? {}
|
|
234
|
+
: { nativeSessionId: completionNativeSessionId }),
|
|
235
|
+
...(completionSession?.launchId === undefined
|
|
236
|
+
? {}
|
|
237
|
+
: { launchId: completionSession.launchId })
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function activeCurrentSession(sessions, taskId, roleName) {
|
|
242
|
+
const current = sessions.filter((candidate) => (candidate.taskId === taskId
|
|
243
|
+
&& candidate.roleName === roleName
|
|
244
|
+
&& !candidate.history));
|
|
245
|
+
return current.find(({ active }) => active === true)
|
|
246
|
+
?? (current.length === 1 ? current[0] : undefined);
|
|
247
|
+
}
|
|
248
|
+
function processState(inventory, session) {
|
|
249
|
+
const matching = inventory.resources.filter((resource) => (resource.kind === "agent-session" && resourceMatchesSession(resource, session)));
|
|
250
|
+
if (matching.some((resource) => resource.processes.length > 0))
|
|
251
|
+
return "live";
|
|
252
|
+
if (matching.some((resource) => resource.paneDead === false))
|
|
253
|
+
return "unknown";
|
|
254
|
+
return "stopped";
|
|
255
|
+
}
|
|
256
|
+
function resourceMatchesSession(resource, session) {
|
|
257
|
+
const owner = resource.owner;
|
|
258
|
+
if (owner.kind !== "task-role" && owner.kind !== "global-role")
|
|
259
|
+
return false;
|
|
260
|
+
if (session.taskId === undefined) {
|
|
261
|
+
if (owner.kind !== "global-role")
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
else if (owner.kind !== "task-role" || owner.taskId !== session.taskId)
|
|
265
|
+
return false;
|
|
266
|
+
if (owner.roleName !== session.roleName)
|
|
267
|
+
return false;
|
|
268
|
+
if (session.nativeSessionId !== undefined && owner.nativeSessionId !== session.nativeSessionId) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
if (session.launchId !== undefined && owner.launchId !== session.launchId)
|
|
272
|
+
return false;
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
function readStateObject(home) {
|
|
276
|
+
const path = join(home, STORAGE_STATE_FILE);
|
|
277
|
+
if (!existsSync(path))
|
|
278
|
+
return {};
|
|
279
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
280
|
+
return record(parsed);
|
|
281
|
+
}
|
|
282
|
+
function countPendingInbox(home) {
|
|
283
|
+
return [join(home, "runtime", "inbox"), join(home, "runtime", "inbox-invalid")]
|
|
284
|
+
.reduce((total, directory) => total + countDirectory(directory), 0);
|
|
285
|
+
}
|
|
286
|
+
function countDirectory(directory) {
|
|
287
|
+
try {
|
|
288
|
+
return readdirSync(directory).filter((name) => (name.endsWith(".json") || name.includes(".tmp-") || !name.startsWith("."))).length;
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
292
|
+
return 0;
|
|
293
|
+
return 1;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function identity(value, reason) {
|
|
297
|
+
return {
|
|
298
|
+
...(value.taskId === undefined ? {} : { taskId: value.taskId }),
|
|
299
|
+
...(value.roleName === undefined ? {} : { roleName: value.roleName }),
|
|
300
|
+
...(value.runId === undefined ? {} : { runId: value.runId }),
|
|
301
|
+
...(value.nativeSessionId === undefined
|
|
302
|
+
? {}
|
|
303
|
+
: { nativeSessionId: value.nativeSessionId }),
|
|
304
|
+
...(value.launchId === undefined ? {} : { launchId: value.launchId }),
|
|
305
|
+
reason
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function record(value) {
|
|
309
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
310
|
+
? value
|
|
311
|
+
: {};
|
|
312
|
+
}
|
|
313
|
+
function text(value) {
|
|
314
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
315
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backward-compatible import path for the one production storage registry.
|
|
3
|
+
* Compatible loading and offline migration share the declaration graph owned by
|
|
4
|
+
* `storage/migration/productionRegistry.ts`.
|
|
5
|
+
*/
|
|
6
|
+
export { createProductionStorageRegistry as createProductionMigrationRegistry } from "../migration/productionRegistry.js";
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, read-only structural scan of raw `state.json`, never through the strict
|
|
3
|
+
* `parseState`/`FileTaskStore` gate. The durable manifest remains authoritative;
|
|
4
|
+
* this scanner supplies member versions and counts so callers can detect
|
|
5
|
+
* manifest/state contradictions without treating an empty locator as current.
|
|
6
|
+
* Older member versions remain version evidence rather than a strict-loader
|
|
7
|
+
* parse failure, while malformed JSON/containers/schemaVersion fields are real
|
|
8
|
+
* `CORRUPTED` evidence.
|
|
9
|
+
*
|
|
10
|
+
* The `path` for each family (from `recordVersions.ts`) is a small locator into
|
|
11
|
+
* `state.json`: segments are `/`-separated after the `state.json#/` prefix, and a
|
|
12
|
+
* `*` segment fans out over every member of the map at that level (so the locator
|
|
13
|
+
* `state.json#/tasks/{star}/workItems` visits the `workItems` map of every task,
|
|
14
|
+
* where `{star}` is a literal `*`).
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { STORAGE_STATE_FILE } from "../taskStore.js";
|
|
19
|
+
/**
|
|
20
|
+
* Scan per-family member versions and counts from raw `state.json`.
|
|
21
|
+
*
|
|
22
|
+
* Rules, per family, evaluated purely (no strict parser):
|
|
23
|
+
* - family absent or empty on disk -> latest as an internal scan sentinel,
|
|
24
|
+
* with count zero so the manifest combiner never infers source currency;
|
|
25
|
+
* - every member at one older/current/newer version -> report that version;
|
|
26
|
+
* - members carrying different versions -> corruption.
|
|
27
|
+
*
|
|
28
|
+
* A family is one durable schema boundary, so a manifest cannot make a mixed
|
|
29
|
+
* member set trustworthy by matching either its minimum or maximum. Mixed
|
|
30
|
+
* members are partial-write/corruption evidence, not a migration source version.
|
|
31
|
+
*
|
|
32
|
+
* Returns a `corruption` instead when the JSON is unparseable, the root is not an
|
|
33
|
+
* object, a path container is not the shape the locator describes, or a record is
|
|
34
|
+
* missing/has an invalid `schemaVersion`.
|
|
35
|
+
*/
|
|
36
|
+
export function scanSourceRecordVersions(home, latestRecord) {
|
|
37
|
+
const statePath = join(home, STORAGE_STATE_FILE);
|
|
38
|
+
if (!existsSync(statePath)) {
|
|
39
|
+
// No state.json means zero persisted members. The latest-valued scan entry
|
|
40
|
+
// is only a sentinel; counts preserve absence for the manifest combiner.
|
|
41
|
+
return {
|
|
42
|
+
record: cloneRecord(latestRecord),
|
|
43
|
+
counts: Object.fromEntries(Object.keys(latestRecord).map((kind) => [kind, 0]))
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
let root;
|
|
47
|
+
try {
|
|
48
|
+
root = JSON.parse(readFileSync(statePath, "utf8"));
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
return corruption(`state.json is not valid JSON: ${messageOf(error)}`);
|
|
52
|
+
}
|
|
53
|
+
if (!isObject(root)) {
|
|
54
|
+
return corruption("state.json is not a JSON object.");
|
|
55
|
+
}
|
|
56
|
+
return scanRecordVersionsFromState(root, latestRecord);
|
|
57
|
+
}
|
|
58
|
+
/** Extract record versions from an already-parsed state snapshot, read-only. */
|
|
59
|
+
export function scanRecordVersionsFromState(root, latestRecord) {
|
|
60
|
+
if (!isObject(root))
|
|
61
|
+
return corruption("state snapshot is not a JSON object.");
|
|
62
|
+
const record = {};
|
|
63
|
+
const counts = {};
|
|
64
|
+
for (const [kind, entry] of Object.entries(latestRecord)) {
|
|
65
|
+
const scan = scanFamily(root, entry.path, entry.version);
|
|
66
|
+
if ("corruption" in scan) {
|
|
67
|
+
return corruption(`record family '${kind}' (${entry.path}): ${scan.corruption}`);
|
|
68
|
+
}
|
|
69
|
+
record[kind] = { version: scan.version, path: entry.path };
|
|
70
|
+
counts[kind] = scan.count;
|
|
71
|
+
}
|
|
72
|
+
return { record, counts };
|
|
73
|
+
}
|
|
74
|
+
/** Resolve one family's source version from `root` following `path`. */
|
|
75
|
+
function scanFamily(root, path, latestVersion) {
|
|
76
|
+
const segments = parseLocator(path);
|
|
77
|
+
if (segments === null)
|
|
78
|
+
return { corruption: `unrecognized path syntax "${path}"` };
|
|
79
|
+
// Resolve the path to the family's endpoint node(s). A `*` fans out over the
|
|
80
|
+
// members of the map at that level; an absent named key contributes nothing
|
|
81
|
+
// (that branch simply has no records), but a present-but-wrong-typed node is
|
|
82
|
+
// structural damage.
|
|
83
|
+
let nodes = [root];
|
|
84
|
+
for (const segment of segments) {
|
|
85
|
+
const next = [];
|
|
86
|
+
for (const node of nodes) {
|
|
87
|
+
if (segment === "*") {
|
|
88
|
+
if (!isObject(node))
|
|
89
|
+
return { corruption: `expected an object to enumerate at "*"` };
|
|
90
|
+
next.push(...Object.values(node));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
if (!isObject(node))
|
|
94
|
+
return { corruption: `expected an object at "${segment}"` };
|
|
95
|
+
if (!Object.hasOwn(node, segment))
|
|
96
|
+
continue; // absent branch: no records here
|
|
97
|
+
next.push(node[segment]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
nodes = next;
|
|
101
|
+
}
|
|
102
|
+
// Each endpoint is either a single record (it carries `schemaVersion`, e.g.
|
|
103
|
+
// tasks/* (the StoredTask aggregate), tasks/*/task, tasks/*/brief) or a map of records (its values carry
|
|
104
|
+
// `schemaVersion`, e.g. configuredAgents, tasks/*/workItems).
|
|
105
|
+
let min = Number.POSITIVE_INFINITY;
|
|
106
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
107
|
+
let count = 0;
|
|
108
|
+
for (const endpoint of nodes) {
|
|
109
|
+
if (endpoint === null || endpoint === undefined)
|
|
110
|
+
continue; // e.g. brief: null
|
|
111
|
+
if (!isObject(endpoint))
|
|
112
|
+
return { corruption: "family endpoint is not an object" };
|
|
113
|
+
if (typeof endpoint.schemaVersion === "number") {
|
|
114
|
+
const version = readSchemaVersion(endpoint);
|
|
115
|
+
if (version === null)
|
|
116
|
+
return { corruption: `record has an invalid schemaVersion` };
|
|
117
|
+
min = Math.min(min, version);
|
|
118
|
+
max = Math.max(max, version);
|
|
119
|
+
count += 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
for (const member of Object.values(endpoint)) {
|
|
123
|
+
if (member === null)
|
|
124
|
+
continue;
|
|
125
|
+
if (!isObject(member))
|
|
126
|
+
return { corruption: "record member is not an object" };
|
|
127
|
+
const version = readSchemaVersion(member);
|
|
128
|
+
if (version === null)
|
|
129
|
+
return { corruption: "record member has a missing/invalid schemaVersion" };
|
|
130
|
+
min = Math.min(min, version);
|
|
131
|
+
max = Math.max(max, version);
|
|
132
|
+
count += 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (count === 0)
|
|
136
|
+
return { version: latestVersion, count }; // zero-count sentinel only
|
|
137
|
+
if (min !== max) {
|
|
138
|
+
return {
|
|
139
|
+
corruption: `family contains mixed schemaVersion values ${min} and ${max}`
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return { version: min, count };
|
|
143
|
+
}
|
|
144
|
+
/** Parse a `state.json#/`-prefixed locator into path segments, or `null`. */
|
|
145
|
+
function parseLocator(path) {
|
|
146
|
+
const prefix = `${STORAGE_STATE_FILE}#/`;
|
|
147
|
+
if (!path.startsWith(prefix))
|
|
148
|
+
return null;
|
|
149
|
+
const body = path.slice(prefix.length);
|
|
150
|
+
if (body.length === 0)
|
|
151
|
+
return null;
|
|
152
|
+
return body.split("/");
|
|
153
|
+
}
|
|
154
|
+
/** A positive-integer `schemaVersion`, or `null` when missing/invalid. */
|
|
155
|
+
function readSchemaVersion(record) {
|
|
156
|
+
const version = record.schemaVersion;
|
|
157
|
+
if (typeof version !== "number" || !Number.isInteger(version) || version < 1)
|
|
158
|
+
return null;
|
|
159
|
+
return version;
|
|
160
|
+
}
|
|
161
|
+
function cloneRecord(record) {
|
|
162
|
+
const copy = {};
|
|
163
|
+
for (const [kind, entry] of Object.entries(record)) {
|
|
164
|
+
copy[kind] = { version: entry.version, path: entry.path };
|
|
165
|
+
}
|
|
166
|
+
return copy;
|
|
167
|
+
}
|
|
168
|
+
function corruption(detail) {
|
|
169
|
+
return { corruption: { corrupted: true, detail } };
|
|
170
|
+
}
|
|
171
|
+
function isObject(value) {
|
|
172
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
173
|
+
}
|
|
174
|
+
function messageOf(error) {
|
|
175
|
+
return error instanceof Error ? error.message : String(error);
|
|
176
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The current-baseline record-axis version map.
|
|
3
|
+
*
|
|
4
|
+
* The three storage axes are `layout`, `aggregate`, and `record`. The layout and
|
|
5
|
+
* aggregate axes are scalars sourced from `storageSchema.ts`; the record axis is
|
|
6
|
+
* a `recordKind -> { version, path }` map because every record family versions
|
|
7
|
+
* independently (see the generic migration core in `../migration`).
|
|
8
|
+
*
|
|
9
|
+
* This map is a SNAPSHOT of the record families that exist on the current
|
|
10
|
+
* baseline, enumerated from their real `schemaVersion`s in `taskStore.ts` and the
|
|
11
|
+
* per-record modules. It is intentionally NOT a frozen "first release" set: as
|
|
12
|
+
* the schema evolves (a new record family, a bumped family version), this map is
|
|
13
|
+
* updated in lockstep, exactly like the scalar constants. It exists so
|
|
14
|
+
* doctor/inspect and the migration planner can reason about record families.
|
|
15
|
+
* Post-baseline record changes require an explicit adjacent registration and
|
|
16
|
+
* transform in the production migration graph.
|
|
17
|
+
*/
|
|
18
|
+
import { CURRENT_AGGREGATE_SCHEMA_VERSION, CURRENT_STORAGE_LAYOUT_VERSION } from "../storageVersions.js";
|
|
19
|
+
import { CURRENT_AGENT_RUN_SCHEMA_VERSION, CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION, CURRENT_AGENT_PROFILE_SCHEMA_VERSION, CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION, CURRENT_CHANGE_SET_SCHEMA_VERSION, CURRENT_DECISION_SCHEMA_VERSION, CURRENT_EVENT_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_INPUT_REQUEST_SCHEMA_VERSION, CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION, CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION, CURRENT_MESSAGE_SCHEMA_VERSION, CURRENT_MILESTONE_SCHEMA_VERSION, CURRENT_PROJECT_SCHEMA_VERSION, CURRENT_REVIEW_ROUND_SCHEMA_VERSION, CURRENT_STORED_TASK_SCHEMA_VERSION, CURRENT_TASK_BRIEF_SCHEMA_VERSION, CURRENT_TASK_ROLE_SCHEMA_VERSION, CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_TASK_SCHEMA_VERSION, CURRENT_WORK_ITEM_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION } from "../taskStore.js";
|
|
20
|
+
import { CURRENT_LEADER_FAILURE_SCHEMA_VERSION } from "../../scheduler/leaderFailure.js";
|
|
21
|
+
import { CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION } from "../../scheduler/operatorNotification.js";
|
|
22
|
+
function descriptor(version, path) {
|
|
23
|
+
return Object.freeze({ version, path });
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Independent persisted-boundary contract for the direct family keys and
|
|
27
|
+
* locators. The upgrade descriptor table is checked against this contract so
|
|
28
|
+
* omitting a newly persisted family or changing a locator cannot make the
|
|
29
|
+
* scanner silently skip that family.
|
|
30
|
+
*/
|
|
31
|
+
const EXPECTED_DIRECT_RECORD_LOCATORS = Object.freeze({
|
|
32
|
+
config: "state.json#/config",
|
|
33
|
+
configuredAgent: "state.json#/configuredAgents",
|
|
34
|
+
project: "state.json#/projects",
|
|
35
|
+
agentProfile: "state.json#/agentProfiles",
|
|
36
|
+
globalRole: "state.json#/globalRoles",
|
|
37
|
+
globalRoleSessionSet: "state.json#/globalRoleSessionSets",
|
|
38
|
+
storedTask: "state.json#/tasks/*",
|
|
39
|
+
task: "state.json#/tasks/*/task",
|
|
40
|
+
taskBrief: "state.json#/tasks/*/brief",
|
|
41
|
+
taskRole: "state.json#/tasks/*/roles",
|
|
42
|
+
managedWorkspace: "state.json#/tasks/*/managedWorkspaces",
|
|
43
|
+
taskRoleSessionSet: "state.json#/tasks/*/roleSessionSets",
|
|
44
|
+
workItem: "state.json#/tasks/*/workItems",
|
|
45
|
+
agentRun: "state.json#/tasks/*/agentRuns",
|
|
46
|
+
reviewRound: "state.json#/tasks/*/reviewRounds",
|
|
47
|
+
changeSet: "state.json#/tasks/*/changeSets",
|
|
48
|
+
integrationAttempt: "state.json#/tasks/*/integrationAttempts",
|
|
49
|
+
activeRunPointer: "state.json#/tasks/*/activeRuns",
|
|
50
|
+
message: "state.json#/tasks/*/messages",
|
|
51
|
+
inputRequest: "state.json#/tasks/*/inputRequests",
|
|
52
|
+
decision: "state.json#/tasks/*/decisions",
|
|
53
|
+
milestone: "state.json#/tasks/*/milestones",
|
|
54
|
+
event: "state.json#/tasks/*/events",
|
|
55
|
+
leaderFailure: "state.json#/tasks/*/leaderFailure",
|
|
56
|
+
operatorNotification: "state.json#/tasks/*/operatorNotification",
|
|
57
|
+
workMailbox: "state.json#/mailboxes"
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* The one authoritative descriptor table for every direct StorageState and
|
|
61
|
+
* StoredTask record family. `path` is a logical, human-readable location of
|
|
62
|
+
* where the family's records live inside the authoritative store; the generic
|
|
63
|
+
* engine treats it as opaque. Update this table whenever a family is added or
|
|
64
|
+
* its `schemaVersion` changes — it tracks the live schema, it does not freeze
|
|
65
|
+
* it.
|
|
66
|
+
*
|
|
67
|
+
* Lazy-initialized to avoid a circular-dependency TDZ: `recordVersions.ts`
|
|
68
|
+
* imports the `CURRENT_*_SCHEMA_VERSION` constants from `taskStore.ts`, which
|
|
69
|
+
* imports `requireStorageSchema` from `storageSchema.ts`, which (via the
|
|
70
|
+
* provider pattern) depends on this module. Building the descriptors lazily
|
|
71
|
+
* means the constants are only read at runtime, after every module has loaded.
|
|
72
|
+
*/
|
|
73
|
+
let currentRecordDescriptors = null;
|
|
74
|
+
function getCurrentRecordDescriptors() {
|
|
75
|
+
if (currentRecordDescriptors === null) {
|
|
76
|
+
currentRecordDescriptors = Object.freeze({
|
|
77
|
+
config: descriptor(CURRENT_CONFIG_SCHEMA_VERSION, "state.json#/config"),
|
|
78
|
+
configuredAgent: descriptor(CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION, "state.json#/configuredAgents"),
|
|
79
|
+
project: descriptor(CURRENT_PROJECT_SCHEMA_VERSION, "state.json#/projects"),
|
|
80
|
+
agentProfile: descriptor(CURRENT_AGENT_PROFILE_SCHEMA_VERSION, "state.json#/agentProfiles"),
|
|
81
|
+
globalRole: descriptor(CURRENT_GLOBAL_ROLE_SCHEMA_VERSION, "state.json#/globalRoles"),
|
|
82
|
+
globalRoleSessionSet: descriptor(CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION, "state.json#/globalRoleSessionSets"),
|
|
83
|
+
// The task aggregate is itself a versioned record family. Keep this family
|
|
84
|
+
// alongside (and distinct from) the nested `task` record family: an older
|
|
85
|
+
// aggregate wrapper must be classified on the record axis before the strict
|
|
86
|
+
// loader is asked to parse its nested members.
|
|
87
|
+
storedTask: descriptor(CURRENT_STORED_TASK_SCHEMA_VERSION, "state.json#/tasks/*"),
|
|
88
|
+
task: descriptor(CURRENT_TASK_SCHEMA_VERSION, "state.json#/tasks/*/task"),
|
|
89
|
+
taskBrief: descriptor(CURRENT_TASK_BRIEF_SCHEMA_VERSION, "state.json#/tasks/*/brief"),
|
|
90
|
+
taskRole: descriptor(CURRENT_TASK_ROLE_SCHEMA_VERSION, "state.json#/tasks/*/roles"),
|
|
91
|
+
managedWorkspace: descriptor(CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION, "state.json#/tasks/*/managedWorkspaces"),
|
|
92
|
+
taskRoleSessionSet: descriptor(CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION, "state.json#/tasks/*/roleSessionSets"),
|
|
93
|
+
workItem: descriptor(CURRENT_WORK_ITEM_SCHEMA_VERSION, "state.json#/tasks/*/workItems"),
|
|
94
|
+
agentRun: descriptor(CURRENT_AGENT_RUN_SCHEMA_VERSION, "state.json#/tasks/*/agentRuns"),
|
|
95
|
+
reviewRound: descriptor(CURRENT_REVIEW_ROUND_SCHEMA_VERSION, "state.json#/tasks/*/reviewRounds"),
|
|
96
|
+
changeSet: descriptor(CURRENT_CHANGE_SET_SCHEMA_VERSION, "state.json#/tasks/*/changeSets"),
|
|
97
|
+
integrationAttempt: descriptor(CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION, "state.json#/tasks/*/integrationAttempts"),
|
|
98
|
+
activeRunPointer: descriptor(CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION, "state.json#/tasks/*/activeRuns"),
|
|
99
|
+
message: descriptor(CURRENT_MESSAGE_SCHEMA_VERSION, "state.json#/tasks/*/messages"),
|
|
100
|
+
inputRequest: descriptor(CURRENT_INPUT_REQUEST_SCHEMA_VERSION, "state.json#/tasks/*/inputRequests"),
|
|
101
|
+
decision: descriptor(CURRENT_DECISION_SCHEMA_VERSION, "state.json#/tasks/*/decisions"),
|
|
102
|
+
milestone: descriptor(CURRENT_MILESTONE_SCHEMA_VERSION, "state.json#/tasks/*/milestones"),
|
|
103
|
+
event: descriptor(CURRENT_EVENT_SCHEMA_VERSION, "state.json#/tasks/*/events"),
|
|
104
|
+
leaderFailure: descriptor(CURRENT_LEADER_FAILURE_SCHEMA_VERSION, "state.json#/tasks/*/leaderFailure"),
|
|
105
|
+
operatorNotification: descriptor(CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION, "state.json#/tasks/*/operatorNotification"),
|
|
106
|
+
workMailbox: descriptor(CURRENT_WORK_MAILBOX_SCHEMA_VERSION, "state.json#/mailboxes")
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return currentRecordDescriptors;
|
|
110
|
+
}
|
|
111
|
+
/** Kept as the public-facing name used by upgrade callers and tests. */
|
|
112
|
+
const CURRENT_RECORD_VERSIONS = getCurrentRecordDescriptors;
|
|
113
|
+
/** The record-axis map for the current baseline (a defensive shallow copy). */
|
|
114
|
+
export function currentRecordVersions(candidate = CURRENT_RECORD_VERSIONS()) {
|
|
115
|
+
assertRecordVersionDescriptors(candidate);
|
|
116
|
+
return { ...candidate };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Fail closed if a record descriptor map drifts from the one authoritative
|
|
120
|
+
* table. The candidate parameter is exported so the regression suite can
|
|
121
|
+
* exercise an unchecked-family version/path drift directly; production callers
|
|
122
|
+
* use the default current descriptor table.
|
|
123
|
+
*/
|
|
124
|
+
export function assertRecordVersionDescriptors(candidate = CURRENT_RECORD_VERSIONS()) {
|
|
125
|
+
const expectedKinds = Object.keys(EXPECTED_DIRECT_RECORD_LOCATORS);
|
|
126
|
+
const mappedKinds = Object.keys(candidate);
|
|
127
|
+
const missing = expectedKinds.filter((kind) => !Object.hasOwn(candidate, kind));
|
|
128
|
+
const unexpected = mappedKinds.filter((kind) => !Object.hasOwn(EXPECTED_DIRECT_RECORD_LOCATORS, kind));
|
|
129
|
+
if (missing.length > 0 || unexpected.length > 0) {
|
|
130
|
+
throw new Error(`Record version map completeness drift: missing=${missing.join(",") || "none"}; `
|
|
131
|
+
+ `unexpected=${unexpected.join(",") || "none"}.`);
|
|
132
|
+
}
|
|
133
|
+
const descriptors = getCurrentRecordDescriptors();
|
|
134
|
+
for (const kind of expectedKinds) {
|
|
135
|
+
const expected = descriptors[kind];
|
|
136
|
+
const expectedPath = EXPECTED_DIRECT_RECORD_LOCATORS[kind];
|
|
137
|
+
const mapped = candidate[kind];
|
|
138
|
+
if (expected === undefined
|
|
139
|
+
|| mapped === undefined
|
|
140
|
+
|| mapped === null
|
|
141
|
+
|| mapped.version !== expected.version
|
|
142
|
+
|| mapped.path !== expectedPath) {
|
|
143
|
+
throw new Error(`Record version map drift for ${kind}: expected=${String(expected?.version)}@${expectedPath}; `
|
|
144
|
+
+ `actual=${String(mapped?.version)}@${String(mapped?.path)}.`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The full latest-supported {@link StorageVersionState} this release understands:
|
|
150
|
+
* the scalar layout/aggregate versions plus the current record map. The engine
|
|
151
|
+
* and classifier compare a source against this to decide the four-state verdict.
|
|
152
|
+
*/
|
|
153
|
+
export function latestStorageVersionState(recordDescriptors = CURRENT_RECORD_VERSIONS()) {
|
|
154
|
+
return {
|
|
155
|
+
layout: CURRENT_STORAGE_LAYOUT_VERSION,
|
|
156
|
+
aggregate: CURRENT_AGGREGATE_SCHEMA_VERSION,
|
|
157
|
+
record: currentRecordVersions(recordDescriptors)
|
|
158
|
+
};
|
|
159
|
+
}
|