@zq-silk/yui 0.11.3 → 0.12.1
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 +23 -4
- package/README.md +44 -22
- package/dist/cli/commandCatalog.js +13 -10
- package/dist/cli/interactionPolicy.js +5 -6
- package/dist/cli/updateCommand.js +3 -1
- package/dist/cli/updateOrchestrator.js +173 -28
- package/dist/cli/updatePorts.js +137 -8
- package/dist/cli/upgradeCommand.js +19 -9
- package/dist/cli.js +156 -57
- package/dist/commands/configCommands.js +13 -46
- package/dist/commands/executionAuditCommands.js +3 -2
- package/dist/commands/jobCommands.js +4 -10
- package/dist/commands/taskCommands.js +234 -200
- package/dist/commands/taskCompletionGate.js +29 -54
- package/dist/commands/taskContextCommand.js +61 -13
- package/dist/commands/taskInputCommands.js +8 -5
- package/dist/commands/taskNextActionCommand.js +56 -6
- package/dist/commands/taskOverviewCommand.js +17 -31
- package/dist/commands/taskRoleRuntimeStatus.js +55 -7
- package/dist/commands/taskWorkspaceCommands.js +5 -2
- package/dist/config/configCatalog.js +3 -3
- package/dist/config/yuiConfig.js +3 -7
- package/dist/context/wakeNotification.js +20 -5
- package/dist/controller/agentRuntimeObserver.js +247 -51
- package/dist/controller/clientRuntime.js +39 -3
- package/dist/controller/controller.js +31 -27
- package/dist/controller/fileSchedulerStoreAdapter.js +390 -332
- package/dist/controller/runtime.js +58 -3
- package/dist/controller/runtimeEventInbox.js +17 -7
- package/dist/controller/runtimeEventProcessor.js +12 -19
- package/dist/controller/runtimeHookRunFence.js +19 -4
- package/dist/controller/runtimeObservationHook.js +45 -0
- package/dist/controller/structuredProviderObservation.js +20 -3
- package/dist/core/controllerClient.js +20 -2
- package/dist/core/controllerServer.js +1 -0
- package/dist/execution/resourceBroker.js +5 -4
- package/dist/executor/agentExecutor.js +52 -50
- package/dist/executor/effectiveLaunch.js +8 -48
- package/dist/executor/fileRoleLaunchPlanner.js +129 -45
- package/dist/executor/workspacePreflightClassification.js +23 -2
- package/dist/interaction/operatorPresentation.js +33 -89
- package/dist/lifecycle/exactRunTerminalization.js +69 -5
- package/dist/observability/executionAudit.js +5 -0
- package/dist/observability/orchestrationMetrics.js +8 -3
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +123 -0
- package/dist/release/runtimeRelease.js +20 -0
- package/dist/repository/taskWorkspacePreparer.js +176 -60
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/deltaRecheck.js +12 -51
- package/dist/review/reviewAcceptance.js +26 -0
- package/dist/review/reviewConfig.js +0 -31
- package/dist/review/reviewDecision.js +113 -0
- package/dist/review/reviewOutcomeClassifier.js +1 -1
- package/dist/review/reviewRound.js +1 -1
- package/dist/review/reviewerAvailability.js +69 -0
- package/dist/run/recoveryProjection.js +45 -6
- package/dist/runtime/agentDriverObservation.js +24 -10
- package/dist/runtime/agentHost.js +159 -85
- package/dist/runtime/builtinAgentDrivers.js +4 -3
- package/dist/runtime/builtinTranscriptObserver.js +301 -64
- package/dist/runtime/builtinTranscriptUsage.js +9 -7
- package/dist/runtime/conversationSwitch.js +277 -0
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/launchBroker.js +12 -0
- package/dist/runtime/processExitOutbox.js +88 -0
- package/dist/runtime/providerRuntimeIdentity.js +29 -1
- package/dist/runtime/runtimeHealthPolicy.js +5 -5
- package/dist/runtime/runtimeObservation.js +28 -0
- package/dist/runtime/runtimeProjection.js +22 -32
- package/dist/runtime/sessionTokenMetrics.js +181 -0
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/runtime/tmuxAdapters.js +4 -1
- package/dist/scheduler/activeRoleRunDelivery.js +34 -199
- package/dist/scheduler/activeTaskProgress.js +7 -6
- package/dist/scheduler/leaderWakeupProcessor.js +42 -138
- package/dist/scheduler/operatorEvent.js +34 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
- package/dist/scheduler/roleRunStall.js +53 -31
- package/dist/scheduler/taskExecutionProjection.js +97 -76
- package/dist/scheduler/taskObservabilityProjection.js +10 -11
- package/dist/storage/migration/productionRegistry.js +379 -0
- package/dist/storage/sqliteSchema.js +66 -23
- package/dist/storage/sqliteStore.js +46 -23
- package/dist/storage/storeRpc.js +0 -1
- package/dist/storage/taskStore.js +15 -30
- package/dist/storage/upgrade/homeClassification.js +52 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
- package/dist/storage/upgrade/recordVersions.js +3 -2
- package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
- package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
- package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
- package/dist/task/completionReadiness.js +10 -6
- package/dist/task/nextAction.js +82 -58
- package/dist/telemetry/sqliteTelemetryStore.js +1 -1
- package/dist/web/assets/client/components.js +13 -3
- package/dist/web/assets/client/i18n.js +6 -2
- package/dist/web/webSnapshot.js +7 -1
- package/i18n/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +60 -20
- package/skills/yui-operator/SKILL.md +13 -4
- package/skills/yui-reviewer/SKILL.md +35 -11
- package/dist/context/sessionContextBudget.js +0 -71
- package/dist/lifecycle/contextBudgetRollover.js +0 -81
- package/dist/lifecycle/taskRoleSessionReset.js +0 -126
- package/dist/scheduler/operatorNotification.js +0 -59
package/dist/storage/storeRpc.js
CHANGED
|
@@ -31,7 +31,6 @@ import { validateIntegrationQueueEntry } from "../integration/integrationQueueEn
|
|
|
31
31
|
import { CURRENT_DURABLE_JOB_SCHEMA_VERSION, validDurableJobTransition, validateDurableJob } from "../job/durableJob.js";
|
|
32
32
|
import { validateGlobalRole, validateTaskRole } from "../role/role.js";
|
|
33
33
|
import { CURRENT_LEADER_FAILURE_SCHEMA_VERSION } from "../scheduler/leaderFailure.js";
|
|
34
|
-
import { CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION } from "../scheduler/operatorNotification.js";
|
|
35
34
|
import { CURRENT_TASK_WAKE_SCHEMA_VERSION, validateTaskWake } from "../scheduler/taskWake.js";
|
|
36
35
|
import { validateTask } from "../task/task.js";
|
|
37
36
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
@@ -46,7 +45,7 @@ import { CURRENT_AGGREGATE_SCHEMA_VERSION, requireCompatibleStorageSchema, requi
|
|
|
46
45
|
export const STORAGE_STATE_FILE = "state.json";
|
|
47
46
|
/** The root StorageState schema is the persisted aggregate document version. */
|
|
48
47
|
export const CURRENT_STORAGE_STATE_SCHEMA_VERSION = CURRENT_AGGREGATE_SCHEMA_VERSION;
|
|
49
|
-
export const CURRENT_CONFIG_SCHEMA_VERSION =
|
|
48
|
+
export const CURRENT_CONFIG_SCHEMA_VERSION = 3;
|
|
50
49
|
export const CURRENT_HOME_IDENTITY_SCHEMA_VERSION = 1;
|
|
51
50
|
export const CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION = 3;
|
|
52
51
|
/**
|
|
@@ -113,7 +112,7 @@ export function executionLaneActiveRunKeyParts(key) {
|
|
|
113
112
|
}
|
|
114
113
|
}
|
|
115
114
|
/** The schema version of each persisted `state.json#/tasks/*` aggregate. */
|
|
116
|
-
export const CURRENT_STORED_TASK_SCHEMA_VERSION =
|
|
115
|
+
export const CURRENT_STORED_TASK_SCHEMA_VERSION = 18;
|
|
117
116
|
/**
|
|
118
117
|
* Persisted nested-record versions consumed by this store's strict parser.
|
|
119
118
|
* Keep these named at the storage boundary so the upgrade record-axis map can
|
|
@@ -1730,22 +1729,16 @@ export class FileTaskStore {
|
|
|
1730
1729
|
this.saveWorkMailbox(consumePendingBatch(mailbox, "normal"));
|
|
1731
1730
|
}
|
|
1732
1731
|
getLeaderFailure(taskId) { return optional(this.#state().tasks[taskId]?.leaderFailure ?? undefined); }
|
|
1733
|
-
saveLeaderFailure(value) {
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
clearOperatorNotification(taskId) { this.#clearSingleton(taskId, "operatorNotification"); }
|
|
1738
|
-
#saveSingleton(taskId, key, value, label) {
|
|
1739
|
-
const schemaVersion = key === "leaderFailure"
|
|
1740
|
-
? CURRENT_LEADER_FAILURE_SCHEMA_VERSION
|
|
1741
|
-
: CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION;
|
|
1742
|
-
const stored = identified(value, schemaVersion, "taskId", taskId, label);
|
|
1743
|
-
this.#requireTaskForWrite(taskId);
|
|
1744
|
-
this.#mutate((state) => { state.tasks[taskId][key] = stored; });
|
|
1732
|
+
saveLeaderFailure(value) {
|
|
1733
|
+
const stored = identified(value, CURRENT_LEADER_FAILURE_SCHEMA_VERSION, "taskId", value.taskId, "Leader failure");
|
|
1734
|
+
this.#requireTaskForWrite(value.taskId);
|
|
1735
|
+
this.#mutate((state) => { state.tasks[value.taskId].leaderFailure = stored; });
|
|
1745
1736
|
}
|
|
1746
|
-
|
|
1747
|
-
this.#mutate((state) => {
|
|
1748
|
-
state.tasks[
|
|
1737
|
+
clearLeaderFailure(taskId) {
|
|
1738
|
+
this.#mutate((state) => {
|
|
1739
|
+
if (state.tasks[taskId] !== undefined)
|
|
1740
|
+
state.tasks[taskId].leaderFailure = null;
|
|
1741
|
+
});
|
|
1749
1742
|
}
|
|
1750
1743
|
#saveTaskRecord(taskId, key, value, label) {
|
|
1751
1744
|
const record = versioned(value, CURRENT_MESSAGE_SCHEMA_VERSION, label);
|
|
@@ -2014,8 +2007,7 @@ function emptyStoredTask(task) {
|
|
|
2014
2007
|
capabilityGrants: {},
|
|
2015
2008
|
releaseWorkflows: {},
|
|
2016
2009
|
publicationReferences: {},
|
|
2017
|
-
leaderFailure: null
|
|
2018
|
-
operatorNotification: null
|
|
2010
|
+
leaderFailure: null
|
|
2019
2011
|
};
|
|
2020
2012
|
}
|
|
2021
2013
|
function emptyTaskIdHighWaterMarks() {
|
|
@@ -2233,8 +2225,7 @@ function parseStoredTask(value, taskId) {
|
|
|
2233
2225
|
"capabilityGrants",
|
|
2234
2226
|
"releaseWorkflows",
|
|
2235
2227
|
"publicationReferences",
|
|
2236
|
-
"leaderFailure"
|
|
2237
|
-
"operatorNotification"
|
|
2228
|
+
"leaderFailure"
|
|
2238
2229
|
], `Task aggregate ${taskId}`);
|
|
2239
2230
|
parseMap(aggregate.changeSets, (record, key) => {
|
|
2240
2231
|
const changeSet = identifiedChangeSet(record, key);
|
|
@@ -2434,14 +2425,8 @@ function parseStoredTask(value, taskId) {
|
|
|
2434
2425
|
}
|
|
2435
2426
|
return reference;
|
|
2436
2427
|
}, "publicationReferences");
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
if (record !== null) {
|
|
2440
|
-
const schemaVersion = key === "leaderFailure"
|
|
2441
|
-
? CURRENT_LEADER_FAILURE_SCHEMA_VERSION
|
|
2442
|
-
: CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION;
|
|
2443
|
-
identified(record, schemaVersion, "taskId", taskId, label);
|
|
2444
|
-
}
|
|
2428
|
+
if (aggregate.leaderFailure !== null) {
|
|
2429
|
+
identified(aggregate.leaderFailure, CURRENT_LEADER_FAILURE_SCHEMA_VERSION, "taskId", taskId, "Leader failure");
|
|
2445
2430
|
}
|
|
2446
2431
|
validateTaskIdHighWaterCoverage(aggregate, taskId);
|
|
2447
2432
|
return aggregate;
|
|
@@ -30,6 +30,7 @@ import { classifyStorage } from "../migration/index.js";
|
|
|
30
30
|
import { inspectStorageSchema } from "../storageSchema.js";
|
|
31
31
|
import { FileTaskStore, STORAGE_STATE_FILE, StorageRecordError } from "../taskStore.js";
|
|
32
32
|
import { SqliteTaskStore } from "../sqliteStore.js";
|
|
33
|
+
import { inspectSqliteSchemaMigrations } from "../sqliteSchema.js";
|
|
33
34
|
import { inspectSourceVersionState } from "./homeMigrationTarget.js";
|
|
34
35
|
import { readMigrationReceipt } from "./migrationReceipt.js";
|
|
35
36
|
/**
|
|
@@ -111,6 +112,44 @@ export function classifyHome(options) {
|
|
|
111
112
|
};
|
|
112
113
|
}
|
|
113
114
|
}
|
|
115
|
+
// Once the logical Home axes are current, the SQLite ledger is the only
|
|
116
|
+
// remaining upgrade coordinate. A valid applied prefix with pending entries
|
|
117
|
+
// is upgradeable in place, not corruption and not a reason to rebuild the
|
|
118
|
+
// database. Ordinary store opens still validate-and-refuse this state; only
|
|
119
|
+
// the explicit updater is allowed to apply it.
|
|
120
|
+
if (schema.status === "current"
|
|
121
|
+
&& schema.currentLayoutVersion >= 7
|
|
122
|
+
&& isFullyCurrent(source, latest)) {
|
|
123
|
+
let sqliteMigration;
|
|
124
|
+
try {
|
|
125
|
+
sqliteMigration = inspectLayout7SqliteMigrations(home);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
...base,
|
|
130
|
+
classification: {
|
|
131
|
+
verdict: "CORRUPTED",
|
|
132
|
+
status: "unsupported",
|
|
133
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
134
|
+
},
|
|
135
|
+
layoutVersion: schema.currentLayoutVersion,
|
|
136
|
+
aggregateVersion: schema.currentAggregateSchemaVersion
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (sqliteMigration.pendingVersions.length > 0) {
|
|
140
|
+
return {
|
|
141
|
+
...base,
|
|
142
|
+
classification: {
|
|
143
|
+
verdict: "MIGRATABLE",
|
|
144
|
+
status: "migration-required",
|
|
145
|
+
stepCount: sqliteMigration.pendingVersions.length
|
|
146
|
+
},
|
|
147
|
+
layoutVersion: schema.currentLayoutVersion,
|
|
148
|
+
aggregateVersion: schema.currentAggregateSchemaVersion,
|
|
149
|
+
sqliteMigration
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
114
153
|
// The reference graph can only be validated by the strict loader, which only
|
|
115
154
|
// understands the current versions. So run it exactly when every axis is
|
|
116
155
|
// already current (the plan would be a no-op); a throw there is genuine
|
|
@@ -136,6 +175,19 @@ export function classifyHome(options) {
|
|
|
136
175
|
: { incompatibleComponent: incompatibleComponentOf(schema) })
|
|
137
176
|
};
|
|
138
177
|
}
|
|
178
|
+
function inspectLayout7SqliteMigrations(home) {
|
|
179
|
+
const db = new Database(join(home, "yui.db"), {
|
|
180
|
+
readonly: true,
|
|
181
|
+
fileMustExist: true
|
|
182
|
+
});
|
|
183
|
+
try {
|
|
184
|
+
db.pragma("query_only = ON");
|
|
185
|
+
return inspectSqliteSchemaMigrations(db);
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
db.close();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
139
191
|
/**
|
|
140
192
|
* Load a Home whose every axis is already current through the strict store
|
|
141
193
|
* gate to detect real structural/reference corruption. This is only ever
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
3
4
|
import { scanControllerResourceInventory } from "../../controller/resourceInventoryLinux.js";
|
|
4
5
|
import { STORAGE_STATE_FILE } from "../taskStore.js";
|
|
6
|
+
import { SQLITE_LAYOUT_VERSION } from "../sqliteSchema.js";
|
|
7
|
+
import { inspectStorageSchema } from "../storageSchema.js";
|
|
5
8
|
/** Pure blocker policy shared by synthetic tests and the real read-only scan. */
|
|
6
9
|
export function classifyOfflineUpgradeFacts(facts) {
|
|
7
10
|
const blockers = [];
|
|
@@ -45,14 +48,13 @@ export async function inspectOfflineUpgradeInventory(home, environment = process
|
|
|
45
48
|
const raw = readRawFacts(home, inventory);
|
|
46
49
|
return classifyOfflineUpgradeFacts(raw);
|
|
47
50
|
}
|
|
48
|
-
function readRawFacts(home, inventory) {
|
|
51
|
+
function readRawFacts(home, inventory, state = readDurableStateObject(home), conservativeDurableSessions = false) {
|
|
49
52
|
const runs = [];
|
|
50
53
|
const sessions = [];
|
|
51
54
|
const inFlight = [];
|
|
52
55
|
const pendingCompletions = [];
|
|
53
56
|
const lifecycle = [];
|
|
54
57
|
const unknownRuntime = [];
|
|
55
|
-
const state = readStateObject(home);
|
|
56
58
|
// Failure to enumerate processes, panes, or sockets means absence cannot be
|
|
57
59
|
// proven. Ownership-load warnings are expected for an old record shape and
|
|
58
60
|
// are handled by the raw durable scan plus unowned live-pane check below.
|
|
@@ -61,6 +63,8 @@ function readRawFacts(home, inventory) {
|
|
|
61
63
|
}
|
|
62
64
|
for (const [taskId, aggregateValue] of Object.entries(record(state.tasks))) {
|
|
63
65
|
const aggregate = record(aggregateValue);
|
|
66
|
+
if (text(record(aggregate.task).status) === "retired")
|
|
67
|
+
continue;
|
|
64
68
|
for (const runValue of Object.values(record(aggregate.agentRuns))) {
|
|
65
69
|
const run = record(runValue);
|
|
66
70
|
const roleName = text(run.roleName);
|
|
@@ -75,6 +79,7 @@ function readRawFacts(home, inventory) {
|
|
|
75
79
|
roleName,
|
|
76
80
|
set: record(setValue),
|
|
77
81
|
inventory,
|
|
82
|
+
conservativeDurableSessions,
|
|
78
83
|
sessions,
|
|
79
84
|
inFlight,
|
|
80
85
|
pendingCompletions
|
|
@@ -86,6 +91,7 @@ function readRawFacts(home, inventory) {
|
|
|
86
91
|
roleName,
|
|
87
92
|
set: record(setValue),
|
|
88
93
|
inventory,
|
|
94
|
+
conservativeDurableSessions,
|
|
89
95
|
sessions,
|
|
90
96
|
inFlight,
|
|
91
97
|
pendingCompletions
|
|
@@ -97,7 +103,7 @@ function readRawFacts(home, inventory) {
|
|
|
97
103
|
const kind = text(target.kind);
|
|
98
104
|
if (kind !== "role-runtime" && kind !== "global-role-runtime")
|
|
99
105
|
continue;
|
|
100
|
-
if (mailbox
|
|
106
|
+
if (!mailboxHasRuntimeWork(mailbox))
|
|
101
107
|
continue;
|
|
102
108
|
lifecycle.push({
|
|
103
109
|
...(text(target.taskId) === undefined ? {} : { taskId: text(target.taskId) }),
|
|
@@ -170,7 +176,7 @@ function isUndeterminableNativeInventoryWarning(warning) {
|
|
|
170
176
|
|| warning.startsWith("Cannot inspect Unix sockets");
|
|
171
177
|
}
|
|
172
178
|
function collectSessionSet(options) {
|
|
173
|
-
const { taskId, roleName, set, inventory } = options;
|
|
179
|
+
const { taskId, roleName, set, inventory, conservativeDurableSessions } = options;
|
|
174
180
|
const currentSessions = Object.entries(record(set.sessions));
|
|
175
181
|
const activeAgentId = text(set.activeAgentId);
|
|
176
182
|
const historyValue = set.history;
|
|
@@ -199,7 +205,7 @@ function collectSessionSet(options) {
|
|
|
199
205
|
};
|
|
200
206
|
options.sessions.push({
|
|
201
207
|
...base,
|
|
202
|
-
processState: processState(inventory, base)
|
|
208
|
+
processState: processState(inventory, base, conservativeDurableSessions)
|
|
203
209
|
});
|
|
204
210
|
}
|
|
205
211
|
const flight = record(set.inFlight);
|
|
@@ -245,12 +251,22 @@ function activeCurrentSession(sessions, taskId, roleName) {
|
|
|
245
251
|
return current.find(({ active }) => active === true)
|
|
246
252
|
?? (current.length === 1 ? current[0] : undefined);
|
|
247
253
|
}
|
|
248
|
-
function processState(inventory, session) {
|
|
254
|
+
function processState(inventory, session, conservativeDurableSessions) {
|
|
249
255
|
const matching = inventory.resources.filter((resource) => (resource.kind === "agent-session" && resourceMatchesSession(resource, session)));
|
|
250
256
|
if (matching.some((resource) => resource.processes.length > 0))
|
|
251
257
|
return "live";
|
|
252
258
|
if (matching.some((resource) => resource.paneDead === false))
|
|
253
259
|
return "unknown";
|
|
260
|
+
// The final SQLite gate runs after BEGIN IMMEDIATE, so no older writer can
|
|
261
|
+
// commit another Session record behind it. Treat every current non-terminal
|
|
262
|
+
// durable Session as active even when the earlier native-process snapshot
|
|
263
|
+
// did not contain it; this closes the inventory-scan -> transaction race.
|
|
264
|
+
if (conservativeDurableSessions
|
|
265
|
+
&& !session.history
|
|
266
|
+
&& session.status !== "stopped"
|
|
267
|
+
&& session.status !== "broken") {
|
|
268
|
+
return "unknown";
|
|
269
|
+
}
|
|
254
270
|
return "stopped";
|
|
255
271
|
}
|
|
256
272
|
function resourceMatchesSession(resource, session) {
|
|
@@ -272,13 +288,135 @@ function resourceMatchesSession(resource, session) {
|
|
|
272
288
|
return false;
|
|
273
289
|
return true;
|
|
274
290
|
}
|
|
275
|
-
function
|
|
291
|
+
function readDurableStateObject(home) {
|
|
292
|
+
const databasePath = join(home, "yui.db");
|
|
293
|
+
const schema = inspectStorageSchema(home);
|
|
294
|
+
const sqliteIsAuthoritative = (schema.status === "current" || schema.status === "unsupported")
|
|
295
|
+
&& schema.currentLayoutVersion >= SQLITE_LAYOUT_VERSION;
|
|
296
|
+
if (sqliteIsAuthoritative && existsSync(databasePath)) {
|
|
297
|
+
return readSqliteStateObject(databasePath);
|
|
298
|
+
}
|
|
276
299
|
const path = join(home, STORAGE_STATE_FILE);
|
|
277
300
|
if (!existsSync(path))
|
|
278
301
|
return {};
|
|
279
302
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
280
303
|
return record(parsed);
|
|
281
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Read only the durable runtime families needed by the offline gate. This raw
|
|
307
|
+
* adapter deliberately does not open SqliteTaskStore: a valid pending schema
|
|
308
|
+
* prefix is exactly the state the explicit upgrader must be able to inspect.
|
|
309
|
+
*/
|
|
310
|
+
function readSqliteStateObject(databasePath) {
|
|
311
|
+
const db = new Database(databasePath, { readonly: true, fileMustExist: true });
|
|
312
|
+
try {
|
|
313
|
+
db.pragma("query_only = ON");
|
|
314
|
+
return readSqliteStateObjectFromDatabase(db);
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
db.close();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Re-check the authoritative SQLite runtime families on an already-open
|
|
322
|
+
* connection. The in-place upgrader calls this inside its write transaction,
|
|
323
|
+
* after every older writer has either committed or rolled back.
|
|
324
|
+
*/
|
|
325
|
+
export function inspectSqliteDurableUpgradeInventory(home, db) {
|
|
326
|
+
return classifyOfflineUpgradeFacts(readRawFacts(home, { resources: [], warnings: [] }, readSqliteStateObjectFromDatabase(db), true));
|
|
327
|
+
}
|
|
328
|
+
function readSqliteStateObjectFromDatabase(db) {
|
|
329
|
+
const taskRows = db.prepare("SELECT task_id, status FROM tasks_catalog").all();
|
|
330
|
+
// Explicit retirement is the only isolation boundary for anomalous
|
|
331
|
+
// historical runtime state. Completed/archived/draft Tasks still fail
|
|
332
|
+
// closed if they retain an active Run or unfinished lifecycle record.
|
|
333
|
+
const retiredTaskIds = new Set(taskRows.filter(({ status }) => status === "retired").map(({ task_id }) => task_id));
|
|
334
|
+
const tasks = Object.fromEntries(taskRows
|
|
335
|
+
.filter(({ task_id }) => !retiredTaskIds.has(task_id))
|
|
336
|
+
.map(({ task_id, status }) => [task_id, {
|
|
337
|
+
task: { status },
|
|
338
|
+
agentRuns: {},
|
|
339
|
+
roleSessionSets: {}
|
|
340
|
+
}]));
|
|
341
|
+
const taskAggregate = (taskId) => {
|
|
342
|
+
const existing = tasks[taskId];
|
|
343
|
+
if (existing !== undefined)
|
|
344
|
+
return existing;
|
|
345
|
+
const created = { task: {}, agentRuns: {}, roleSessionSets: {} };
|
|
346
|
+
tasks[taskId] = created;
|
|
347
|
+
return created;
|
|
348
|
+
};
|
|
349
|
+
for (const row of db.prepare("SELECT task_id, run_id, role_name, status, payload FROM agent_runs").all()) {
|
|
350
|
+
if (retiredTaskIds.has(row.task_id))
|
|
351
|
+
continue;
|
|
352
|
+
const aggregate = taskAggregate(row.task_id);
|
|
353
|
+
record(aggregate.agentRuns)[row.run_id] = {
|
|
354
|
+
...parseJsonRecord(row.payload, "agent_runs.payload"),
|
|
355
|
+
id: row.run_id,
|
|
356
|
+
roleName: row.role_name,
|
|
357
|
+
status: row.status
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
for (const row of db.prepare("SELECT task_id, role_name, payload FROM role_session_sets").all()) {
|
|
361
|
+
if (retiredTaskIds.has(row.task_id))
|
|
362
|
+
continue;
|
|
363
|
+
const aggregate = taskAggregate(row.task_id);
|
|
364
|
+
record(aggregate.roleSessionSets)[row.role_name] = parseJsonRecord(row.payload, "role_session_sets.payload");
|
|
365
|
+
}
|
|
366
|
+
const globalRoleSessionSets = {};
|
|
367
|
+
for (const row of db.prepare("SELECT name, payload FROM global_role_session_sets").all()) {
|
|
368
|
+
globalRoleSessionSets[row.name] = parseJsonRecord(row.payload, "global_role_session_sets.payload");
|
|
369
|
+
}
|
|
370
|
+
const mailboxColumns = new Set(db.pragma("table_info(mailboxes)").map(({ name }) => name));
|
|
371
|
+
const hasInputDelivery = mailboxColumns.has("input_delivery");
|
|
372
|
+
const mailboxes = {};
|
|
373
|
+
for (const row of db.prepare(`SELECT target_key, target_kind, task_id, role_name, processing, pending${hasInputDelivery ? ", input_delivery" : ""} FROM mailboxes`).all()) {
|
|
374
|
+
if (row.task_id !== null && retiredTaskIds.has(row.task_id))
|
|
375
|
+
continue;
|
|
376
|
+
mailboxes[row.target_key] = {
|
|
377
|
+
target: {
|
|
378
|
+
kind: row.target_kind,
|
|
379
|
+
...(row.task_id === null ? {} : { taskId: row.task_id }),
|
|
380
|
+
...(row.role_name === null ? {} : { roleName: row.role_name })
|
|
381
|
+
},
|
|
382
|
+
processing: parseNullableJson(row.processing, "mailboxes.processing"),
|
|
383
|
+
pending: parseNullableJson(row.pending, "mailboxes.pending"),
|
|
384
|
+
inputDelivery: parseNullableJson(row.input_delivery ?? null, "mailboxes.input_delivery")
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
return { tasks, globalRoleSessionSets, mailboxes };
|
|
388
|
+
}
|
|
389
|
+
function mailboxHasRuntimeWork(mailbox) {
|
|
390
|
+
if (mailbox.processing !== null && mailbox.processing !== undefined)
|
|
391
|
+
return true;
|
|
392
|
+
if (mailbox.inputDelivery !== null && mailbox.inputDelivery !== undefined)
|
|
393
|
+
return true;
|
|
394
|
+
if (mailbox.pending === null || mailbox.pending === undefined)
|
|
395
|
+
return false;
|
|
396
|
+
const pending = record(mailbox.pending);
|
|
397
|
+
if (Object.hasOwn(pending, "normal") || Object.hasOwn(pending, "userCorrection")) {
|
|
398
|
+
return pending.normal !== null || pending.userCorrection !== null;
|
|
399
|
+
}
|
|
400
|
+
// Pre-v14 mailboxes store one pending batch directly.
|
|
401
|
+
return Object.keys(pending).length > 0;
|
|
402
|
+
}
|
|
403
|
+
function parseJsonRecord(raw, label) {
|
|
404
|
+
const parsed = JSON.parse(raw);
|
|
405
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
406
|
+
throw new Error(`${label} is not a JSON object.`);
|
|
407
|
+
}
|
|
408
|
+
return parsed;
|
|
409
|
+
}
|
|
410
|
+
function parseNullableJson(raw, label) {
|
|
411
|
+
if (raw === null)
|
|
412
|
+
return null;
|
|
413
|
+
try {
|
|
414
|
+
return JSON.parse(raw);
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
throw new Error(`${label} is not valid JSON.`, { cause: error });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
282
420
|
function countPendingInbox(home) {
|
|
283
421
|
return [join(home, "runtime", "inbox"), join(home, "runtime", "inbox-invalid")]
|
|
284
422
|
.reduce((total, directory) => total + countDirectory(directory), 0);
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
import { CURRENT_AGGREGATE_SCHEMA_VERSION, CURRENT_STORAGE_LAYOUT_VERSION } from "../storageVersions.js";
|
|
19
19
|
import { CURRENT_AGENT_RUN_SCHEMA_VERSION, CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION, CURRENT_AGENT_PROFILE_SCHEMA_VERSION, CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION, CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION, CURRENT_CHANGE_SET_SCHEMA_VERSION, CURRENT_CONTEXT_SNAPSHOT_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_INTEGRATION_QUEUE_SCHEMA_VERSION, CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION, CURRENT_MESSAGE_SCHEMA_VERSION, CURRENT_MILESTONE_SCHEMA_VERSION, CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION, CURRENT_PROJECT_SCHEMA_VERSION, CURRENT_RELEASE_WORKFLOW_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
20
|
import { CURRENT_LEADER_FAILURE_SCHEMA_VERSION } from "../../scheduler/leaderFailure.js";
|
|
21
|
-
import { CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION } from "../../scheduler/operatorNotification.js";
|
|
22
21
|
import { CURRENT_TASK_WAKE_SCHEMA_VERSION } from "../../scheduler/taskWake.js";
|
|
23
22
|
import { CURRENT_DURABLE_JOB_SCHEMA_VERSION } from "../../job/durableJob.js";
|
|
24
23
|
function descriptor(version, path) {
|
|
@@ -118,7 +117,9 @@ function getCurrentRecordDescriptors() {
|
|
|
118
117
|
releaseWorkflow: descriptor(CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION, "state.json#/tasks/*/releaseWorkflows"),
|
|
119
118
|
publicationReference: descriptor(CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION, "state.json#/tasks/*/publicationReferences"),
|
|
120
119
|
leaderFailure: descriptor(CURRENT_LEADER_FAILURE_SCHEMA_VERSION, "state.json#/tasks/*/leaderFailure"),
|
|
121
|
-
|
|
120
|
+
// Historical family retained in the version graph so StoredTask v17
|
|
121
|
+
// Homes can migrate; current v18 aggregates never store this field.
|
|
122
|
+
operatorNotification: descriptor(1, "state.json#/tasks/*/operatorNotification"),
|
|
122
123
|
workMailbox: descriptor(CURRENT_WORK_MAILBOX_SCHEMA_VERSION, "state.json#/mailboxes")
|
|
123
124
|
});
|
|
124
125
|
}
|
|
@@ -149,6 +149,12 @@ export function createSqliteMigrationTarget(options) {
|
|
|
149
149
|
}
|
|
150
150
|
const stamp = now().toISOString().replace(/[:.]/g, "-");
|
|
151
151
|
let backupPath;
|
|
152
|
+
const schemaPath = join(home, STORAGE_SCHEMA_FILE);
|
|
153
|
+
const sourceSchemaText = readFileSync(schemaPath, "utf8");
|
|
154
|
+
const receiptPath = migrationReceiptPath(home);
|
|
155
|
+
const sourceReceiptText = existsSync(receiptPath)
|
|
156
|
+
? readFileSync(receiptPath, "utf8")
|
|
157
|
+
: null;
|
|
152
158
|
// Phase 1: back up any existing yui.db, then promote the sidecar.
|
|
153
159
|
try {
|
|
154
160
|
if (existsSync(committedDbPath)) {
|
|
@@ -185,7 +191,7 @@ export function createSqliteMigrationTarget(options) {
|
|
|
185
191
|
// Phase 2: advance schema.json to layout 7 in the same critical section.
|
|
186
192
|
try {
|
|
187
193
|
if (stagedSchemaManifest !== null) {
|
|
188
|
-
writeTextFileAtomically(
|
|
194
|
+
writeTextFileAtomically(schemaPath, `${JSON.stringify(stagedSchemaManifest, null, 2)}\n`);
|
|
189
195
|
}
|
|
190
196
|
// Certify the dual-copy state (Issue 01): the 6→7 migration retains
|
|
191
197
|
// state.json read-only, so the Home legitimately holds both copies.
|
|
@@ -204,9 +210,22 @@ export function createSqliteMigrationTarget(options) {
|
|
|
204
210
|
}
|
|
205
211
|
}
|
|
206
212
|
catch (error) {
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
213
|
+
// A sidecar or receipt writer can fail after an atomic rename. Restore
|
|
214
|
+
// the exact source metadata as well as the database before claiming the
|
|
215
|
+
// layout switch did not commit.
|
|
216
|
+
let rollbackError;
|
|
217
|
+
try {
|
|
218
|
+
writeTextFileAtomically(schemaPath, sourceSchemaText);
|
|
219
|
+
if (sourceReceiptText === null) {
|
|
220
|
+
rmSync(receiptPath, { force: true });
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
writeTextFileAtomically(receiptPath, sourceReceiptText);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch (restoreError) {
|
|
227
|
+
rollbackError = restoreError;
|
|
228
|
+
}
|
|
210
229
|
try {
|
|
211
230
|
if (backupPath !== undefined && existsSync(backupPath)) {
|
|
212
231
|
removeSqliteFileSet(committedDbPath);
|
|
@@ -216,14 +235,17 @@ export function createSqliteMigrationTarget(options) {
|
|
|
216
235
|
removeSqliteFileSet(committedDbPath);
|
|
217
236
|
}
|
|
218
237
|
}
|
|
219
|
-
catch {
|
|
238
|
+
catch (restoreError) {
|
|
239
|
+
rollbackError ??= restoreError;
|
|
240
|
+
}
|
|
241
|
+
if (rollbackError !== undefined) {
|
|
220
242
|
throw new AmbiguousSwitchError({
|
|
221
243
|
homePath: home,
|
|
222
244
|
backupPath: backupPath ?? committedDbPath,
|
|
223
245
|
stagingPath: stagedDbPath,
|
|
224
|
-
detail: `SQLite database was promoted but schema.json could not be advanced (${messageOf(error)}), ` +
|
|
225
|
-
`and the
|
|
226
|
-
`
|
|
246
|
+
detail: `SQLite database was promoted but schema.json or its receipt could not be durably advanced (${messageOf(error)}), ` +
|
|
247
|
+
`and the database/metadata rollback also failed (${messageOf(rollbackError)}). ` +
|
|
248
|
+
`Inspect ${committedDbPath}, ${schemaPath}, ${receiptPath}, and the backup before recovery.`
|
|
227
249
|
});
|
|
228
250
|
}
|
|
229
251
|
throw error;
|
|
@@ -38,7 +38,7 @@ import { AmbiguousSwitchError } from "../migration/index.js";
|
|
|
38
38
|
import { describeActiveRuntime, homeRuntimeIsActive, inspectHomeRuntime, inspectSourceVersionState, inspectSnapshotVersionState } from "./homeMigrationTarget.js";
|
|
39
39
|
import { writeSwitchProgress } from "./switchProgress.js";
|
|
40
40
|
import { moveSqliteFileSet, removeSqliteFileSet } from "./sqliteFileSet.js";
|
|
41
|
-
import { COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME, copySqlitePassthroughState, computeDbFamilyChecksums, computeStateFamilyChecksums, populateSqliteFromState,
|
|
41
|
+
import { COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME, copySqlitePassthroughState, computeDbFamilyChecksums, computeStateFamilyChecksums, populateSqliteFromState, readMigrationSourceStateFromSqlite } from "./sqliteStateMigration.js";
|
|
42
42
|
/** Build the SQLite-backed record-migration target. */
|
|
43
43
|
export function createSqliteRecordMigrationTarget(options) {
|
|
44
44
|
const home = options.home;
|
|
@@ -70,7 +70,7 @@ export function createSqliteRecordMigrationTarget(options) {
|
|
|
70
70
|
readSource() {
|
|
71
71
|
const manifestRaw = readFileSync(join(home, STORAGE_SCHEMA_FILE), "utf8");
|
|
72
72
|
const schemaManifest = parseJsonObject(manifestRaw, STORAGE_SCHEMA_FILE);
|
|
73
|
-
const state =
|
|
73
|
+
const state = readMigrationSourceStateFromSqlite(home);
|
|
74
74
|
return Object.freeze({ schemaManifest, state });
|
|
75
75
|
},
|
|
76
76
|
writeFreshOutput(snapshot) {
|
|
@@ -129,6 +129,8 @@ export function createSqliteRecordMigrationTarget(options) {
|
|
|
129
129
|
}
|
|
130
130
|
const stamp = now().toISOString().replace(/[:.]/g, "-");
|
|
131
131
|
let backupPath;
|
|
132
|
+
const schemaPath = join(home, STORAGE_SCHEMA_FILE);
|
|
133
|
+
const sourceSchemaText = readFileSync(schemaPath, "utf8");
|
|
132
134
|
// Phase 1: back up the existing yui.db, then promote the sidecar.
|
|
133
135
|
try {
|
|
134
136
|
if (existsSync(committedDbPath)) {
|
|
@@ -167,13 +169,20 @@ export function createSqliteRecordMigrationTarget(options) {
|
|
|
167
169
|
// critical section. The layout version is unchanged.
|
|
168
170
|
try {
|
|
169
171
|
if (stagedSchemaManifest !== null) {
|
|
170
|
-
writeTextFileAtomically(
|
|
172
|
+
writeTextFileAtomically(schemaPath, `${JSON.stringify(stagedSchemaManifest, null, 2)}\n`);
|
|
171
173
|
}
|
|
172
174
|
}
|
|
173
175
|
catch (error) {
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
176
|
+
// A durable writer can report an fsync failure after its atomic rename,
|
|
177
|
+
// so "write threw" does not prove schema.json stayed old. Restore both
|
|
178
|
+
// the exact source sidecar and database before claiming no switch.
|
|
179
|
+
let rollbackError;
|
|
180
|
+
try {
|
|
181
|
+
writeTextFileAtomically(schemaPath, sourceSchemaText);
|
|
182
|
+
}
|
|
183
|
+
catch (restoreError) {
|
|
184
|
+
rollbackError = restoreError;
|
|
185
|
+
}
|
|
177
186
|
try {
|
|
178
187
|
if (backupPath !== undefined && existsSync(backupPath)) {
|
|
179
188
|
removeSqliteFileSet(committedDbPath);
|
|
@@ -183,15 +192,18 @@ export function createSqliteRecordMigrationTarget(options) {
|
|
|
183
192
|
removeSqliteFileSet(committedDbPath);
|
|
184
193
|
}
|
|
185
194
|
}
|
|
186
|
-
catch {
|
|
195
|
+
catch (restoreError) {
|
|
196
|
+
rollbackError ??= restoreError;
|
|
197
|
+
}
|
|
198
|
+
if (rollbackError !== undefined) {
|
|
187
199
|
writeInterruptedMarker(home, backupPath ?? committedDbPath, stagedDbPath, now);
|
|
188
200
|
throw new AmbiguousSwitchError({
|
|
189
201
|
homePath: home,
|
|
190
202
|
backupPath: backupPath ?? committedDbPath,
|
|
191
203
|
stagingPath: stagedDbPath,
|
|
192
|
-
detail: `SQLite database was promoted but schema.json could not be advanced (${messageOf(error)}), ` +
|
|
193
|
-
`and the
|
|
194
|
-
`
|
|
204
|
+
detail: `SQLite database was promoted but schema.json could not be durably advanced (${messageOf(error)}), ` +
|
|
205
|
+
`and the database/schema rollback also failed (${messageOf(rollbackError)}). ` +
|
|
206
|
+
`Inspect ${committedDbPath}, ${schemaPath}, and the backup before recovery.`
|
|
195
207
|
});
|
|
196
208
|
}
|
|
197
209
|
throw error;
|
|
@@ -214,7 +226,7 @@ export function createSqliteRecordMigrationTarget(options) {
|
|
|
214
226
|
function readSourceFresh() {
|
|
215
227
|
const manifestRaw = readFileSync(join(home, STORAGE_SCHEMA_FILE), "utf8");
|
|
216
228
|
const schemaManifest = parseJsonObject(manifestRaw, STORAGE_SCHEMA_FILE);
|
|
217
|
-
const state =
|
|
229
|
+
const state = readMigrationSourceStateFromSqlite(home);
|
|
218
230
|
return Object.freeze({ schemaManifest, state });
|
|
219
231
|
}
|
|
220
232
|
/**
|