@zq-silk/yui 0.15.9 → 0.15.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/ARCHITECTURE.md +8 -4
  2. package/ARCHITECTURE.zh-CN.md +5 -2
  3. package/README.md +13 -5
  4. package/dist/agent/launchEnvironment.js +7 -0
  5. package/dist/agentRun/agentRun.js +3 -0
  6. package/dist/cli/commandCatalog.js +64 -16
  7. package/dist/cli/interactionPolicy.js +7 -3
  8. package/dist/cli/managedDiagnostics.js +1 -1
  9. package/dist/cli/updateOrchestrator.js +24 -1
  10. package/dist/cli/updatePorts.js +7 -3
  11. package/dist/cli/upgradeCommand.js +42 -2
  12. package/dist/cli.js +381 -107
  13. package/dist/commands/executionAuditCommands.js +10 -0
  14. package/dist/commands/globalRoleCommands.js +339 -4
  15. package/dist/commands/projectCommands.js +50 -22
  16. package/dist/commands/releaseCommands.js +18 -0
  17. package/dist/commands/taskActor.js +25 -0
  18. package/dist/commands/taskCommands.js +586 -96
  19. package/dist/commands/taskIntegrationCommands.js +19 -39
  20. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  21. package/dist/commands/taskOverviewCommand.js +4 -3
  22. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  23. package/dist/commands/taskPublicationCommands.js +11 -2
  24. package/dist/commands/taskPublicationVerifyCommand.js +23 -39
  25. package/dist/commands/taskRemoteDeliveryCommand.js +22 -11
  26. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  27. package/dist/context/runContextPack.js +3 -0
  28. package/dist/context/taskCatalog.js +187 -0
  29. package/dist/context/taskContext.js +55 -6
  30. package/dist/controller/agentHostObservation.js +155 -0
  31. package/dist/controller/clientRuntime.js +17 -2
  32. package/dist/controller/controller.js +14 -2
  33. package/dist/controller/fileSchedulerStoreAdapter.js +519 -25
  34. package/dist/controller/globalInputDelivery.js +132 -0
  35. package/dist/controller/jobControl.js +6 -2
  36. package/dist/controller/providerRetryAdmission.js +100 -0
  37. package/dist/controller/providerRetryDelivery.js +218 -0
  38. package/dist/controller/resourceInventory.js +14 -4
  39. package/dist/controller/resourceInventoryLinux.js +2 -6
  40. package/dist/controller/runtime.js +117 -7
  41. package/dist/controller/runtimeEventInbox.js +32 -3
  42. package/dist/controller/runtimeEventProcessor.js +26 -6
  43. package/dist/controller/runtimeHookRunFence.js +75 -19
  44. package/dist/controller/structuredProviderObservation.js +133 -70
  45. package/dist/coordination/workMailboxQueue.js +5 -0
  46. package/dist/execution/workItemExecutionProjection.js +1 -1
  47. package/dist/executor/agentExecutor.js +64 -4
  48. package/dist/executor/executorRegistry.js +3 -0
  49. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  50. package/dist/integration/deliveryObligation.js +2 -1
  51. package/dist/integration/gitIntegrationService.js +329 -386
  52. package/dist/integration/integrationAttempt.js +30 -4
  53. package/dist/integration/integrationQueueService.js +7 -7
  54. package/dist/integration/integrationSourceApplication.js +323 -0
  55. package/dist/lifecycle/exactRunTerminalization.js +4 -1
  56. package/dist/message/globalInterrupt.js +33 -0
  57. package/dist/message/globalProviderRetry.js +15 -0
  58. package/dist/message/inputControlResolution.js +106 -0
  59. package/dist/message/message.js +367 -0
  60. package/dist/message/messageContinuation.js +126 -3
  61. package/dist/message/taskInterrupt.js +34 -0
  62. package/dist/observability/executionAudit.js +19 -0
  63. package/dist/observability/orchestrationMetrics.js +1 -1
  64. package/dist/release/releaseHandover.js +22 -0
  65. package/dist/release/releaseWorkflowPorts.js +15 -7
  66. package/dist/repository/gitWorkspace.js +430 -107
  67. package/dist/repository/projectMaintenanceLock.js +75 -18
  68. package/dist/repository/taskWorkspaceCoordinator.js +182 -101
  69. package/dist/repository/taskWorkspacePreparer.js +205 -72
  70. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  71. package/dist/repository/workspaceCleanupInspection.js +187 -0
  72. package/dist/resources/resourceDiscovery.js +3 -2
  73. package/dist/runtime/agentError.js +5 -3
  74. package/dist/runtime/agentHost.js +179 -82
  75. package/dist/runtime/agentHostCompatibility.js +127 -0
  76. package/dist/runtime/agentHostProtocol.js +53 -0
  77. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  78. package/dist/runtime/codexAppServerRuntime.js +34 -3
  79. package/dist/runtime/executionEnvironment.js +0 -19
  80. package/dist/runtime/launchBroker.js +6 -0
  81. package/dist/runtime/providerControl.js +5 -1
  82. package/dist/runtime/providerRetry.js +198 -0
  83. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  84. package/dist/runtime/sessionReconciliation.js +4 -4
  85. package/dist/runtime/sessionTokenMetrics.js +15 -5
  86. package/dist/runtime/structuredProviderHost.js +6 -2
  87. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  88. package/dist/runtime/taskUsageMetrics.js +275 -0
  89. package/dist/runtime/tmuxAdapters.js +5 -3
  90. package/dist/scheduler/activeRoleRunDelivery.js +12 -0
  91. package/dist/scheduler/leaderWakeupProcessor.js +5 -0
  92. package/dist/scheduler/operatorEvent.js +4 -0
  93. package/dist/scheduler/taskExecutionProjection.js +38 -6
  94. package/dist/scheduler/taskObservabilityProjection.js +6 -44
  95. package/dist/scheduler/wakeReason.js +7 -1
  96. package/dist/scheduler/wakeupQueue.js +2 -0
  97. package/dist/setup/setupCommand.js +26 -8
  98. package/dist/storage/homeLayout.js +130 -0
  99. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  100. package/dist/storage/migrations/integrationContinuation.js +104 -0
  101. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  102. package/dist/storage/sqliteSchema.js +167 -4
  103. package/dist/storage/sqliteStore.js +57 -1
  104. package/dist/storage/storageVersions.js +1 -1
  105. package/dist/storage/storeRpc.js +2 -0
  106. package/dist/storage/taskCatalog.js +123 -0
  107. package/dist/storage/taskStore.js +2 -0
  108. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  109. package/dist/task/archiveDiagnostics.js +129 -0
  110. package/dist/task/archivePreflight.js +124 -0
  111. package/dist/task/nextAction.js +44 -11
  112. package/dist/task/publicationAdoption.js +56 -0
  113. package/dist/task/publicationReference.js +10 -0
  114. package/dist/task/remoteDelivery.js +31 -16
  115. package/dist/web/assets/client/app.js +147 -17
  116. package/dist/web/assets/client/components.js +56 -13
  117. package/dist/web/assets/client/i18n.js +78 -4
  118. package/dist/web/assets/client/taskSurface.js +108 -1
  119. package/dist/web/assets/client/view.js +39 -8
  120. package/dist/web/assets/shell.js +29 -0
  121. package/dist/web/assets/styles/layout.js +8 -1
  122. package/dist/web/assets/styles/widgets.js +12 -0
  123. package/dist/web/webServer.js +131 -4
  124. package/dist/web/webSnapshot.js +16 -6
  125. package/dist/web/webTaskSurface.js +222 -5
  126. package/dist/workspace/cleanupInspection.js +63 -0
  127. package/dist/workspace/workItemChangeSetManager.js +111 -35
  128. package/docs/agent-result-consumption.md +4 -0
  129. package/docs/agent-result-consumption.zh-CN.md +3 -0
  130. package/docs/agent-runtime-drivers.md +7 -0
  131. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  132. package/docs/architecture/README.md +2 -0
  133. package/docs/architecture/README.zh-CN.md +3 -1
  134. package/docs/architecture/capabilities-and-resources.md +30 -5
  135. package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
  136. package/docs/managed-turn-and-session-runtime.md +47 -0
  137. package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
  138. package/docs/observability/README.md +62 -0
  139. package/docs/observability/README.zh-CN.md +47 -0
  140. package/docs/project-refresh.md +77 -0
  141. package/docs/project-refresh.zh-CN.md +59 -0
  142. package/docs/provider-retry.md +70 -0
  143. package/docs/release-workflow.md +39 -0
  144. package/docs/release-workflow.zh-CN.md +29 -0
  145. package/docs/sqlite-control-plane-design.md +223 -1
  146. package/docs/task-delivery.md +133 -13
  147. package/docs/task-delivery.zh-CN.md +99 -10
  148. package/docs/task-discovery.md +102 -0
  149. package/docs/task-discovery.zh-CN.md +86 -0
  150. package/docs/testing/verification-levels.md +40 -0
  151. package/docs/testing/verification-levels.zh-CN.md +23 -0
  152. package/i18n/README.zh-CN.md +13 -7
  153. package/package.json +1 -1
  154. package/skills/yui-leader/references/execution.md +154 -51
  155. package/skills/yui-leader/references/integration.md +52 -2
  156. package/skills/yui-operator/SKILL.md +19 -3
  157. package/skills/yui-reviewer/SKILL.md +4 -0
  158. package/skills/yui-runtime/SKILL.md +42 -0
  159. package/skills/yui-runtime/references/publication.md +42 -0
  160. package/skills/yui-runtime/references/recovery.md +24 -0
