@zq-silk/yui 0.13.8 → 0.13.9

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 (67) hide show
  1. package/README.md +9 -8
  2. package/dist/cli/commandCatalog.js +21 -2
  3. package/dist/cli.js +42 -13
  4. package/dist/commands/agentCommands.js +1 -1
  5. package/dist/commands/configCommands.js +1 -86
  6. package/dist/commands/executionAuditCommands.js +17 -16
  7. package/dist/commands/globalRoleCommands.js +4 -4
  8. package/dist/commands/sessionCommands.js +2 -6
  9. package/dist/commands/taskActor.js +1 -2
  10. package/dist/commands/taskCommands.js +92 -9
  11. package/dist/commands/taskRoleRuntimeStatus.js +3 -3
  12. package/dist/config/configCatalog.js +1 -6
  13. package/dist/config/yuiConfig.js +0 -79
  14. package/dist/controller/clientRuntime.js +40 -3
  15. package/dist/controller/controller.js +29 -52
  16. package/dist/controller/fileSchedulerStoreAdapter.js +305 -1056
  17. package/dist/controller/runtime.js +12 -5
  18. package/dist/controller/runtimeHookRunFence.js +3 -9
  19. package/dist/controller/runtimeLaunchCoordinator.js +44 -67
  20. package/dist/controller/structuredProviderObservation.js +18 -5
  21. package/dist/coordination/workMailbox.js +4 -4
  22. package/dist/execution/executionHealth.js +1 -1
  23. package/dist/executor/agentExecutor.js +92 -68
  24. package/dist/executor/executorRegistry.js +8 -20
  25. package/dist/executor/fileRoleLaunchPlanner.js +24 -90
  26. package/dist/executor/turnCompletion.js +5 -5
  27. package/dist/lifecycle/exactRunTerminalization.js +1 -1
  28. package/dist/observability/executionAudit.js +40 -94
  29. package/dist/operator/operatorSessionHistory.js +7 -5
  30. package/dist/role/role.js +1 -1
  31. package/dist/run/agentRun.js +4 -54
  32. package/dist/runtime/agentDriver.js +2 -0
  33. package/dist/runtime/agentError.js +114 -0
  34. package/dist/runtime/agentHost.js +55 -82
  35. package/dist/runtime/builtinAgentDrivers.js +21 -9
  36. package/dist/runtime/builtinAgentErrorMappers.js +150 -0
  37. package/dist/runtime/exactControlPlane.js +6 -12
  38. package/dist/runtime/index.js +0 -1
  39. package/dist/runtime/launchBroker.js +5 -19
  40. package/dist/runtime/lifecycleReservation.js +20 -4
  41. package/dist/runtime/providerRuntimeIdentity.js +3 -2
  42. package/dist/runtime/runtimeBinding.js +0 -27
  43. package/dist/runtime/runtimeObservation.js +7 -16
  44. package/dist/runtime/runtimeSessionCandidate.js +3 -10
  45. package/dist/runtime/sessionLaunchRequest.js +1 -2
  46. package/dist/runtime/sessionReconciliation.js +2 -2
  47. package/dist/runtime/structuredProviderHost.js +44 -79
  48. package/dist/runtime/taskRuntimeIsolation.js +0 -7
  49. package/dist/runtime/tmuxAdapters.js +6 -49
  50. package/dist/scheduler/activeRoleRunDelivery.js +218 -168
  51. package/dist/scheduler/leaderWakeupProcessor.js +126 -86
  52. package/dist/scheduler/roleRunLiveness.js +4 -1
  53. package/dist/scheduler/roleRunStall.js +7 -11
  54. package/dist/scheduler/wakeReason.js +4 -0
  55. package/dist/storage/migration/productionRegistry.js +332 -0
  56. package/dist/storage/sqliteSchema.js +54 -2
  57. package/dist/storage/sqliteStore.js +11 -44
  58. package/dist/storage/taskStore.js +7 -35
  59. package/package.json +1 -1
  60. package/skills/yui-leader/SKILL.md +30 -8
  61. package/skills/yui-operator/SKILL.md +14 -6
  62. package/skills/yui-runtime/SKILL.md +6 -4
  63. package/dist/lifecycle/providerErrorClass.js +0 -152
  64. package/dist/run/providerRetry.js +0 -226
  65. package/dist/run/providerRetryConfig.js +0 -27
  66. package/dist/runtime/providerErrorCodes.js +0 -278
  67. package/dist/runtime/providerRecoveryDecision.js +0 -55
@@ -51,6 +51,8 @@ const AGENT_RUN_CONTEXT_PROTOCOL_FROM_VERSION = 7;
51
51
  const AGENT_RUN_CONTEXT_PROTOCOL_TO_VERSION = 8;
52
52
  const AGENT_RUN_RETRY_EPISODE_FROM_VERSION = 8;
53
53
  const AGENT_RUN_RETRY_EPISODE_TO_VERSION = 9;
54
+ const AGENT_RUN_ERROR_FACTS_FROM_VERSION = 9;
55
+ const AGENT_RUN_ERROR_FACTS_TO_VERSION = 10;
54
56
  const MESSAGE_WAKE_POLICY_FROM_VERSION = 2;
