@zq-silk/yui 0.15.8 → 0.15.11
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 +2 -0
- package/ARCHITECTURE.zh-CN.md +151 -0
- package/README.md +211 -14
- package/dist/agent/launchEnvironment.js +7 -0
- package/dist/artifacts/artifactCapability.js +74 -0
- package/dist/artifacts/artifactCommitLock.js +249 -0
- package/dist/artifacts/artifactPaths.js +151 -0
- package/dist/artifacts/gitArtifactRef.js +146 -0
- package/dist/artifacts/managedGit.js +332 -0
- package/dist/artifacts/taskArtifactRepository.js +277 -0
- package/dist/cli/commandCatalog.js +40 -16
- package/dist/cli/interactionPolicy.js +3 -3
- package/dist/cli/updateOrchestrator.js +24 -1
- package/dist/cli/updatePorts.js +7 -3
- package/dist/cli/upgradeCommand.js +42 -2
- package/dist/cli.js +403 -93
- package/dist/commands/globalRoleCommands.js +314 -4
- package/dist/commands/operatorCommands.js +33 -2
- package/dist/commands/projectCommands.js +6 -7
- package/dist/commands/releaseCommands.js +18 -0
- package/dist/commands/taskActivationCommands.js +22 -0
- package/dist/commands/taskActor.js +25 -0
- package/dist/commands/taskCommands.js +846 -155
- package/dist/commands/taskIntegrationCommands.js +16 -38
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
- package/dist/commands/taskRoleRuntimeStatus.js +35 -0
- package/dist/context/runContextPack.js +28 -16
- package/dist/context/taskContext.js +64 -5
- package/dist/controller/agentHostObservation.js +155 -0
- package/dist/controller/clientRuntime.js +17 -2
- package/dist/controller/controller.js +11 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
- package/dist/controller/globalInputDelivery.js +119 -0
- package/dist/controller/jobControl.js +6 -2
- package/dist/controller/resourceInventory.js +14 -4
- package/dist/controller/resourceInventoryLinux.js +2 -6
- package/dist/controller/runtime.js +81 -6
- package/dist/controller/runtimeEventInbox.js +32 -3
- package/dist/controller/runtimeEventProcessor.js +26 -6
- package/dist/controller/runtimeHookRunFence.js +75 -19
- package/dist/controller/structuredProviderObservation.js +133 -70
- package/dist/coordination/workMailboxQueue.js +5 -0
- package/dist/execution/workItemExecutionProjection.js +1 -1
- package/dist/executor/agentExecutor.js +64 -4
- package/dist/executor/executorRegistry.js +3 -0
- package/dist/executor/fileRoleLaunchPlanner.js +78 -118
- package/dist/integration/deliveryObligation.js +2 -1
- package/dist/integration/gitIntegrationService.js +312 -382
- package/dist/integration/integrationAttempt.js +30 -4
- package/dist/integration/integrationQueueService.js +7 -7
- package/dist/integration/integrationSourceApplication.js +323 -0
- package/dist/kernel/builtinCapabilities.js +32 -24
- package/dist/message/globalInterrupt.js +33 -0
- package/dist/message/inputControlResolution.js +106 -0
- package/dist/message/message.js +423 -0
- package/dist/message/messageContinuation.js +126 -3
- package/dist/message/taskInterrupt.js +34 -0
- package/dist/observability/orchestrationMetrics.js +1 -1
- package/dist/plugins/pluginService.js +11 -3
- package/dist/release/releaseHandover.js +22 -0
- package/dist/release/releaseWorkflowPorts.js +15 -7
- package/dist/repository/gitWorkspace.js +72 -15
- package/dist/repository/taskWorkspaceCoordinator.js +134 -0
- package/dist/repository/taskWorkspacePreparer.js +120 -49
- package/dist/repository/workItemCandidateSnapshot.js +34 -0
- package/dist/resources/projectResource.js +0 -48
- package/dist/resources/projectResourceService.js +3 -81
- package/dist/resources/resourceDiscovery.js +3 -2
- package/dist/runtime/agentHost.js +152 -72
- package/dist/runtime/agentHostCompatibility.js +127 -0
- package/dist/runtime/agentHostProtocol.js +53 -0
- package/dist/runtime/executionEnvironment.js +0 -19
- package/dist/runtime/launchBroker.js +6 -0
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/taskRuntimeIsolation.js +30 -6
- package/dist/runtime/tmuxAdapters.js +5 -3
- package/dist/scheduler/operatorEvent.js +4 -0
- package/dist/scheduler/taskExecutionProjection.js +12 -1
- package/dist/scheduler/wakeReason.js +7 -1
- package/dist/scheduler/wakeupQueue.js +2 -0
- package/dist/setup/setupCommand.js +29 -16
- package/dist/storage/homeLayout.js +130 -0
- package/dist/storage/migrations/artifactsToGit.js +338 -0
- package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
- package/dist/storage/migrations/integrationContinuation.js +104 -0
- package/dist/storage/migrations/submitIntent.js +126 -0
- package/dist/storage/migrations/unifyHomeLayout.js +925 -0
- package/dist/storage/sqliteSchema.js +173 -7
- package/dist/storage/sqliteStore.js +41 -22
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -1
- package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
- package/dist/task/archiveDiagnostics.js +128 -0
- package/dist/task/nextAction.js +44 -11
- package/dist/task/taskActivation.js +26 -0
- package/dist/task/taskActivationService.js +85 -69
- package/dist/task/taskSubmission.js +236 -0
- package/dist/web/assets/client/app.js +58 -2
- package/dist/web/assets/client/components.js +1 -0
- package/dist/web/assets/client/i18n.js +6 -0
- package/dist/web/assets/client/taskSurface.js +202 -7
- package/dist/web/assets/client/view.js +7 -4
- package/dist/web/assets/shell.js +23 -0
- package/dist/web/assets/styles/layout.js +1 -1
- package/dist/web/assets/styles/widgets.js +12 -0
- package/dist/web/webServer.js +135 -4
- package/dist/web/webSnapshot.js +4 -3
- package/dist/web/webTaskSurface.js +225 -8
- package/dist/workItem/workItem.js +14 -10
- package/dist/workspace/workItemChangeSetManager.js +18 -2
- package/docs/agent-result-consumption.md +2 -0
- package/docs/agent-result-consumption.zh-CN.md +81 -0
- package/docs/agent-runtime-drivers.md +2 -0
- package/docs/agent-runtime-drivers.zh-CN.md +77 -0
- package/docs/architecture/README.md +44 -32
- package/docs/architecture/README.zh-CN.md +43 -0
- package/docs/architecture/capabilities-and-resources.md +118 -79
- package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
- package/docs/managed-turn-and-session-runtime.md +2 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
- package/docs/observability/README.md +2 -0
- package/docs/observability/README.zh-CN.md +71 -0
- package/docs/plugin-sdk.md +320 -217
- package/docs/plugin-sdk.zh-CN.md +293 -0
- package/docs/provider-runtime.md +2 -0
- package/docs/provider-runtime.zh-CN.md +132 -0
- package/docs/release-workflow.md +41 -0
- package/docs/release-workflow.zh-CN.md +266 -0
- package/docs/roles-and-configuration.md +2 -0
- package/docs/roles-and-configuration.zh-CN.md +96 -0
- package/docs/sqlite-control-plane-design.md +225 -1
- package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
- package/docs/task-dag-semantics.md +80 -57
- package/docs/task-dag-semantics.zh-CN.md +59 -0
- package/docs/task-delivery.md +2 -0
- package/docs/task-delivery.zh-CN.md +82 -0
- package/docs/task-local-identity.md +2 -0
- package/docs/task-local-identity.zh-CN.md +58 -0
- package/docs/testing/verification-levels.md +26 -0
- package/docs/testing/verification-levels.zh-CN.md +80 -0
- package/i18n/README.zh-CN.md +199 -10
- package/package.json +2 -1
- package/skills/yui-leader/SKILL.md +88 -331
- package/skills/yui-leader/references/execution.md +405 -0
- package/skills/yui-leader/references/integration.md +52 -2
- package/skills/yui-leader/references/planning.md +109 -0
- package/skills/yui-leader/references/task-plugins.md +8 -4
- package/skills/yui-operator/SKILL.md +22 -4
- package/skills/yui-runtime/SKILL.md +27 -0
- package/skills/yui-runtime/references/publication.md +20 -0
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
import { createHash } from "node:crypto";
|
|
24
24
|
import { REMOVE_RUNTIME_GENERATION_SQL, removeRuntimeGenerationRecords } from "./migrations/removeRuntimeGeneration.js";
|
|
25
25
|
import { migrateAgentRunContract } from "./migrations/agentRunContract.js";
|
|
26
|
+
import { migrateArtifactsToGit } from "./migrations/artifactsToGit.js";
|
|
27
|
+
import { migrateIntegrationContinuation } from "./migrations/integrationContinuation.js";
|
|
28
|
+
import { UNIFY_HOME_LAYOUT_SQL, migrateUnifyHomeLayout } from "./migrations/unifyHomeLayout.js";
|
|
29
|
+
import { COLLAPSE_WORKTREE_LAYOUT_SQL, migrateCollapseWorktreeLayout } from "./migrations/collapseWorktreeLayout.js";
|
|
26
30
|
import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "./storageVersions.js";
|
|
27
31
|
/** Telemetry retention bounds (§4.4). Open question 3 in §11; defaults from the design. */
|
|
28
32
|
export const TELEMETRY_KEEP_PER_RUN = 200;
|
|
@@ -642,6 +646,27 @@ CREATE TABLE IF NOT EXISTS context_snapshots (
|
|
|
642
646
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_context_snapshots_scope_sequence
|
|
643
647
|
ON context_snapshots(task_id, scope, COALESCE(scope_ref, ''), sequence);
|
|
644
648
|
`;
|
|
649
|
+
/**
|
|
650
|
+
* The Global-owned Message store (decision-3 §9/§11). It extends the Message
|
|
651
|
+
* store to a Global owner exactly as `global_role_session_sets` parallels
|
|
652
|
+
* `role_session_sets`: keyed by the Global Role `name` with an FK to
|
|
653
|
+
* `global_roles`, never by a fabricated `task_id`, and never a new private
|
|
654
|
+
* queue. Ids are minted from the existing `global_sequences` counter, so a
|
|
655
|
+
* Global input has an explicit owner and an authorizable reference without
|
|
656
|
+
* reusing Task record shapes or Task permissions.
|
|
657
|
+
*/
|
|
658
|
+
const GLOBAL_ROLE_MESSAGE_SQL = `
|
|
659
|
+
CREATE TABLE IF NOT EXISTS global_role_messages (
|
|
660
|
+
name TEXT NOT NULL,
|
|
661
|
+
message_id TEXT NOT NULL,
|
|
662
|
+
seq INTEGER NOT NULL,
|
|
663
|
+
payload TEXT NOT NULL,
|
|
664
|
+
created_at TEXT NOT NULL,
|
|
665
|
+
PRIMARY KEY (name, message_id),
|
|
666
|
+
FOREIGN KEY (name) REFERENCES global_roles(name)
|
|
667
|
+
);
|
|
668
|
+
CREATE INDEX IF NOT EXISTS idx_global_role_messages_seq ON global_role_messages(name, seq);
|
|
669
|
+
`;
|
|
645
670
|
const MIGRATION_1_SQL = [
|
|
646
671
|
BASELINE_CORE_SQL,
|
|
647
672
|
BASELINE_JOB_AND_RELEASE_SQL,
|
|
@@ -1117,6 +1142,128 @@ UPDATE review_rounds SET payload = json_set(payload, '$.executionGroup.lanes', j
|
|
|
1117
1142
|
// now retain explicit replacement intent; OS owner records may identify
|
|
1118
1143
|
// the dedicated execution child, independently of a disposable Host.
|
|
1119
1144
|
sql: "SELECT 1; -- Replacement intent, independent retained native control evidence/process custody, and optional fixed TaskWake refs"
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
version: 19,
|
|
1148
|
+
name: "task-artifacts-local-git",
|
|
1149
|
+
introducedIn: "0.16.0",
|
|
1150
|
+
// Retire the DB-owned immutable Artifact store: file/directory artifacts now
|
|
1151
|
+
// live in a per-Task local Git repository, referenced by a self-certifying
|
|
1152
|
+
// `commit + relativePath`. The whole rewrite is payload work that must READ
|
|
1153
|
+
// the `artifacts` table before it is dropped, so it runs entirely in
|
|
1154
|
+
// `migrateData` (which executes after this `sql`) — the table is dropped
|
|
1155
|
+
// there, last, once its rows have been moved. Requirement A's independent,
|
|
1156
|
+
// deterministic submit-intent backfill (events + id_sequences only) is the
|
|
1157
|
+
// final step inside that same transaction. This `sql` is intentionally a
|
|
1158
|
+
// no-op: dropping the table here would destroy the rows before they move.
|
|
1159
|
+
//
|
|
1160
|
+
// The 18->19 contract also DECLARES three optional `messages.payload` fields
|
|
1161
|
+
// added by Requirement A — `intent` (record|discuss|develop), an optional
|
|
1162
|
+
// client idempotency `submissionKey`, and a frozen `submissionReceipt`
|
|
1163
|
+
// disposition. They add no column and no table and are only ever written
|
|
1164
|
+
// going forward (absent `intent` reads as discuss; absent key is keyless;
|
|
1165
|
+
// absent receipt is a pre-19 message), so NO historical row is rewritten and
|
|
1166
|
+
// no key or receipt is ever fabricated for old data — declaring them here
|
|
1167
|
+
// satisfies "optional still requires a migration declaration".
|
|
1168
|
+
sql: "SELECT 1; -- artifacts move to per-Task local Git; see migrateArtifactsToGit",
|
|
1169
|
+
migrateData: migrateArtifactsToGit
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
version: 20,
|
|
1173
|
+
name: "integration-conflict-continuation",
|
|
1174
|
+
introducedIn: "0.16.0",
|
|
1175
|
+
// Widen Integration status with conflicted; declare optional sourceProgress
|
|
1176
|
+
// (Git cursor/reflog action) and checkInputDigest. Exact old bound FF Job
|
|
1177
|
+
// receipts supply these facts; unprovable history remains unchanged.
|
|
1178
|
+
// Frozen Context and events are never rewritten.
|
|
1179
|
+
sql: "SELECT 1; -- Integration Git progress and exact check admission",
|
|
1180
|
+
migrateData: migrateIntegrationContinuation
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
version: 21,
|
|
1184
|
+
name: "force-archive-independent-cleanup",
|
|
1185
|
+
introducedIn: "0.16.0",
|
|
1186
|
+
// Declare optional task.archived force/cleanup/warnings/retainedResources
|
|
1187
|
+
// audit fields and task.archive-cleanup resource/status/detail/paths events;
|
|
1188
|
+
// runtime.event-obsolete may retain the full originalEvent as source evidence.
|
|
1189
|
+
// Archived Tasks may retain exact runtime/workspace/mailbox ownership.
|
|
1190
|
+
// Existing archive, completion and Publication history is unchanged.
|
|
1191
|
+
sql: "SELECT 1; -- archive intent and cleanup evidence remain separate facts"
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
version: 22,
|
|
1195
|
+
name: "controller-owned-agent-host-ingress",
|
|
1196
|
+
introducedIn: "0.16.0",
|
|
1197
|
+
// Add optional `host` source metadata to the stable Inbox v1 observation
|
|
1198
|
+
// envelope. Such facts contain no resolved Run: current Controller alone
|
|
1199
|
+
// validates ownership and commits observations, process custody and native
|
|
1200
|
+
// account locations. Existing Inbox facts and domain history stay intact.
|
|
1201
|
+
// Host status capabilities/transport diagnostics are live, not stored.
|
|
1202
|
+
sql: "SELECT 1; -- Controller-owned Host ingress; no historical data rewrite"
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
version: 23,
|
|
1206
|
+
name: "unified-message-input-control",
|
|
1207
|
+
introducedIn: "0.16.1",
|
|
1208
|
+
// Widen the Message payload with an optional durable input action
|
|
1209
|
+
// (queue/steer) plus its stable requestId, and an optional interrupt-then
|
|
1210
|
+
// handoff claim (requestId + exact target AgentRun). Existing messages carry
|
|
1211
|
+
// neither field and keep their current queue semantics and delivery history;
|
|
1212
|
+
// no native Turn identity is synthesized and no history is rewritten. The
|
|
1213
|
+
// same contiguous migration adds the Global-owned Message store
|
|
1214
|
+
// (global_role_messages), extending the Message store to a Global owner
|
|
1215
|
+
// exactly as global_role_session_sets parallels role_session_sets: an
|
|
1216
|
+
// explicit Global owner and authorizable reference, no fabricated Task and
|
|
1217
|
+
// no new private global queue (decision-3 §9/§11).
|
|
1218
|
+
sql: `SELECT 1; -- Message input actions, exact Session delivery pins, provider receipts and interrupt-then claims; Global Session provider bindings and native interrupt evidence\n${GLOBAL_ROLE_MESSAGE_SQL}`
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
version: 24,
|
|
1222
|
+
name: "unify-home-layout",
|
|
1223
|
+
introducedIn: "0.15.9",
|
|
1224
|
+
// Unify every Yui self-managed path under a single canonical YUI_HOME. The
|
|
1225
|
+
// managed Git worktrees move from the out-of-Home `defaultWorkspace` into
|
|
1226
|
+
// `<home>/workspaces/{worktree,tasks}` and the provider runtimes from the
|
|
1227
|
+
// `<home>.task-runtimes` sibling into `<home>/runtime/task-runtimes`. Only
|
|
1228
|
+
// the durable Git worktree tree is physically relocated, by COPY (never
|
|
1229
|
+
// rename-away): its replica is digest-verified against the source and
|
|
1230
|
+
// atomically published while the original is PRESERVED as the rollback
|
|
1231
|
+
// anchor; the regenerable task views and disposable runtimes are pointer
|
|
1232
|
+
// rewrites only. The copied worktrees are then repaired, and only the
|
|
1233
|
+
// persisted pointers the runtime trusts as live are rewritten. Frozen
|
|
1234
|
+
// evidence (Context snapshots, terminal Run Git evidence, candidate
|
|
1235
|
+
// snapshots, terminal Jobs, reports) and self-healing/self-rediscovered
|
|
1236
|
+
// records (Task cwd, active Role workspaces, views, resource registry) are
|
|
1237
|
+
// left intact; a queued/running Job bound under a relocating root is refused
|
|
1238
|
+
// rather than silently moved. The data/filesystem transform is the
|
|
1239
|
+
// substantive step; the SQL is inert.
|
|
1240
|
+
sql: UNIFY_HOME_LAYOUT_SQL,
|
|
1241
|
+
migrateData: migrateUnifyHomeLayout
|
|
1242
|
+
},
|
|
1243
|
+
{
|
|
1244
|
+
version: 25,
|
|
1245
|
+
name: "collapse-worktree-layout",
|
|
1246
|
+
introducedIn: "0.15.9",
|
|
1247
|
+
// Collapse the two-layer managed workspace layout into a single layer of real
|
|
1248
|
+
// Git worktrees addressed by Task/owner and Project. Each managed worktree
|
|
1249
|
+
// moves from its per-Project physical root
|
|
1250
|
+
// (`<home>/workspaces/worktree/<projectName>/<taskKey>/<roleKey>`) to the
|
|
1251
|
+
// location the v19 symlink view used to point at
|
|
1252
|
+
// (`<home>/workspaces/tasks/<taskId>/<owner>/<projectDirectory>`), so the
|
|
1253
|
+
// logical entry path and the physical worktree become the same directory and
|
|
1254
|
+
// the symlink-view indirection is gone; an integration attempt moves under a
|
|
1255
|
+
// new `tasks/<taskId>/integrations/<id>/` owner root, addressed by the bound
|
|
1256
|
+
// Project directory rather than the Project name. Only the durable Git
|
|
1257
|
+
// worktrees are physically relocated, by COPY (never rename-away): each
|
|
1258
|
+
// replica is digest-verified against the source and atomically published over
|
|
1259
|
+
// the regenerable view symlink while the original is PRESERVED as the
|
|
1260
|
+
// rollback anchor. The copied worktrees are then repaired, and only the
|
|
1261
|
+
// persisted pointers the runtime trusts as live are rewritten. Frozen
|
|
1262
|
+
// evidence and self-healing records are left intact; a queued/running Job
|
|
1263
|
+
// bound under a relocating worktree is refused rather than silently moved.
|
|
1264
|
+
// The data/filesystem transform is the substantive step; the SQL is inert.
|
|
1265
|
+
sql: COLLAPSE_WORKTREE_LAYOUT_SQL,
|
|
1266
|
+
migrateData: migrateCollapseWorktreeLayout
|
|
1120
1267
|
}
|
|
1121
1268
|
]);
|
|
1122
1269
|
for (let index = 0; index < MIGRATIONS.length; index += 1) {
|
|
@@ -1350,13 +1497,24 @@ export function migrateSqliteSchema(db, options) {
|
|
|
1350
1497
|
throw new SqliteSchemaMigrationError(`storage version ${applied.currentVersion} is older than the minimum supported `
|
|
1351
1498
|
+ `${MIN_SUPPORTED_STORAGE_VERSION}`, "admission");
|
|
1352
1499
|
}
|
|
1353
|
-
|
|
1500
|
+
// Production callers omit `throughVersion`, so the effective target is head
|
|
1501
|
+
// and behavior is unchanged; a partial target is honored only in apply mode.
|
|
1502
|
+
const effectiveTarget = options.mode === "apply" && options.throughVersion !== undefined
|
|
1503
|
+
? options.throughVersion
|
|
1504
|
+
: CURRENT_STORAGE_VERSION;
|
|
1505
|
+
const pending = MIGRATIONS.filter((migration) => !applied.versions.has(migration.version) && migration.version <= effectiveTarget);
|
|
1354
1506
|
if (!ledgerWasCreated && pending.length > 0 && options.mode === "validate") {
|
|
1355
1507
|
throw new SqliteSchemaMigrationError(`Storage version ${applied.currentVersion} requires an explicit upgrade to `
|
|
1356
1508
|
+ `${CURRENT_STORAGE_VERSION}`, "admission");
|
|
1357
1509
|
}
|
|
1510
|
+
// Bound the applied steps to `throughVersion` (default and clamp: head), so a
|
|
1511
|
+
// migration test can stop at one step's intermediate state while production
|
|
1512
|
+
// always advances to head. The validate-mode guard above still fires on the
|
|
1513
|
+
// FULL pending set — the bound never lets an ordinary open partially migrate.
|
|
1514
|
+
const targetVersion = Math.min(options.throughVersion ?? CURRENT_STORAGE_VERSION, CURRENT_STORAGE_VERSION);
|
|
1515
|
+
const toApply = pending.filter((migration) => migration.version <= targetVersion);
|
|
1358
1516
|
const newlyApplied = [];
|
|
1359
|
-
for (const migration of
|
|
1517
|
+
for (const migration of toApply) {
|
|
1360
1518
|
db.exec(migration.sql);
|
|
1361
1519
|
migration.migrateData?.(db);
|
|
1362
1520
|
const appliedAt = new Date().toISOString();
|
|
@@ -1364,19 +1522,26 @@ export function migrateSqliteSchema(db, options) {
|
|
|
1364
1522
|
VALUES (?, ?, ?, ?)`).run(migration.version, migration.name, appliedAt, checksum(migration.sql));
|
|
1365
1523
|
newlyApplied.push(migration.version);
|
|
1366
1524
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1525
|
+
// The table/index inventory describes the HEAD shape; only assert it once the
|
|
1526
|
+
// migration has actually advanced to head (a deliberate partial target stops
|
|
1527
|
+
// earlier and is validated when the real upgrade later completes it).
|
|
1528
|
+
if (effectiveTarget >= CURRENT_STORAGE_VERSION)
|
|
1529
|
+
validateSchemaObjects(db);
|
|
1530
|
+
// The true head reached: the highest already-applied version or, when this
|
|
1531
|
+
// run advanced the ledger, the last step it committed. Bounded runs report
|
|
1532
|
+
// the intermediate head; an unbounded run reports CURRENT_STORAGE_VERSION.
|
|
1533
|
+
const head = newlyApplied.at(-1) ?? applied.currentVersion;
|
|
1534
|
+
return { newlyApplied, head };
|
|
1369
1535
|
};
|
|
1370
|
-
const
|
|
1536
|
+
const outcome = options.mode === "apply" && !db.inTransaction
|
|
1371
1537
|
? db.transaction(migrate)()
|
|
1372
1538
|
: migrate();
|
|
1373
|
-
return { applied: newlyApplied, version:
|
|
1539
|
+
return { applied: outcome.newlyApplied, version: outcome.head };
|
|
1374
1540
|
}
|
|
1375
1541
|
/** The names of every table the schema creates (for tests/introspection). */
|
|
1376
1542
|
export const SQLITE_SCHEMA_TABLES = [
|
|
1377
1543
|
"plugin_intents",
|
|
1378
1544
|
"plugin_validations",
|
|
1379
|
-
"artifacts",
|
|
1380
1545
|
"local_resources",
|
|
1381
1546
|
"environment_preparations",
|
|
1382
1547
|
"schema_migrations",
|
|
@@ -1387,6 +1552,7 @@ export const SQLITE_SCHEMA_TABLES = [
|
|
|
1387
1552
|
"projects",
|
|
1388
1553
|
"global_roles",
|
|
1389
1554
|
"global_role_session_sets",
|
|
1555
|
+
"global_role_messages",
|
|
1390
1556
|
"global_sequences",
|
|
1391
1557
|
"tasks_catalog",
|
|
1392
1558
|
"managed_workspaces",
|
|
@@ -40,11 +40,12 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
40
40
|
import Database from "better-sqlite3";
|
|
41
41
|
import { validatePluginValidation } from "../plugins/pluginPackage.js";
|
|
42
42
|
import { validatePluginIntent, validatePluginIntentFailure } from "../plugins/pluginIntent.js";
|
|
43
|
-
import {
|
|
43
|
+
import { validateLocalResource, validateEnvironmentPreparation } from "../resources/projectResource.js";
|
|
44
44
|
import { validateConfiguredAgent } from "../agent/agent.js";
|
|
45
45
|
import { validateTaskBrief } from "../brief/taskBrief.js";
|
|
46
46
|
import { consumePendingBatch, mailboxTargetKey, validateWorkMailbox } from "../coordination/workMailbox.js";
|
|
47
47
|
import { validateContextSnapshot } from "../context/contextSnapshot.js";
|
|
48
|
+
import { validateGlobalRoleMessage } from "../message/message.js";
|
|
48
49
|
import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
|
|
49
50
|
import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
|
|
50
51
|
import { sessionOwnerProcessKey } from "../runtime/sessionOwnerIdentity.js";
|
|
@@ -548,6 +549,8 @@ export class SqliteTaskStore {
|
|
|
548
549
|
validateTaskRole(record);
|
|
549
550
|
if (table === "global_roles")
|
|
550
551
|
validateGlobalRole(record);
|
|
552
|
+
if (table === "global_role_messages")
|
|
553
|
+
validateGlobalRoleMessage(record);
|
|
551
554
|
return record;
|
|
552
555
|
}
|
|
553
556
|
#sortById(rows, idOf) {
|
|
@@ -614,26 +617,6 @@ export class SqliteTaskStore {
|
|
|
614
617
|
});
|
|
615
618
|
}
|
|
616
619
|
// -- projects ---------------------------------------------------------------
|
|
617
|
-
saveArtifact(artifact) {
|
|
618
|
-
validateArtifact(artifact);
|
|
619
|
-
this.#mutate(() => {
|
|
620
|
-
const previous = this.getArtifact(artifact.taskId, artifact.id);
|
|
621
|
-
if (previous !== null) {
|
|
622
|
-
if (!isDeepStrictEqual(previous, artifact))
|
|
623
|
-
throw new StorageRecordError("Artifacts are immutable.");
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
this.#db.prepare("INSERT INTO artifacts (task_id, id, payload) VALUES (?, ?, ?)")
|
|
627
|
-
.run(artifact.taskId, artifact.id, this.#json(artifact));
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
getArtifact(taskId, artifactId) {
|
|
631
|
-
const artifact = this.#getPayload("artifacts", "task_id = ? AND id = ?", [taskId, artifactId]);
|
|
632
|
-
return artifact === null ? null : validateArtifact(artifact);
|
|
633
|
-
}
|
|
634
|
-
listArtifacts(taskId) {
|
|
635
|
-
return this.#listPayload("artifacts", "task_id = ?", [taskId]).map(validateArtifact);
|
|
636
|
-
}
|
|
637
620
|
savePluginValidation(validation) {
|
|
638
621
|
validatePluginValidation(validation);
|
|
639
622
|
this.#mutate(() => {
|
|
@@ -784,7 +767,7 @@ export class SqliteTaskStore {
|
|
|
784
767
|
activeRunRefs.push(`${task.id}/${run.id}`);
|
|
785
768
|
}
|
|
786
769
|
for (const attempt of this.listIntegrationAttempts(task.id)) {
|
|
787
|
-
if (
|
|
770
|
+
if (["running", "blocked", "conflicted", "validating"].includes(attempt.status)) {
|
|
788
771
|
unresolvedIntegrationRefs.push(`${task.id}/${attempt.id}`);
|
|
789
772
|
}
|
|
790
773
|
}
|
|
@@ -875,6 +858,42 @@ export class SqliteTaskStore {
|
|
|
875
858
|
this.#saveRuntimeSessionCandidate(sessions);
|
|
876
859
|
});
|
|
877
860
|
}
|
|
861
|
+
// -- global role messages ---------------------------------------------------
|
|
862
|
+
nextGlobalRoleMessageId() { return this.#nextGlobalId("global-message"); }
|
|
863
|
+
saveGlobalRoleMessage(message) {
|
|
864
|
+
validateGlobalRoleMessage(message);
|
|
865
|
+
this.#requireGlobalRole(message.roleName);
|
|
866
|
+
this.#mutate(() => {
|
|
867
|
+
const seq = this.#globalMessageSequence(message.id);
|
|
868
|
+
this.#db.prepare(`INSERT INTO global_role_messages (name, message_id, seq, payload, created_at) VALUES (?, ?, ?, ?, ?)`).run(message.roleName, message.id, seq, this.#json(message), message.createdAt);
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
listGlobalRoleMessages(roleName) {
|
|
872
|
+
return this.#sortById(this.#listPayload("global_role_messages", "name = ?", [roleName]), (message) => message.id);
|
|
873
|
+
}
|
|
874
|
+
updateGlobalRoleMessage(message) {
|
|
875
|
+
validateGlobalRoleMessage(message);
|
|
876
|
+
this.#requireGlobalRole(message.roleName);
|
|
877
|
+
this.#mutate(() => {
|
|
878
|
+
const seq = this.#globalMessageSequence(message.id);
|
|
879
|
+
const result = this.#db.prepare(`UPDATE global_role_messages SET seq = ?, payload = ? WHERE name = ? AND message_id = ?`).run(seq, this.#json(message), message.roleName, message.id);
|
|
880
|
+
if (result.changes === 0) {
|
|
881
|
+
throw new StorageRecordError(`Global message not found: ${message.roleName}/${message.id}`);
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
#requireGlobalRole(name) {
|
|
886
|
+
const row = this.#db.prepare("SELECT 1 FROM global_roles WHERE name = ?").get(name);
|
|
887
|
+
if (row === undefined)
|
|
888
|
+
throw new StorageRecordError(`Global Role not found: ${name}`);
|
|
889
|
+
}
|
|
890
|
+
/** A Global message id is `global-message-<n>`; its numeric tail is its seq. */
|
|
891
|
+
#globalMessageSequence(id) {
|
|
892
|
+
const match = /^global-message-([1-9]\d*)$/.exec(id);
|
|
893
|
+
if (match === null)
|
|
894
|
+
throw new StorageRecordError(`Global message id is invalid: ${id}`);
|
|
895
|
+
return Number(match[1]);
|
|
896
|
+
}
|
|
878
897
|
// -- tasks ------------------------------------------------------------------
|
|
879
898
|
nextTaskId() { return this.#nextGlobalId("task"); }
|
|
880
899
|
saveTask(task) {
|
package/dist/storage/storeRpc.js
CHANGED
|
@@ -29,7 +29,7 @@ import { StorageCancelledError, StorageConflictError, StorageRecordError } from
|
|
|
29
29
|
*/
|
|
30
30
|
const READ_ONLY_STORE_METHODS = new Set([
|
|
31
31
|
"getPluginIntent", "listPluginIntents",
|
|
32
|
-
"getPluginValidation", "
|
|
32
|
+
"getPluginValidation", "getLocalResource", "listLocalResources",
|
|
33
33
|
"getEnvironmentPreparation", "listEnvironmentPreparations",
|
|
34
34
|
"rootDirectory",
|
|
35
35
|
"getConfig",
|
|
@@ -46,6 +46,7 @@ const READ_ONLY_STORE_METHODS = new Set([
|
|
|
46
46
|
"getGlobalRole",
|
|
47
47
|
"getGlobalRoleSessionSet",
|
|
48
48
|
"listGlobalRoleSessionSets",
|
|
49
|
+
"listGlobalRoleMessages",
|
|
49
50
|
"listTasks",
|
|
50
51
|
"getTask",
|
|
51
52
|
"readNextActionFacts",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join } from "node:path";
|
|
2
2
|
import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
3
3
|
import Database from "better-sqlite3";
|
|
4
|
+
import { inspectAgentHostCompatibility, describeAgentHostUpgradeBlockers } from "../../runtime/agentHostCompatibility.js";
|
|
4
5
|
import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation } from "../../runtime/runtimeObservation.js";
|
|
5
6
|
import { RUNTIME_PROCESS_EXIT_TASK_EVENT, validateRuntimeProcessExitObservation } from "../../runtime/processExitObservation.js";
|
|
6
7
|
import { validateAgentProfile } from "../../profile/agentProfile.js";
|
|
@@ -10,6 +11,9 @@ import { validateRun } from "../../agentRun/agentRun.js";
|
|
|
10
11
|
import { validateDurableJob } from "../../job/durableJob.js";
|
|
11
12
|
import { validateWorkItem } from "../../workItem/workItem.js";
|
|
12
13
|
import { SqliteTaskStore } from "../sqliteStore.js";
|
|
14
|
+
import { storageBackupRoot } from "../homeLayout.js";
|
|
15
|
+
import { preflightUnifyHomeLayout } from "../migrations/unifyHomeLayout.js";
|
|
16
|
+
import { preflightCollapseWorktreeLayout } from "../migrations/collapseWorktreeLayout.js";
|
|
13
17
|
import { migrateSqliteSchema, storageMigrationPlan } from "../sqliteSchema.js";
|
|
14
18
|
import { CURRENT_DATABASE_FILENAME, inspectStorageSchema } from "../storageSchema.js";
|
|
15
19
|
import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "../storageVersions.js";
|
|
@@ -38,6 +42,15 @@ export async function runStorageUpgrade(options) {
|
|
|
38
42
|
? classification.classification.blocker.action
|
|
39
43
|
: "Use a compatible Yui release.");
|
|
40
44
|
}
|
|
45
|
+
const incompatibleHosts = await inspectAgentHostCompatibility(options.home);
|
|
46
|
+
if (incompatibleHosts.length > 0) {
|
|
47
|
+
return {
|
|
48
|
+
...blocked(state.status === "current" ? currentClassification() : migratableClassification(state.currentVersion), "host-compatibility", `Existing Agent Hosts cannot safely use the target Controller/storage:\n${describeAgentHostUpgradeBlockers(incompatibleHosts)}`, "Keep the current Controller, Home and Host processes unchanged. Inspect the named Sessions and original pending "
|
|
49
|
+
+ "inputs/results; let current work settle, then use explicitly authorized Session replacement/cleanup at a safe "
|
|
50
|
+
+ "boundary before retrying the upgrade. This target cannot hot-patch legacy Host memory."),
|
|
51
|
+
hosts: incompatibleHosts
|
|
52
|
+
};
|
|
53
|
+
}
|
|
41
54
|
if (state.status === "current") {
|
|
42
55
|
try {
|
|
43
56
|
validateCurrentStore(options.home);
|
|
@@ -73,6 +86,21 @@ export async function runStorageUpgrade(options) {
|
|
|
73
86
|
+ `to ${CURRENT_STORAGE_VERSION}.`), "corruption", "The storage migration registry is incomplete.", "Install a Yui release that carries the complete migration chain.");
|
|
74
87
|
}
|
|
75
88
|
const classification = migratableClassification(state.currentVersion);
|
|
89
|
+
// Genuine pre-migration safety check, shared by ALL migrating modes. The
|
|
90
|
+
// unify-home data migration (18->19) physically relocates on-disk trees and
|
|
91
|
+
// rewrites live launch pointers, so before any mode proceeds we run its READ-
|
|
92
|
+
// ONLY preflight and refuse up front on the same conditions the migration would
|
|
93
|
+
// throw on inside the apply transaction: a queued/running Job bound under a
|
|
94
|
+
// relocating root, or a conflicting relocation target. The migration is applied
|
|
95
|
+
// offline (after the operator has stopped this Home's writers), so the preflight
|
|
96
|
+
// does not scan for live processes. Surfacing these here — before the Controller
|
|
97
|
+
// is stopped or a backup is taken — is what makes `--dry-run` and the updater's
|
|
98
|
+
// `--update-preflight` a genuine readiness signal rather than a mere list of
|
|
99
|
+
// schema steps, while the in-transaction checks remain the authoritative guard
|
|
100
|
+
// against anything that starts after this point.
|
|
101
|
+
const migrationBlocked = preflightMigrationBlockers(options.home, plan, classification);
|
|
102
|
+
if (migrationBlocked !== null)
|
|
103
|
+
return migrationBlocked;
|
|
76
104
|
if (options.mode === "update-preflight") {
|
|
77
105
|
return {
|
|
78
106
|
outcome: "update-preflight",
|
|
@@ -223,7 +251,10 @@ async function createDatabaseBackup(home, sourceVersion, now) {
|
|
|
223
251
|
const source = join(home, CURRENT_DATABASE_FILENAME);
|
|
224
252
|
if (!existsSync(source))
|
|
225
253
|
throw new Error("The authoritative yui.db is missing.");
|
|
226
|
-
|
|
254
|
+
// Backups live inside Home so a single canonical root contains every
|
|
255
|
+
// self-managed artifact; the upgrade fence still protects them from the
|
|
256
|
+
// live database because they are a distinct partition from yui.db.
|
|
257
|
+
const backupDirectory = storageBackupRoot(home);
|
|
227
258
|
mkdirSync(backupDirectory, { recursive: true, mode: 0o700 });
|
|
228
259
|
const timestamp = now.toISOString().replaceAll(":", "-");
|
|
229
260
|
const backupPath = join(backupDirectory, `pre-storage-v${sourceVersion}-${timestamp}.db`);
|
|
@@ -313,6 +344,68 @@ function blocked(classification, stage, message, action) {
|
|
|
313
344
|
sceneUnchanged: true
|
|
314
345
|
};
|
|
315
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Run each path-relocating data migration's READ-ONLY preflight against the
|
|
349
|
+
* current (pre-upgrade) database and, when any finds a blocker, return a
|
|
350
|
+
* `blocked` result the caller surfaces before touching the Controller or the
|
|
351
|
+
* Home. Returns `null` when the plan carries no path-relocating data migration,
|
|
352
|
+
* or when every preflight is clear.
|
|
353
|
+
*
|
|
354
|
+
* Both the 18->19 unify and the 19->20 collapse migrations physically relocate
|
|
355
|
+
* managed worktrees and share the same blocker shape (in-flight Job, conflicting
|
|
356
|
+
* relocation target). When a Home is upgraded across both in one run, their
|
|
357
|
+
* blockers are aggregated so the operator sees every readiness problem at once.
|
|
358
|
+
*
|
|
359
|
+
* The database is opened read-only so the checks cannot mutate the authoritative
|
|
360
|
+
* store, and the handle is always closed. An unexpected failure to evaluate a
|
|
361
|
+
* preflight is itself a fail-closed blocker: we must not advance to an
|
|
362
|
+
* irreversible migration on an unverifiable readiness signal.
|
|
363
|
+
*/
|
|
364
|
+
function preflightMigrationBlockers(home, plan, classification) {
|
|
365
|
+
const relocatesUnify = plan.some((step) => step.name === "unify-home-layout");
|
|
366
|
+
const relocatesCollapse = plan.some((step) => step.name === "collapse-worktree-layout");
|
|
367
|
+
// Only meaningful when the plan actually includes a path-relocating migration.
|
|
368
|
+
if (!relocatesUnify && !relocatesCollapse)
|
|
369
|
+
return null;
|
|
370
|
+
let blockers;
|
|
371
|
+
try {
|
|
372
|
+
const database = new Database(join(home, CURRENT_DATABASE_FILENAME), {
|
|
373
|
+
readonly: true,
|
|
374
|
+
fileMustExist: true
|
|
375
|
+
});
|
|
376
|
+
try {
|
|
377
|
+
blockers = [];
|
|
378
|
+
if (relocatesUnify)
|
|
379
|
+
blockers.push(...preflightUnifyHomeLayout(database).blockers);
|
|
380
|
+
if (relocatesCollapse)
|
|
381
|
+
blockers.push(...preflightCollapseWorktreeLayout(database).blockers);
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
database.close();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
return {
|
|
389
|
+
...blocked(classification, "in-flight", `The storage migration readiness check could not be completed: ${messageOf(error)}`, "Resolve the reported problem, confirm no Job is queued or running against a managed "
|
|
390
|
+
+ "workspace, then rerun the upgrade.")
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (blockers.length === 0)
|
|
394
|
+
return null;
|
|
395
|
+
return {
|
|
396
|
+
outcome: "blocked",
|
|
397
|
+
stage: "in-flight",
|
|
398
|
+
message: "Refusing to migrate: the Home is not safe to relocate under the current layout yet. "
|
|
399
|
+
+ blockers.map((entry) => entry.detail).join("; ") + ".",
|
|
400
|
+
action: "Resolve the reported condition(s) — let a queued/running Job finish or cancel it, and "
|
|
401
|
+
+ "clear any conflicting relocation target — then rerun the upgrade. The migration is applied "
|
|
402
|
+
+ "offline, so first confirm the Controller and any execution writers for this Home are "
|
|
403
|
+
+ "stopped. The authoritative Home is unchanged.",
|
|
404
|
+
blockers: blockers.map((entry) => ({ reason: entry.reason, detail: entry.detail })),
|
|
405
|
+
classification,
|
|
406
|
+
sceneUnchanged: true
|
|
407
|
+
};
|
|
408
|
+
}
|
|
316
409
|
function messageOf(error) {
|
|
317
410
|
return error instanceof Error ? error.message : String(error);
|
|
318
411
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
2
|
+
import { StorageRecordError, StorageConflictError, StorageCancelledError } from "../storage/taskStore.js";
|
|
3
|
+
import { StorageSchemaError } from "../storage/storageSchema.js";
|
|
4
|
+
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
5
|
+
export class ArchiveAuditPersistenceError extends Error {
|
|
6
|
+
constructor(cause) {
|
|
7
|
+
super(`Archive audit could not be persisted: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
8
|
+
this.name = "ArchiveAuditPersistenceError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function isArchivePersistenceFailure(error) {
|
|
12
|
+
return error instanceof ArchiveAuditPersistenceError
|
|
13
|
+
|| error instanceof StorageRecordError || error instanceof StorageConflictError
|
|
14
|
+
|| error instanceof StorageCancelledError || error instanceof StorageSchemaError
|
|
15
|
+
|| (error instanceof Error && (("code" in error && String(error.code).startsWith("SQLITE_"))
|
|
16
|
+
|| (error.cause !== undefined && error.cause !== error && isArchivePersistenceFailure(error.cause))));
|
|
17
|
+
}
|
|
18
|
+
/** Current resource references, not another cleanup queue or writable status. */
|
|
19
|
+
export function archiveRetainedResources(store, task) {
|
|
20
|
+
const workspaces = store.listManagedWorkspaces(task.id);
|
|
21
|
+
const resources = workspaces.map(workspace => ({
|
|
22
|
+
resource: managedWorkspaceKey(workspace.owner),
|
|
23
|
+
detail: "Managed workspace retained; removal has not been recorded.",
|
|
24
|
+
paths: [workspace.root, ...workspace.entries.filter(e => e.access === "write").map(e => e.path)]
|
|
25
|
+
}));
|
|
26
|
+
if (task.cwd !== undefined && !workspaces.some(w => w.owner.type === "task")) {
|
|
27
|
+
resources.push({ resource: `task:${task.id}`, detail: "Task workspace reference retained.", paths: [task.cwd] });
|
|
28
|
+
}
|
|
29
|
+
for (const run of store.listRuns(task.id).filter(r => r.status === "active")) {
|
|
30
|
+
resources.push({ resource: `run:${task.id}/${run.id}`,
|
|
31
|
+
detail: `AgentRun remains active (${run.roleName}); archive is not stop evidence.` });
|
|
32
|
+
}
|
|
33
|
+
for (const job of store.listDurableJobs(task.id).filter(j => ["queued", "running", "unknown-needs-attention"].includes(j.status))) {
|
|
34
|
+
resources.push({ resource: `job:${task.id}/${job.id}`, detail: `DurableJob remains ${job.status}.` });
|
|
35
|
+
}
|
|
36
|
+
for (const attempt of store.listIntegrationAttempts(task.id).filter(a => ["running", "blocked", "conflicted", "validating"].includes(a.status))) {
|
|
37
|
+
resources.push({ resource: `integration-attempt:${task.id}/${attempt.id}`,
|
|
38
|
+
detail: `Integration remains ${attempt.status}.` });
|
|
39
|
+
}
|
|
40
|
+
for (const item of store.listWorkItems(task.id).filter(i => i.status === "open")) {
|
|
41
|
+
resources.push({ resource: `work-item:${task.id}/${item.id}`, detail: "WorkItem remains unaccepted." });
|
|
42
|
+
}
|
|
43
|
+
for (const round of store.listReviewRounds(task.id).filter(r => !["completed", "failed"].includes(r.status))) {
|
|
44
|
+
resources.push({ resource: `review-round:${task.id}/${round.id}`, detail: `Review remains ${round.status}.` });
|
|
45
|
+
}
|
|
46
|
+
for (const input of store.listInputRequests(task.id).filter(i => i.status === "open")) {
|
|
47
|
+
resources.push({ resource: `input:${task.id}/${input.id}`, detail: "Unresolved input preserved; not answered by archive." });
|
|
48
|
+
}
|
|
49
|
+
for (const role of store.listRoles(task.id)) {
|
|
50
|
+
const sessions = store.getTaskRoleSessionSet(task.id, role.name);
|
|
51
|
+
for (const session of Object.values(sessions?.sessions ?? {})) {
|
|
52
|
+
if (session.status === "ended")
|
|
53
|
+
continue;
|
|
54
|
+
resources.push({ resource: `session:${task.id}/${role.name}/${session.agentId}/${session.nativeSessionId}`,
|
|
55
|
+
detail: `Session is ${session.status}; physical quiescence is not established by archive.` });
|
|
56
|
+
}
|
|
57
|
+
const provider = sessions?.providerBinding;
|
|
58
|
+
if (provider !== undefined && provider !== null && provider.run != null
|
|
59
|
+
&& !["completed", "failed", "cancelled"].includes(provider.run.status)) {
|
|
60
|
+
resources.push({ resource: `provider-input:${task.id}/${role.name}/${provider.run.attemptId}`,
|
|
61
|
+
detail: `Provider input is ${provider.run.status}; exact input identity is retained.` });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const mailbox of store.listWorkMailboxes()) {
|
|
65
|
+
const target = mailbox.target;
|
|
66
|
+
if (!("taskId" in target) || target.taskId !== task.id)
|
|
67
|
+
continue;
|
|
68
|
+
if (mailbox.processing === null && mailbox.pending === null)
|
|
69
|
+
continue;
|
|
70
|
+
resources.push({
|
|
71
|
+
resource: `mailbox:${target.kind}:${task.id}${"roleName" in target ? `/${target.roleName}` : ""}`,
|
|
72
|
+
detail: `Original mailbox retained${mailbox.processing === null ? "" : ` (${mailbox.processing.batchId})`}; not replayed or acknowledged by archive.`
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return resources;
|
|
76
|
+
}
|
|
77
|
+
export function archiveDeliveryWarnings(delivery) {
|
|
78
|
+
return delivery.projects.filter(p => p.codeDelivery !== "none" && (!p.merged || !p.verified))
|
|
79
|
+
.map(p => ({ resource: `delivery:${delivery.taskId}/${p.projectId}`,
|
|
80
|
+
detail: `${p.coverage}: ${p.reason} Merged=${p.merged}; verified=${p.verified}.` }));
|
|
81
|
+
}
|
|
82
|
+
/** Every completed/failed resource attempt is committed before trying the next.
|
|
83
|
+
* Do not catch storage failures: callers must not report unrecorded cleanup as success.
|
|
84
|
+
*/
|
|
85
|
+
export function recordArchiveCleanup(store, taskId, diagnostic, status, now = new Date()) {
|
|
86
|
+
try {
|
|
87
|
+
store.transaction(tx => {
|
|
88
|
+
if (tx.getTask(taskId)?.status !== "archived")
|
|
89
|
+
throw new Error(`Task is not archived: ${taskId}.`);
|
|
90
|
+
tx.saveEvent(taskId, createTaskEvent(tx.nextEventId(taskId), taskId, "task.archive-cleanup", {
|
|
91
|
+
resource: diagnostic.resource, detail: diagnostic.detail, status,
|
|
92
|
+
...(diagnostic.paths === undefined ? {} : { paths: JSON.stringify(diagnostic.paths) })
|
|
93
|
+
}, now));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
throw new ArchiveAuditPersistenceError(error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function taskArchiveDiagnostics(store, task) {
|
|
101
|
+
const events = store.listEvents(task.id);
|
|
102
|
+
const archive = events.filter(e => e.type === "task.archived").at(-1);
|
|
103
|
+
const cleanup = events.filter(e => e.type === "task.archive-cleanup");
|
|
104
|
+
const warnings = archive?.payload.warnings === undefined
|
|
105
|
+
? [] : JSON.parse(archive.payload.warnings);
|
|
106
|
+
// These are historical attempt diagnostics; current references below remain
|
|
107
|
+
// authoritative even when explicit later resource cleanup has succeeded.
|
|
108
|
+
for (const event of cleanup.filter(e => e.payload.status === "retained")) {
|
|
109
|
+
warnings.push({ resource: event.payload.resource, detail: event.payload.detail,
|
|
110
|
+
...(event.payload.paths === undefined ? {} : { paths: JSON.parse(event.payload.paths) }) });
|
|
111
|
+
}
|
|
112
|
+
const forced = archive?.payload.force === "true";
|
|
113
|
+
const cleanupFinished = !forced || cleanup.some(e => e.payload.status === "finished");
|
|
114
|
+
if (forced && !cleanupFinished)
|
|
115
|
+
warnings.push({ resource: `task:${task.id}`,
|
|
116
|
+
detail: "Archive committed; cleanup has not finished. Retained references require explicit inspection; archive retry does not replay cleanup." });
|
|
117
|
+
return { archived: task.status === "archived", forced,
|
|
118
|
+
disposition: archive?.payload.workspaceDisposition ?? null,
|
|
119
|
+
cleanupFinished, warnings, retainedResources: archiveRetainedResources(store, task),
|
|
120
|
+
cleanupEvents: cleanup };
|
|
121
|
+
}
|
|
122
|
+
export function renderArchiveDiagnostics(data) {
|
|
123
|
+
return [
|
|
124
|
+
`Archived: ${data.archived}; forced: ${data.forced}; cleanup finished: ${data.cleanupFinished}`,
|
|
125
|
+
...data.warnings.map(w => `Warning [${w.resource}]: ${w.detail}`),
|
|
126
|
+
...data.retainedResources.map(r => `Retained [${r.resource}]: ${r.detail}${r.paths ? ` (${r.paths.join(", ")})` : ""}`)
|
|
127
|
+
].join("\n") + "\n";
|
|
128
|
+
}
|