@@ -24,6 +24,9 @@ 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
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";
27
30
  import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "./storageVersions.js";
28
31
  /** Telemetry retention bounds (§4.4). Open question 3 in §11; defaults from the design. */
29
32
  export const TELEMETRY_KEEP_PER_RUN = 200;
@@ -643,6 +646,27 @@ CREATE TABLE IF NOT EXISTS context_snapshots (
643
646
  CREATE UNIQUE INDEX IF NOT EXISTS idx_context_snapshots_scope_sequence
644
647
  ON context_snapshots(task_id, scope, COALESCE(scope_ref, ''), sequence);
645
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
+ `;
646
670
  const MIGRATION_1_SQL = [
647
671
  BASELINE_CORE_SQL,
648
672
  BASELINE_JOB_AND_RELEASE_SQL,
@@ -1143,6 +1167,134 @@ UPDATE review_rounds SET payload = json_set(payload, '$.executionGroup.lanes', j
1143
1167
  // satisfies "optional still requires a migration declaration".
1144
1168
  sql: "SELECT 1; -- artifacts move to per-Task local Git; see migrateArtifactsToGit",
1145
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
1267
+ },
1268
+ {
1269
+ version: 26,
1270
+ name: "publication-candidate-adoption",
1271
+ introducedIn: "0.15.12",
1272
+ // Add optional Publication.headCommit (the observed PR/MR source head) and
1273
+ // publication.candidate-adopted events binding publicationId, completionEventId,
1274
+ // projectId, acceptedCommit/localCommit, acceptedTree/candidateTree, diffDigest,
1275
+ // acceptance and by, optionally integrationId/integrationDigest. Derived
1276
+ // task.archived mergeCoverage may also be uncovered or unverified.
1277
+ // No stored delivery index and no inferred adoption or verification:
1278
+ // valid old Publication, completion and archive payloads remain byte-identical.
1279
+ sql: "SELECT 1; -- fixed acceptance-to-publication evidence; preserve all historical facts"
1280
+ },
1281
+ {
1282
+ version: 27,
1283
+ name: "bounded-provider-input-retry",
1284
+ introducedIn: "0.16.0",
1285
+ // Optional owned input/recovery records on the existing Provider binding;
1286
+ // positive Driver transient evidence on StandardAgentError. No historical
1287
+ // failures are scanned, scheduled, reclassified or replayed by upgrading.
1288
+ sql: `
1289
+ -- ProviderTurn.input/retrySupported/failure; ProviderRuntimeBinding.retry/retryHistory/retryDisabled;
1290
+ -- optional error retryable and retryAfterMs facts. Index existing records only.
1291
+ CREATE INDEX idx_task_provider_retry ON role_session_sets(
1292
+ json_extract(payload, '$.providerBinding.retry.status')
1293
+ ) WHERE json_extract(payload, '$.providerBinding.retry.status') IN ('waiting', 'in-flight');
1294
+ CREATE INDEX idx_global_provider_retry ON global_role_session_sets(
1295
+ json_extract(payload, '$.providerBinding.retry.status')
1296
+ ) WHERE json_extract(payload, '$.providerBinding.retry.status') IN ('waiting', 'in-flight');
1297
+ `
1146
1298
  }
1147
1299
  ]);
1148
1300
  for (let index = 0; index < MIGRATIONS.length; index += 1) {
@@ -1386,8 +1538,14 @@ export function migrateSqliteSchema(db, options) {
1386
1538
  throw new SqliteSchemaMigrationError(`Storage version ${applied.currentVersion} requires an explicit upgrade to `
1387
1539
  + `${CURRENT_STORAGE_VERSION}`, "admission");
1388
1540
  }
1541
+ // Bound the applied steps to `throughVersion` (default and clamp: head), so a
1542
+ // migration test can stop at one step's intermediate state while production
1543
+ // always advances to head. The validate-mode guard above still fires on the
1544
+ // FULL pending set — the bound never lets an ordinary open partially migrate.
1545
+ const targetVersion = Math.min(options.throughVersion ?? CURRENT_STORAGE_VERSION, CURRENT_STORAGE_VERSION);
1546
+ const toApply = pending.filter((migration) => migration.version <= targetVersion);
1389
1547
  const newlyApplied = [];
1390
- for (const migration of pending) {
1548
+ for (const migration of toApply) {
1391
1549
  db.exec(migration.sql);
1392
1550
  migration.migrateData?.(db);
1393
1551
  const appliedAt = new Date().toISOString();
@@ -1400,12 +1558,16 @@ export function migrateSqliteSchema(db, options) {
1400
1558
  // earlier and is validated when the real upgrade later completes it).
1401
1559
  if (effectiveTarget >= CURRENT_STORAGE_VERSION)
1402
1560
  validateSchemaObjects(db);
1403
- return newlyApplied;
1561
+ // The true head reached: the highest already-applied version or, when this
1562
+ // run advanced the ledger, the last step it committed. Bounded runs report
1563
+ // the intermediate head; an unbounded run reports CURRENT_STORAGE_VERSION.
1564
+ const head = newlyApplied.at(-1) ?? applied.currentVersion;
1565
+ return { newlyApplied, head };
1404
1566
  };
1405
- const newlyApplied = options.mode === "apply" && !db.inTransaction
1567
+ const outcome = options.mode === "apply" && !db.inTransaction
1406
1568
  ? db.transaction(migrate)()
1407
1569
  : migrate();
1408
- return { applied: newlyApplied, version: CURRENT_STORAGE_VERSION };
1570
+ return { applied: outcome.newlyApplied, version: outcome.head };
1409
1571
  }
1410
1572
  /** The names of every table the schema creates (for tests/introspection). */
1411
1573
  export const SQLITE_SCHEMA_TABLES = [
@@ -1421,6 +1583,7 @@ export const SQLITE_SCHEMA_TABLES = [
1421
1583
  "projects",
1422
1584
  "global_roles",
1423
1585
  "global_role_session_sets",
1586
+ "global_role_messages",
1424
1587
  "global_sequences",
1425
1588
  "tasks_catalog",
1426
1589
  "managed_workspaces",
@@ -38,6 +38,7 @@ import { existsSync, mkdirSync } from "node:fs";
38
38
  import { join } from "node:path";
39
39
  import { isDeepStrictEqual } from "node:util";
40
40
  import Database from "better-sqlite3";
41
+ import { queryTaskCatalog } from "./taskCatalog.js";
41
42
  import { validatePluginValidation } from "../plugins/pluginPackage.js";
42
43
  import { validatePluginIntent, validatePluginIntentFailure } from "../plugins/pluginIntent.js";
43
44
  import { validateLocalResource, validateEnvironmentPreparation } from "../resources/projectResource.js";
@@ -45,6 +46,7 @@ import { validateConfiguredAgent } from "../agent/agent.js";
45
46
  import { validateTaskBrief } from "../brief/taskBrief.js";
46
47
  import { consumePendingBatch, mailboxTargetKey, validateWorkMailbox } from "../coordination/workMailbox.js";
47
48
  import { validateContextSnapshot } from "../context/contextSnapshot.js";
49
+ import { validateGlobalRoleMessage } from "../message/message.js";
48
50
  import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
49
51
  import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
50
52
  import { sessionOwnerProcessKey } from "../runtime/sessionOwnerIdentity.js";
@@ -548,6 +550,8 @@ export class SqliteTaskStore {
548
550
  validateTaskRole(record);
549
551
  if (table === "global_roles")
550
552
  validateGlobalRole(record);
553
+ if (table === "global_role_messages")
554
+ validateGlobalRoleMessage(record);
551
555
  return record;
552
556
  }
553
557
  #sortById(rows, idOf) {
@@ -764,7 +768,7 @@ export class SqliteTaskStore {
764
768
  activeRunRefs.push(`${task.id}/${run.id}`);
765
769
  }
766
770
  for (const attempt of this.listIntegrationAttempts(task.id)) {
767
- if (attempt.status === "running" || attempt.status === "blocked") {
771
+ if (["running", "blocked", "conflicted", "validating"].includes(attempt.status)) {
768
772
  unresolvedIntegrationRefs.push(`${task.id}/${attempt.id}`);
769
773
  }
770
774
  }
@@ -855,6 +859,42 @@ export class SqliteTaskStore {
855
859
  this.#saveRuntimeSessionCandidate(sessions);
856
860
  });
857
861
  }
862
+ // -- global role messages ---------------------------------------------------
863
+ nextGlobalRoleMessageId() { return this.#nextGlobalId("global-message"); }
864
+ saveGlobalRoleMessage(message) {
865
+ validateGlobalRoleMessage(message);
866
+ this.#requireGlobalRole(message.roleName);
867
+ this.#mutate(() => {
868
+ const seq = this.#globalMessageSequence(message.id);
869
+ 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);
870
+ });
871
+ }
872
+ listGlobalRoleMessages(roleName) {
873
+ return this.#sortById(this.#listPayload("global_role_messages", "name = ?", [roleName]), (message) => message.id);
874
+ }
875
+ updateGlobalRoleMessage(message) {
876
+ validateGlobalRoleMessage(message);
877
+ this.#requireGlobalRole(message.roleName);
878
+ this.#mutate(() => {
879
+ const seq = this.#globalMessageSequence(message.id);
880
+ 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);
881
+ if (result.changes === 0) {
882
+ throw new StorageRecordError(`Global message not found: ${message.roleName}/${message.id}`);
883
+ }
884
+ });
885
+ }
886
+ #requireGlobalRole(name) {
887
+ const row = this.#db.prepare("SELECT 1 FROM global_roles WHERE name = ?").get(name);
888
+ if (row === undefined)
889
+ throw new StorageRecordError(`Global Role not found: ${name}`);
890
+ }
891
+ /** A Global message id is `global-message-<n>`; its numeric tail is its seq. */
892
+ #globalMessageSequence(id) {
893
+ const match = /^global-message-([1-9]\d*)$/.exec(id);
894
+ if (match === null)
895
+ throw new StorageRecordError(`Global message id is invalid: ${id}`);
896
+ return Number(match[1]);
897
+ }
858
898
  // -- tasks ------------------------------------------------------------------
859
899
  nextTaskId() { return this.#nextGlobalId("task"); }
860
900
  saveTask(task) {
@@ -882,6 +922,15 @@ export class SqliteTaskStore {
882
922
  const tasks = this.#listPayload("task_records", "1=1", []);
883
923
  return this.#sortById(tasks, (task) => task.id);
884
924
  }
925
+ queryTaskCatalog(query) {
926
+ return queryTaskCatalog(this.#db, query);
927
+ }
928
+ listTaskChoices() {
929
+ const rows = this.#db.prepare(`SELECT c.task_id AS id, c.status,
930
+ json_extract(r.payload, '$.title') AS title
931
+ FROM tasks_catalog c JOIN task_records r ON r.task_id = c.task_id`).all();
932
+ return this.#sortById(rows, task => task.id);
933
+ }
885
934
  getTask(id) {
886
935
  return this.#getPayload("task_records", "task_id = ?", [id]);
887
936
  }
@@ -1228,6 +1277,13 @@ export class SqliteTaskStore {
1228
1277
  listRoleSessionSets(taskId) {
1229
1278
  return this.#sortById(this.#listPayload("role_session_sets", "task_id = ?", [taskId]), (set) => set.owner.roleName);
1230
1279
  }
1280
+ listProviderRetrySessions() {
1281
+ const pending = "json_extract(payload, '$.providerBinding.retry.status') IN ('waiting', 'in-flight')";
1282
+ return [
1283
+ ...this.#listPayload("role_session_sets", pending, []),
1284
+ ...this.#listPayload("global_role_session_sets", pending, [])
1285
+ ];
1286
+ }
1231
1287
  listRuntimeSessionCandidates(query = {}) {
1232
1288
  const taskIds = query.taskIds === undefined
1233
1289
  ? undefined
@@ -13,4 +13,4 @@
13
13
  * intermediate Yui releases.
14
14
  */
15
15
  export const MIN_SUPPORTED_STORAGE_VERSION = 1;
16
- export const CURRENT_STORAGE_VERSION = 19;
16
+ export const CURRENT_STORAGE_VERSION = 27;
@@ -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",
@@ -71,6 +72,7 @@ const READ_ONLY_STORE_METHODS = new Set([
71
72
  "getRoleSessionSet",
72
73
  "getTaskRoleSessionSet",
73
74
  "listRoleSessionSets",
75
+ "listProviderRetrySessions",
74
76
  "listRuntimeSessionCandidates",
75
77
  "getRoleSession",
76
78
  "getWorkItem",
@@ -0,0 +1,123 @@
1
+ /** Current facts, not another persisted projection or execution status. */
2
+ export const CATALOG_ATTENTION = [
3
+ "openInputs", "pendingOperations", "unknownOperations", "executionSignals"
4
+ ];
5
+ /**
6
+ * All filtering/counting stays in SQLite. Only page-sized rows and fixed
7
+ * attention samples cross into JS; no Task/Run/Message/Event body is decoded.
8
+ * The existing catalog and record tables remain the only stored authorities.
9
+ */
10
+ export function queryTaskCatalog(db, query) {
11
+ const scope = ["1=1"];
12
+ const params = { limit: query.limit };
13
+ if (query.taskId !== undefined) {
14
+ scope.push("c.task_id = @taskId");
15
+ params.taskId = query.taskId;
16
+ }
17
+ if (!query.all)
18
+ scope.push("c.status <> 'archived'");
19
+ const match = ["1=1"];
20
+ if (query.status !== undefined) {
21
+ match.push("status = @status");
22
+ params.status = query.status;
23
+ }
24
+ if (query.project !== undefined) {
25
+ match.push(`EXISTS (SELECT 1 FROM task_records r, json_each(r.payload, '$.projectBindings') p
26
+ WHERE r.task_id = facts.id AND json_extract(p.value, '$.projectId') = @project)`);
27
+ params.project = query.project;
28
+ }
29
+ if (query.search) {
30
+ match.push(`EXISTS (SELECT 1 FROM task_records r WHERE r.task_id = facts.id AND
31
+ (instr(lower(json_extract(r.payload, '$.title')), lower(@search)) > 0
32
+ OR instr(lower(r.task_id), lower(@search)) > 0
33
+ OR EXISTS (SELECT 1 FROM json_each(r.payload, '$.tags') tag
34
+ WHERE instr(lower(tag.value), lower(@search)) > 0)
35
+ OR EXISTS (SELECT 1 FROM json_each(r.payload, '$.projectBindings') b
36
+ JOIN projects p ON p.id = json_extract(b.value, '$.projectId')
37
+ WHERE instr(lower(json_extract(p.payload, '$.name')), lower(@search)) > 0)))`);
38
+ params.search = query.search;
39
+ }
40
+ if (query.attention !== undefined)
41
+ match.push(`${query.attention} > 0`);
42
+ const position = (name, op) => {
43
+ const value = query[name];
44
+ if (value === undefined)
45
+ return;
46
+ params[`${name}Date`] = value.createdAt;
47
+ params[`${name}Id`] = value.id;
48
+ match.push(`(createdAt, id) ${op} (@${name}Date, @${name}Id)`);
49
+ };
50
+ position("through", "<=");
51
+ // A fixed upper key excludes newly-created Tasks on later pages. Updates
52
+ // to mutable filters are current reads, not a snapshot/session protocol.
53
+ const after = [];
54
+ if (query.after !== undefined) {
55
+ params.afterDate = query.after.createdAt;
56
+ params.afterId = query.after.id;
57
+ after.push("(createdAt, id) > (@afterDate, @afterId)");
58
+ }
59
+ const count = (table, condition) => `(SELECT count(*) FROM ${table} x WHERE x.task_id = c.task_id AND (${condition}))`;
60
+ // Execution signals deliberately preserve raw inspectable conditions:
61
+ // live runs (including unknown admission/identity/stall), unresolved work,
62
+ // integrations, and pending Leader recovery. They are NOT a second
63
+ // classifier for working/blocked/success; precise execution stays in detail.
64
+ // Including all live runs is conservative: an off-page runtime attention
65
+ // cannot disappear just because a lightweight read did not fold its history.
66
+ const cte = `WITH facts AS MATERIALIZED (
67
+ SELECT c.task_id AS id, c.created_at AS createdAt, c.updated_at AS updatedAt, c.status,
68
+ ${count("work_items", "1=1")} AS workItems,
69
+ ${count("turns", "status = 'active'")} AS activeRuns,
70
+ ${count("input_requests", "status = 'open'")} AS openInputs,
71
+ ${count("durable_jobs", "status IN ('queued','running')")} AS pendingOperations,
72
+ ${count("durable_jobs", "status = 'unknown-needs-attention'")} AS unknownOperations,
73
+ CASE WHEN c.status IN ('active','draft') THEN
74
+ ${count("turns", "status = 'active'")} +
75
+ ${count("work_items", "status = 'open' OR (status = 'accepted' AND json_extract(payload, '$.currentExecutionGroupId') IS NOT NULL)")} +
76
+ ${count("integration_attempts", "status IN ('running','validating','blocked','conflicted','failed')")} +
77
+ ${count("review_rounds", "status IN ('pending','running','failed')")} +
78
+ ${count("task_projections", "kind = 'leader-failure' AND payload IS NOT NULL AND payload <> 'null'")} +
79
+ ${count("mailboxes", "role_name = 'leader' AND (json_type(pending) = 'object' OR json_type(processing) = 'object')")}
80
+ ELSE 0 END AS executionSignals
81
+ FROM tasks_catalog c WHERE ${scope.join(" AND ")}
82
+ ), matching AS MATERIALIZED (SELECT * FROM facts WHERE ${match.join(" AND ")})
83
+ SELECT
84
+ (SELECT json_group_array(json_object('status',status,'count',n))
85
+ FROM (SELECT status,count(*) n FROM facts GROUP BY status)) AS counts,
86
+ (SELECT count(*) FROM matching) AS total,
87
+ (SELECT json_object('id',id,'createdAt',createdAt) FROM matching
88
+ ORDER BY createdAt DESC,id DESC LIMIT 1) AS upper,
89
+ ${CATALOG_ATTENTION.map(kind => `
90
+ (SELECT sum(${kind}) FROM facts) AS ${kind}Count,
91
+ (SELECT count(*) FROM facts WHERE ${kind} > 0) AS ${kind}Tasks,
92
+ (SELECT json_group_array(id) FROM (SELECT id FROM facts WHERE ${kind} > 0
93
+ ORDER BY createdAt,id LIMIT 4)) AS ${kind}Ids`).join(",")},
94
+ (SELECT json_group_array(json_object(
95
+ 'id',page.id,'createdAt',page.createdAt,'updatedAt',page.updatedAt,'status',page.status,
96
+ 'title',substr(json_extract(r.payload,'$.title'),1,256),
97
+ 'summary',substr(json_extract(r.brief,'$.leaderSummary'),1,512),
98
+ 'summaryPresent',json(CASE WHEN r.brief IS NULL THEN 'false' ELSE 'true' END),
99
+ 'workItems',page.workItems,'activeRuns',page.activeRuns,
100
+ 'openInputs',page.openInputs,'pendingOperations',page.pendingOperations,
101
+ 'unknownOperations',page.unknownOperations,'executionSignals',page.executionSignals)
102
+ ORDER BY page.createdAt,page.id)
103
+ FROM (SELECT * FROM matching ${after.length ? `WHERE ${after.join(" AND ")}` : ""}
104
+ ORDER BY createdAt,id LIMIT @limit) page
105
+ JOIN task_records r ON r.task_id = page.id) AS rows`;
106
+ const result = db.prepare(cte).get(params);
107
+ const counts = { draft: 0, active: 0, completed: 0, cancelled: 0, archived: 0, total: 0 };
108
+ for (const entry of JSON.parse(result.counts)) {
109
+ counts[entry.status] = entry.count;
110
+ counts.total += entry.count;
111
+ }
112
+ return {
113
+ rows: JSON.parse(result.rows),
114
+ total: result.total,
115
+ through: result.upper === null ? null : JSON.parse(result.upper),
116
+ counts,
117
+ attention: Object.fromEntries(CATALOG_ATTENTION.map(kind => [kind, {
118
+ count: Number(result[`${kind}Count`] ?? 0),
119
+ taskCount: Number(result[`${kind}Tasks`] ?? 0),
120
+ taskIds: JSON.parse(result[`${kind}Ids`])
121
+ }]))
122
+ };
123
+ }
@@ -448,6 +448,8 @@ export function storedPublicationReference(value) {
448
448
  fields.push("targetBranch");
449
449
  if (reference.localCommit !== undefined)
450
450
  fields.push("localCommit");
451
+ if (reference.headCommit !== undefined)
452
+ fields.push("headCommit");
451
453
  if (reference.remoteCommit !== undefined)
452
454
  fields.push("remoteCommit");
453
455
  if (reference.evidence !== undefined)
@@ -1,6 +1,7 @@
1
- import { basename, dirname, join } from "node:path";
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
- const backupDirectory = join(dirname(home), `${basename(home)}-backups`);
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
  }