55
57
  const MESSAGE_WAKE_POLICY_TO_VERSION = 3;
56
58
  const REVIEW_ROUND_FROM_VERSION = 2;
@@ -99,6 +101,8 @@ const RELEASE_WORKFLOW_FROM_VERSION = 0;
99
101
  const RELEASE_WORKFLOW_TO_VERSION = 1;
100
102
  const WORK_MAILBOX_FROM_VERSION = 1;
101
103
  const WORK_MAILBOX_TO_VERSION = 2;
104
+ const HOST_OWNED_WORK_MAILBOX_FROM_VERSION = 2;
105
+ const HOST_OWNED_WORK_MAILBOX_TO_VERSION = 3;
102
106
  const TASK_ROLE_FROM_VERSION = 3;
103
107
  const TASK_ROLE_TO_VERSION = 4;
104
108
  const TASK_ROLE_SESSION_SET_FROM_VERSION = 4;
@@ -107,12 +111,18 @@ const STRUCTURED_PROVIDER_SESSION_SET_FROM_VERSION = 5;
107
111
  const STRUCTURED_PROVIDER_SESSION_SET_TO_VERSION = 6;
108
112
  const AGENT_FIRST_SESSION_SET_FROM_VERSION = 6;
109
113
  const AGENT_FIRST_SESSION_SET_TO_VERSION = 7;
114
+ const UNIFIED_TASK_SESSION_SET_FROM_VERSION = 7;
115
+ const UNIFIED_TASK_SESSION_SET_TO_VERSION = 8;
116
+ const UNIFIED_GLOBAL_SESSION_SET_FROM_VERSION = 3;
117
+ const UNIFIED_GLOBAL_SESSION_SET_TO_VERSION = 4;
110
118
  const PUBLICATION_REFERENCE_FROM_VERSION = 0;
111
119
  const PUBLICATION_REFERENCE_TO_VERSION = 1;
112
120
  const CONFIG_FROM_VERSION = 1;
113
121
  const CONFIG_TO_VERSION = 2;
114
122
  const CONFIG_REVIEW_CONTROLS_FROM_VERSION = 2;
115
123
  const CONFIG_REVIEW_CONTROLS_TO_VERSION = 3;
124
+ const CONFIG_AGENT_RECOVERY_FROM_VERSION = 3;
125
+ const CONFIG_AGENT_RECOVERY_TO_VERSION = 4;
116
126
  /**
117
127
  * Build the authoritative production graph. Transition intent and executable
118
128
  * transforms are registered together here; compatible loading and offline
@@ -163,6 +173,7 @@ export function createProductionStorageRegistry() {
163
173
  .registerOfflineMigration(projectOwnershipStep())
164
174
  .registerOfflineMigration(configV2Step())
165
175
  .registerOfflineMigration(configReviewControlsRemovalStep())
176
+ .registerOfflineMigration(configAgentRecoveryStep())
166
177
  .registerCompatible(projectKnowledgeProposalsStep())
167
178
  .registerCompatible(projectLifecycleStep())
168
179
  .registerOfflineMigration(taskWorkspaceIdentityStep())
@@ -183,6 +194,7 @@ export function createProductionStorageRegistry() {
183
194
  .registerOfflineMigration(recordFamilyStep("agentRun", AGENT_RUN_OPTIONAL_FIELDS_FROM_VERSION, AGENT_RUN_OPTIONAL_FIELDS_TO_VERSION, "agentRuns"))
184
195
  .registerOfflineMigration(agentRunContextProtocolStep())
185
196
  .registerOfflineMigration(agentRunRetryEpisodeStep())
197
+ .registerOfflineMigration(agentRunErrorFactsStep())
186
198
  .registerOfflineMigration(messageWakePolicyStep())
187
199
  .registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_FROM_VERSION, REVIEW_ROUND_TO_VERSION, "reviewRounds"))
188
200
  .registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_GIT_SNAPSHOT_FROM_VERSION, REVIEW_ROUND_GIT_SNAPSHOT_TO_VERSION, "reviewRounds"))
@@ -207,10 +219,13 @@ export function createProductionStorageRegistry() {
207
219
  .registerOfflineMigration(capabilityGrantIntroductionStep())
208
220
  .registerOfflineMigration(releaseWorkflowIntroductionStep())
209
221
  .registerOfflineMigration(workMailboxV2Step())
222
+ .registerOfflineMigration(hostOwnedWorkMailboxStep())
210
223
  .registerOfflineMigration(agentRunOwnedTaskRoleStep())
211
224
  .registerOfflineMigration(taskRoleSessionSetV5Step())
212
225
  .registerOfflineMigration(structuredProviderSessionSetV6Step())
213
226
  .registerOfflineMigration(agentFirstSessionSetV7Step())
227
+ .registerOfflineMigration(unifiedTaskSessionSetStep())
228
+ .registerOfflineMigration(unifiedGlobalSessionSetStep())
214
229
  .registerOfflineMigration(publicationReferenceIntroductionStep());
215
230
  assertRegistryCoversBaselineToCurrent(registry);
216
231
  return registry;
@@ -379,6 +394,53 @@ function migrateConfigV2ToV3(snapshot) {
379
394
  }
380
395
  };
381
396
  }
397
+ /** Removes Core-owned Provider retry policy; recovery is chosen from AgentError facts. */
398
+ function configAgentRecoveryStep() {
399
+ return {
400
+ axis: "record",
401
+ recordKind: "config",
402
+ fromVersion: CONFIG_AGENT_RECOVERY_FROM_VERSION,
403
+ toVersion: CONFIG_AGENT_RECOVERY_TO_VERSION,
404
+ preconditions: requireConfigV3,
405
+ transform: migrateConfigV3ToV4,
406
+ declaredEffects: []
407
+ };
408
+ }
409
+ function requireConfigV3(snapshot) {
410
+ const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
411
+ if (versions.config !== CONFIG_AGENT_RECOVERY_FROM_VERSION) {
412
+ throw new Error(`Record config migration requires manifest version ${CONFIG_AGENT_RECOVERY_FROM_VERSION}.`);
413
+ }
414
+ if (snapshot.state === null)
415
+ return;
416
+ const config = asObject(snapshot.state.config, "Yui config");
417
+ if (config.schemaVersion !== CONFIG_AGENT_RECOVERY_FROM_VERSION) {
418
+ throw new Error(`Yui config must use schemaVersion ${CONFIG_AGENT_RECOVERY_FROM_VERSION} before migration.`);
419
+ }
420
+ }
421
+ function migrateConfigV3ToV4(snapshot) {
422
+ requireConfigV3(snapshot);
423
+ const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
424
+ const schemaManifest = {
425
+ ...snapshot.schemaManifest,
426
+ recordVersions: { ...versions, config: CONFIG_AGENT_RECOVERY_TO_VERSION }
427
+ };
428
+ if (snapshot.state === null)
429
+ return { schemaManifest, state: null };
430
+ const config = asObject(snapshot.state.config, "Yui config");
431
+ const { providerRetryMode: _mode, providerRetryAdapters: _adapters, providerRetryDelaysSeconds: _delays, providerRetryMaxWindowSeconds: _window, ...retained } = config;
432
+ void _mode;
433
+ void _adapters;
434
+ void _delays;
435
+ void _window;
436
+ return {
437
+ schemaManifest,
438
+ state: {
439
+ ...snapshot.state,
440
+ config: { ...retained, schemaVersion: CONFIG_AGENT_RECOVERY_TO_VERSION }
441
+ }
442
+ };
443
+ }
382
444
  function workItemExecutionGroupHistoryStep() {
383
445
  return {
384
446
  axis: "record",
@@ -2245,6 +2307,46 @@ function migrateAgentRunsToRetryEpisodes(snapshot) {
2245
2307
  }
2246
2308
  return { schemaManifest, state: { ...snapshot.state, tasks: nextTasks } };
2247
2309
  }
2310
+ /** Deletes the retired retry episode; an AgentRun now records scheduling only. */
2311
+ function agentRunErrorFactsStep() {
2312
+ return {
2313
+ axis: "record",
2314
+ recordKind: "agentRun",
2315
+ fromVersion: AGENT_RUN_ERROR_FACTS_FROM_VERSION,
2316
+ toVersion: AGENT_RUN_ERROR_FACTS_TO_VERSION,
2317
+ preconditions: (snapshot) => requireRecordFamilyVersion(snapshot, "agentRun", AGENT_RUN_ERROR_FACTS_FROM_VERSION, "agentRuns"),
2318
+ transform: migrateAgentRunsToErrorFacts,
2319
+ declaredEffects: []
2320
+ };
2321
+ }
2322
+ function migrateAgentRunsToErrorFacts(snapshot) {
2323
+ requireRecordFamilyVersion(snapshot, "agentRun", AGENT_RUN_ERROR_FACTS_FROM_VERSION, "agentRuns");
2324
+ const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2325
+ const schemaManifest = {
2326
+ ...snapshot.schemaManifest,
2327
+ recordVersions: { ...versions, agentRun: AGENT_RUN_ERROR_FACTS_TO_VERSION }
2328
+ };
2329
+ if (snapshot.state === null)
2330
+ return { schemaManifest, state: null };
2331
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
2332
+ const nextTasks = {};
2333
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
2334
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
2335
+ const records = asObject(task.agentRuns ?? {}, `agentRun map ${taskId}`);
2336
+ const nextRecords = {};
2337
+ for (const [runId, rawRecord] of Object.entries(records)) {
2338
+ const record = asObject(rawRecord, `agentRun ${taskId}/${runId}`);
2339
+ const { providerRetry: _retry, ...retained } = record;
2340
+ void _retry;
2341
+ nextRecords[runId] = {
2342
+ ...retained,
2343
+ schemaVersion: AGENT_RUN_ERROR_FACTS_TO_VERSION
2344
+ };
2345
+ }
2346
+ nextTasks[taskId] = { ...task, agentRuns: nextRecords };
2347
+ }
2348
+ return { schemaManifest, state: { ...snapshot.state, tasks: nextTasks } };
2349
+ }
2248
2350
  function requiredMigrationPositiveInteger(value, label) {
2249
2351
  if (!Number.isSafeInteger(value) || value < 1) {
2250
2352
  throw new Error(`${label} must be a positive integer.`);
@@ -2683,6 +2785,77 @@ function migrateWorkMailboxV1ToV2(snapshot) {
2683
2785
  state: { ...snapshot.state, mailboxes: nextMailboxes }
2684
2786
  };
2685
2787
  }
2788
+ /** A Host launch reservation belongs to the Role owner and launchId, never a Run. */
2789
+ function hostOwnedWorkMailboxStep() {
2790
+ return {
2791
+ axis: "record",
2792
+ recordKind: "workMailbox",
2793
+ fromVersion: HOST_OWNED_WORK_MAILBOX_FROM_VERSION,
2794
+ toVersion: HOST_OWNED_WORK_MAILBOX_TO_VERSION,
2795
+ preconditions: requireWorkMailboxV2Family,
2796
+ transform: migrateWorkMailboxV2ToV3,
2797
+ declaredEffects: []
2798
+ };
2799
+ }
2800
+ function requireWorkMailboxV2Family(snapshot) {
2801
+ const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2802
+ if (versions.workMailbox !== HOST_OWNED_WORK_MAILBOX_FROM_VERSION) {
2803
+ throw new Error(`Record workMailbox migration requires manifest version ${HOST_OWNED_WORK_MAILBOX_FROM_VERSION}.`);
2804
+ }
2805
+ if (snapshot.state === null)
2806
+ return;
2807
+ const mailboxes = asObject(snapshot.state.mailboxes, "state mailboxes");
2808
+ for (const [key, rawMailbox] of Object.entries(mailboxes)) {
2809
+ const mailbox = asObject(rawMailbox, `WorkMailbox ${key}`);
2810
+ if (mailbox.schemaVersion !== HOST_OWNED_WORK_MAILBOX_FROM_VERSION) {
2811
+ throw new Error(`WorkMailbox ${key} must use schemaVersion ${HOST_OWNED_WORK_MAILBOX_FROM_VERSION}.`);
2812
+ }
2813
+ }
2814
+ }
2815
+ function migrateWorkMailboxV2ToV3(snapshot) {
2816
+ requireWorkMailboxV2Family(snapshot);
2817
+ const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2818
+ const schemaManifest = {
2819
+ ...snapshot.schemaManifest,
2820
+ recordVersions: { ...versions, workMailbox: HOST_OWNED_WORK_MAILBOX_TO_VERSION }
2821
+ };
2822
+ if (snapshot.state === null)
2823
+ return { schemaManifest, state: null };
2824
+ const mailboxes = asObject(snapshot.state.mailboxes, "state mailboxes");
2825
+ const nextMailboxes = {};
2826
+ for (const [key, rawMailbox] of Object.entries(mailboxes)) {
2827
+ const mailbox = asObject(rawMailbox, `WorkMailbox ${key}`);
2828
+ const target = asObject(mailbox.target, `WorkMailbox ${key} target`);
2829
+ const processing = mailbox.processing === null
2830
+ ? null
2831
+ : asObject(mailbox.processing, `WorkMailbox ${key} processing`);
2832
+ const batch = processing === null
2833
+ ? null
2834
+ : asObject(processing.batch, `WorkMailbox ${key} processing batch`);
2835
+ const lifecycleReservation = (target.kind === "role-runtime"
2836
+ || target.kind === "global-role-runtime")
2837
+ && Array.isArray(batch?.reasons)
2838
+ && batch.reasons.includes("runtime-launch-reserved");
2839
+ const nextProcessing = processing === null
2840
+ ? null
2841
+ : lifecycleReservation
2842
+ ? (() => {
2843
+ const { executionRef: _run, ...retained } = processing;
2844
+ void _run;
2845
+ return retained;
2846
+ })()
2847
+ : processing;
2848
+ nextMailboxes[key] = {
2849
+ ...mailbox,
2850
+ schemaVersion: HOST_OWNED_WORK_MAILBOX_TO_VERSION,
2851
+ processing: nextProcessing
2852
+ };
2853
+ }
2854
+ return {
2855
+ schemaManifest,
2856
+ state: { ...snapshot.state, mailboxes: nextMailboxes }
2857
+ };
2858
+ }
2686
2859
  /**
2687
2860
  * v1 used `processing` for both internal/initial dispatch ownership and later
2688
2861
  * model input. A crashed later input may already have reached the provider;
@@ -2861,6 +3034,165 @@ function migrateTaskRoleSessionSetV6ToV7(snapshot) {
2861
3034
  }
2862
3035
  return { schemaManifest, state: { ...snapshot.state, tasks: nextTasks } };
2863
3036
  }
3037
+ /** Session stores existence only; Host readiness and Turn activity are runtime facts. */
3038
+ function unifiedTaskSessionSetStep() {
3039
+ return {
3040
+ axis: "record",
3041
+ recordKind: "taskRoleSessionSet",
3042
+ fromVersion: UNIFIED_TASK_SESSION_SET_FROM_VERSION,
3043
+ toVersion: UNIFIED_TASK_SESSION_SET_TO_VERSION,
3044
+ preconditions: requireTaskRoleSessionSetV7Family,
3045
+ transform: migrateTaskRoleSessionSetV7ToV8,
3046
+ declaredEffects: []
3047
+ };
3048
+ }
3049
+ function requireTaskRoleSessionSetV7Family(snapshot) {
3050
+ const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3051
+ if (versions.taskRoleSessionSet !== UNIFIED_TASK_SESSION_SET_FROM_VERSION) {
3052
+ throw new Error(`Record taskRoleSessionSet migration requires manifest version ${UNIFIED_TASK_SESSION_SET_FROM_VERSION}.`);
3053
+ }
3054
+ if (snapshot.state === null)
3055
+ return;
3056
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
3057
+ for (const [taskId, rawAggregate] of Object.entries(tasks)) {
3058
+ const aggregate = asObject(rawAggregate, `Task aggregate ${taskId}`);
3059
+ const sets = asObject(aggregate.roleSessionSets, `Task Role session sets ${taskId}`);
3060
+ for (const [roleName, rawSet] of Object.entries(sets)) {
3061
+ const set = asObject(rawSet, `Task Role session set ${taskId}/${roleName}`);
3062
+ if (set.schemaVersion !== UNIFIED_TASK_SESSION_SET_FROM_VERSION) {
3063
+ throw new Error(`Task Role session set ${taskId}/${roleName} must use schemaVersion ${UNIFIED_TASK_SESSION_SET_FROM_VERSION}.`);
3064
+ }
3065
+ }
3066
+ }
3067
+ }
3068
+ function migrateTaskRoleSessionSetV7ToV8(snapshot) {
3069
+ requireTaskRoleSessionSetV7Family(snapshot);
3070
+ const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3071
+ const schemaManifest = {
3072
+ ...snapshot.schemaManifest,
3073
+ recordVersions: {
3074
+ ...versions,
3075
+ taskRoleSessionSet: UNIFIED_TASK_SESSION_SET_TO_VERSION
3076
+ }
3077
+ };
3078
+ if (snapshot.state === null)
3079
+ return { schemaManifest, state: null };
3080
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
3081
+ const nextTasks = {};
3082
+ for (const [taskId, rawAggregate] of Object.entries(tasks)) {
3083
+ const aggregate = asObject(rawAggregate, `Task aggregate ${taskId}`);
3084
+ const sets = asObject(aggregate.roleSessionSets, `Task Role session sets ${taskId}`);
3085
+ const nextSets = {};
3086
+ for (const [roleName, rawSet] of Object.entries(sets)) {
3087
+ const set = asObject(rawSet, `Task Role session set ${taskId}/${roleName}`);
3088
+ const sessions = migrateRoleAgentSessionMap(set.sessions, `Task Role sessions ${taskId}/${roleName}`);
3089
+ const history = set.history === undefined
3090
+ ? undefined
3091
+ : migrateRoleAgentSessionArray(set.history, `Task Role session history ${taskId}/${roleName}`);
3092
+ nextSets[roleName] = {
3093
+ ...set,
3094
+ schemaVersion: UNIFIED_TASK_SESSION_SET_TO_VERSION,
3095
+ sessions,
3096
+ ...(history === undefined ? {} : { history })
3097
+ };
3098
+ }
3099
+ nextTasks[taskId] = { ...aggregate, roleSessionSets: nextSets };
3100
+ }
3101
+ return { schemaManifest, state: { ...snapshot.state, tasks: nextTasks } };
3102
+ }
3103
+ function unifiedGlobalSessionSetStep() {
3104
+ return {
3105
+ axis: "record",
3106
+ recordKind: "globalRoleSessionSet",
3107
+ fromVersion: UNIFIED_GLOBAL_SESSION_SET_FROM_VERSION,
3108
+ toVersion: UNIFIED_GLOBAL_SESSION_SET_TO_VERSION,
3109
+ preconditions: requireGlobalRoleSessionSetV3Family,
3110
+ transform: migrateGlobalRoleSessionSetV3ToV4,
3111
+ declaredEffects: []
3112
+ };
3113
+ }
3114
+ function requireGlobalRoleSessionSetV3Family(snapshot) {
3115
+ const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3116
+ if (versions.globalRoleSessionSet !== UNIFIED_GLOBAL_SESSION_SET_FROM_VERSION) {
3117
+ throw new Error(`Record globalRoleSessionSet migration requires manifest version ${UNIFIED_GLOBAL_SESSION_SET_FROM_VERSION}.`);
3118
+ }
3119
+ if (snapshot.state === null)
3120
+ return;
3121
+ const sets = asObject(snapshot.state.globalRoleSessionSets, "Global Role session sets");
3122
+ for (const [roleName, rawSet] of Object.entries(sets)) {
3123
+ const set = asObject(rawSet, `Global Role session set ${roleName}`);
3124
+ if (set.schemaVersion !== UNIFIED_GLOBAL_SESSION_SET_FROM_VERSION) {
3125
+ throw new Error(`Global Role session set ${roleName} must use schemaVersion ${UNIFIED_GLOBAL_SESSION_SET_FROM_VERSION}.`);
3126
+ }
3127
+ }
3128
+ }
3129
+ function migrateGlobalRoleSessionSetV3ToV4(snapshot) {
3130
+ requireGlobalRoleSessionSetV3Family(snapshot);
3131
+ const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
3132
+ const schemaManifest = {
3133
+ ...snapshot.schemaManifest,
3134
+ recordVersions: {
3135
+ ...versions,
3136
+ globalRoleSessionSet: UNIFIED_GLOBAL_SESSION_SET_TO_VERSION
3137
+ }
3138
+ };
3139
+ if (snapshot.state === null)
3140
+ return { schemaManifest, state: null };
3141
+ const sets = asObject(snapshot.state.globalRoleSessionSets, "Global Role session sets");
3142
+ const nextSets = {};
3143
+ for (const [roleName, rawSet] of Object.entries(sets)) {
3144
+ const set = asObject(rawSet, `Global Role session set ${roleName}`);
3145
+ const history = set.history === undefined
3146
+ ? undefined
3147
+ : migrateRoleAgentSessionMap(set.history, `Global Role session history ${roleName}`);
3148
+ nextSets[roleName] = {
3149
+ ...set,
3150
+ schemaVersion: UNIFIED_GLOBAL_SESSION_SET_TO_VERSION,
3151
+ sessions: migrateRoleAgentSessionMap(set.sessions, `Global Role sessions ${roleName}`),
3152
+ ...(history === undefined ? {} : { history })
3153
+ };
3154
+ }
3155
+ return {
3156
+ schemaManifest,
3157
+ state: { ...snapshot.state, globalRoleSessionSets: nextSets }
3158
+ };
3159
+ }
3160
+ function migrateRoleAgentSessionMap(value, label) {
3161
+ const sessions = asObject(value, label);
3162
+ return Object.fromEntries(Object.entries(sessions).map(([key, session]) => [
3163
+ key,
3164
+ migrateRoleAgentSessionV3(session, `${label}/${key}`)
3165
+ ]));
3166
+ }
3167
+ function migrateRoleAgentSessionArray(value, label) {
3168
+ if (!Array.isArray(value))
3169
+ throw new Error(`${label} must be an array.`);
3170
+ return value.map((session, index) => migrateRoleAgentSessionV3(session, `${label}/${String(index)}`));
3171
+ }
3172
+ function migrateRoleAgentSessionV3(value, label) {
3173
+ const session = asObject(value, label);
3174
+ if (session.schemaVersion !== 3) {
3175
+ throw new Error(`${label} must use schemaVersion 3.`);
3176
+ }
3177
+ const status = session.status;
3178
+ if (status !== "reserved"
3179
+ && status !== "ready"
3180
+ && status !== "running"
3181
+ && status !== "stopped"
3182
+ && status !== "broken") {
3183
+ throw new Error(`${label} has an invalid Session status.`);
3184
+ }
3185
+ const { status: _status, ...retained } = session;
3186
+ void _status;
3187
+ return {
3188
+ ...retained,
3189
+ schemaVersion: 4,
3190
+ status: status === "stopped" || status === "broken" ? "ended" : "active",
3191
+ ...(status === "stopped"
3192
+ ? { endReason: "stopped" }
3193
+ : status === "broken" ? { endReason: "failed" } : {})
3194
+ };
3195
+ }
2864
3196
  function requireTaskRoleSessionSetV5Family(snapshot) {
2865
3197
  const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2866
3198
  if (versions.taskRoleSessionSet !== STRUCTURED_PROVIDER_SESSION_SET_FROM_VERSION) {
@@ -27,7 +27,7 @@ export const SQLITE_LAYOUT_VERSION = 7;
27
27
  /** The aggregate version of the normalized SQLite schema. */
28
28
  export const SQLITE_AGGREGATE_VERSION = 1;
29
29
  /** The current schema migration version. */
30
- export const SQLITE_SCHEMA_VERSION = 17;
30
+ export const SQLITE_SCHEMA_VERSION = 18;
31
31
  /** Telemetry retention bounds (§4.4). Open question 3 in §11; defaults from the design. */
32
32
  export const TELEMETRY_KEEP_PER_GENERATION = 200;
33
33
  export const TELEMETRY_RUN_CAP = 50_000;
@@ -934,6 +934,57 @@ CREATE TABLE IF NOT EXISTS context_snapshots (
934
934
  CREATE UNIQUE INDEX IF NOT EXISTS idx_context_snapshots_scope_sequence
935
935
  ON context_snapshots(task_id, scope, COALESCE(scope_ref, ''), sequence);
936
936
  `;
937
+ /**
938
+ * Migration 18: align the current-Session hot projection with Session v4.
939
+ *
940
+ * Session existence now has one lifecycle: active or ended. Ended Sessions do
941
+ * not belong in this bounded projection, so row presence itself means active;
942
+ * the projection carries no independently writable lifecycle status. Host
943
+ * cleanup is derived solely from whether the Session still names a Host
944
+ * activation. Valid earlier live rows are projected into that contract;
945
+ * terminal broken rows disappear with the rest of ended Session history.
946
+ */
947
+ const MIGRATION_18_SQL = `
948
+ DROP INDEX idx_runtime_session_cleanup_required;
949
+ ALTER TABLE runtime_session_candidates RENAME TO runtime_session_candidates_v11;
950
+
951
+ CREATE TABLE runtime_session_candidates (
952
+ scope TEXT NOT NULL CHECK (scope IN ('task','global')),
953
+ task_id TEXT NOT NULL,
954
+ role_name TEXT NOT NULL,
955
+ agent_id TEXT NOT NULL,
956
+ adapter_id TEXT NOT NULL,
957
+ native_session_id TEXT NOT NULL,
958
+ launch_id TEXT,
959
+ session_updated_at TEXT NOT NULL,
960
+ cleanup_required INTEGER NOT NULL CHECK (cleanup_required IN (0,1)),
961
+ PRIMARY KEY (scope, task_id, role_name),
962
+ CHECK (
963
+ (scope = 'task' AND length(task_id) > 0)
964
+ OR (scope = 'global' AND task_id = '')
965
+ ),
966
+ CHECK (
967
+ cleanup_required = CASE WHEN launch_id IS NOT NULL THEN 1 ELSE 0 END
968
+ )
969
+ );
970
+
971
+ INSERT INTO runtime_session_candidates (
972
+ scope, task_id, role_name, agent_id, adapter_id, native_session_id,
973
+ launch_id, session_updated_at, cleanup_required
974
+ )
975
+ SELECT
976
+ scope, task_id, role_name, agent_id, adapter_id, native_session_id,
977
+ launch_id, session_updated_at,
978
+ CASE WHEN launch_id IS NOT NULL THEN 1 ELSE 0 END
979
+ FROM runtime_session_candidates_v11
980
+ WHERE status IN ('reserved','ready','running');
981
+
982
+ DROP TABLE runtime_session_candidates_v11;
983
+
984
+ CREATE INDEX idx_runtime_session_cleanup_required
985
+ ON runtime_session_candidates(scope, task_id, role_name)
986
+ WHERE cleanup_required = 1;
987
+ `;
937
988
  /**
938
989
  * The single forward migration history for current SQLite Homes. New durable
939
990
  * layout, aggregate, and record changes belong here and must be expressible as
@@ -957,7 +1008,8 @@ const MIGRATIONS = [
957
1008
  { version: 14, axis: "record", recordKind: "workMailbox", sql: MIGRATION_14_SQL },
958
1009
  { version: 15, axis: "record", recordKind: "publicationReference", sql: MIGRATION_15_SQL },
959
1010
  { version: 16, axis: "record", recordKind: "taskWake", sql: MIGRATION_16_SQL },
960
- { version: 17, axis: "record", recordKind: "contextSnapshot", sql: MIGRATION_17_SQL }
1011
+ { version: 17, axis: "record", recordKind: "contextSnapshot", sql: MIGRATION_17_SQL },
1012
+ { version: 18, axis: "layout", sql: MIGRATION_18_SQL }
961
1013
  ];
962
1014
  /** Current hot-path indexes whose absence would invalidate a current Home. */
963
1015
  const REQUIRED_SCHEMA_INDEXES = [
@@ -1150,7 +1150,7 @@ export class SqliteTaskStore {
1150
1150
  ? ""
1151
1151
  : ` WHERE ${predicates.join(" AND ")}`;
1152
1152
  const rows = this.#db.prepare(`SELECT scope, task_id, role_name, agent_id, adapter_id,
1153
- native_session_id, launch_id, status, session_updated_at,
1153
+ native_session_id, launch_id, session_updated_at,
1154
1154
  cleanup_required
1155
1155
  FROM runtime_session_candidates${where}`).all(...parameters);
1156
1156
  const candidates = rows.map((row) => ({
@@ -1161,7 +1161,6 @@ export class SqliteTaskStore {
1161
1161
  adapterId: row.adapter_id,
1162
1162
  nativeSessionId: row.native_session_id,
1163
1163
  ...(row.launch_id === null ? {} : { launchId: row.launch_id }),
1164
- status: row.status,
1165
1164
  sessionUpdatedAt: row.session_updated_at,
1166
1165
  cleanupRequired: row.cleanup_required === 1
1167
1166
  })).sort(compareRuntimeSessionCandidates);
@@ -1217,14 +1216,14 @@ export class SqliteTaskStore {
1217
1216
  json_extract(active_session, '$.adapterId') AS adapter_id,
1218
1217
  json_extract(active_session, '$.nativeSessionId') AS native_session_id,
1219
1218
  json_extract(active_session, '$.launchId') AS launch_id,
1220
- json_extract(active_session, '$.status') AS status,
1219
+ CASE
1220
+ WHEN json_extract(active_session, '$.status') = 'active'
1221
+ THEN 1 ELSE 0
1222
+ END AS is_active,
1221
1223
  json_extract(active_session, '$.updatedAt') AS session_updated_at,
1222
1224
  CASE
1223
- WHEN json_extract(active_session, '$.status') NOT IN ('stopped','broken')
1224
- AND (
1225
- json_extract(active_session, '$.status') = 'running'
1226
- OR json_type(active_session, '$.launchId') = 'text'
1227
- )
1225
+ WHEN json_extract(active_session, '$.status') = 'active'
1226
+ AND json_type(active_session, '$.launchId') = 'text'
1228
1227
  THEN 1 ELSE 0
1229
1228
  END AS cleanup_required
1230
1229
  FROM active`).get(...source.parameters);
@@ -1245,7 +1244,7 @@ export class SqliteTaskStore {
1245
1244
  && row.adapter_id === candidate.adapterId
1246
1245
  && row.native_session_id === candidate.nativeSessionId
1247
1246
  && (row.launch_id ?? undefined) === candidate.launchId
1248
- && row.status === candidate.status
1247
+ && row.is_active === 1
1249
1248
  && row.session_updated_at === candidate.sessionUpdatedAt
1250
1249
  && row.cleanup_required === (candidate.cleanupRequired ? 1 : 0);
1251
1250
  }
@@ -1277,16 +1276,15 @@ export class SqliteTaskStore {
1277
1276
  const taskId = candidate.owner.scope === "task" ? candidate.owner.taskId : "";
1278
1277
  this.#db.prepare(`INSERT INTO runtime_session_candidates (
1279
1278
  scope, task_id, role_name, agent_id, adapter_id, native_session_id,
1280
- launch_id, status, session_updated_at, cleanup_required
1281
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1279
+ launch_id, session_updated_at, cleanup_required
1280
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1282
1281
  ON CONFLICT(scope, task_id, role_name) DO UPDATE SET
1283
1282
  agent_id = excluded.agent_id,
1284
1283
  adapter_id = excluded.adapter_id,
1285
1284
  native_session_id = excluded.native_session_id,
1286
1285
  launch_id = excluded.launch_id,
1287
- status = excluded.status,
1288
1286
  session_updated_at = excluded.session_updated_at,
1289
- cleanup_required = excluded.cleanup_required`).run(candidate.owner.scope, taskId, candidate.owner.roleName, candidate.agentId, candidate.adapterId, candidate.nativeSessionId, candidate.launchId ?? null, candidate.status, candidate.sessionUpdatedAt, candidate.cleanupRequired ? 1 : 0);
1287
+ cleanup_required = excluded.cleanup_required`).run(candidate.owner.scope, taskId, candidate.owner.roleName, candidate.agentId, candidate.adapterId, candidate.nativeSessionId, candidate.launchId ?? null, candidate.sessionUpdatedAt, candidate.cleanupRequired ? 1 : 0);
1290
1288
  }
1291
1289
  #deleteRuntimeSessionCandidate(owner) {
1292
1290
  this.#db.prepare(`DELETE FROM runtime_session_candidates
@@ -1585,37 +1583,6 @@ export class SqliteTaskStore {
1585
1583
  payload = excluded.payload, updated_at = excluded.updated_at`).run(run.taskId, run.id, run.roleName, run.status, this.#json(run), this.#now());
1586
1584
  });
1587
1585
  }
1588
- /**
1589
- * Issue 04: SQLite-native pending retry query. A single indexed scan
1590
- * replaces the adapter's per-Task in-memory sweep, so Controller deadline
1591
- * arming no longer materializes every Task and Run in JavaScript.
1592
- */
1593
- listPendingProviderRetries(taskIds) {
1594
- const selectedTaskIds = taskIds === undefined
1595
- ? undefined
1596
- : [...new Set(taskIds)].sort(numericCompare);
1597
- if (selectedTaskIds?.length === 0)
1598
- return [];
1599
- const taskPredicate = selectedTaskIds === undefined
1600
- ? ""
1601
- : ` AND tc.task_id IN (${selectedTaskIds.map(() => "?").join(", ")})`;
1602
- const rows = this.#db.prepare(`SELECT DISTINCT ar.task_id AS taskId, ar.run_id AS runId, ar.role_name AS roleName,
1603
- json_extract(ar.payload, '$.providerRetry.state') AS state,
1604
- CASE json_extract(ar.payload, '$.providerRetry.state')
1605
- WHEN 'scheduled' THEN json_extract(ar.payload, '$.providerRetry.nextAttemptAt')
1606
- ELSE json_extract(ar.payload, '$.providerRetry.episodeDeadlineAt')
1607
- END AS dueAt
1608
- FROM tasks_catalog tc INDEXED BY idx_tasks_active
1609
- JOIN active_runs ap ON ap.task_id = tc.task_id
1610
- JOIN agent_runs ar ON ar.task_id = ap.task_id AND ar.run_id = ap.run_id
1611
- WHERE tc.is_active = 1
1612
- AND ar.status = 'active'
1613
- AND json_extract(ar.payload, '$.providerRetry.state') IN
1614
- ('scheduled', 'dispatching', 'awaiting-progress')${taskPredicate}`).all(...(selectedTaskIds ?? []));
1615
- return rows.sort((left, right) => (numericCompare(left.taskId, right.taskId)
1616
- || numericCompare(left.roleName, right.roleName)
1617
- || numericCompare(left.runId, right.runId)));
1618
- }
1619
1586
  // -- review rounds ----------------------------------------------------------
1620
1587
  nextReviewRoundId(taskId) { return this.#nextTaskRecordId(taskId, "reviewRound"); }
1621
1588
  getReviewRound(taskId, reviewRoundId) {