@zq-silk/yui 0.5.3 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/agentConfigurationPicker.js +1 -1
  4. package/dist/cli/commandCatalog.js +251 -13
  5. package/dist/cli/updateOrchestrator.js +8 -0
  6. package/dist/cli/updatePorts.js +76 -22
  7. package/dist/cli.js +264 -20
  8. package/dist/commands/configCommands.js +83 -9
  9. package/dist/commands/controllerCommands.js +103 -0
  10. package/dist/commands/deliveryGuardPreflight.js +35 -0
  11. package/dist/commands/durableJobCommands.js +231 -0
  12. package/dist/commands/executionAuditCommands.js +193 -0
  13. package/dist/commands/grantCommands.js +374 -0
  14. package/dist/commands/projectCommands.js +119 -81
  15. package/dist/commands/releaseCommands.js +444 -0
  16. package/dist/commands/resourcesCommands.js +274 -0
  17. package/dist/commands/sessionCommands.js +104 -0
  18. package/dist/commands/taskActor.js +117 -0
  19. package/dist/commands/taskChangeSetCommands.js +60 -0
  20. package/dist/commands/taskCommands.js +610 -201
  21. package/dist/commands/taskCompletionGate.js +78 -1
  22. package/dist/commands/taskContextCommand.js +24 -6
  23. package/dist/commands/taskInputCommands.js +1 -1
  24. package/dist/commands/taskIntegrationCommands.js +136 -33
  25. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  26. package/dist/commands/taskNextActionCommand.js +85 -0
  27. package/dist/commands/taskOverlapCommands.js +120 -0
  28. package/dist/commands/taskOverviewCommand.js +36 -8
  29. package/dist/commands/telemetryCommands.js +330 -0
  30. package/dist/commands/workflowCommands.js +415 -0
  31. package/dist/config/yuiConfig.js +60 -0
  32. package/dist/controller/clientRuntime.js +42 -1
  33. package/dist/controller/controller.js +413 -61
  34. package/dist/controller/controllerMain.js +25 -2
  35. package/dist/controller/domainIdentity.js +16 -8
  36. package/dist/controller/ephemeralResourceReaper.js +2 -1
  37. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  38. package/dist/controller/handoverCandidate.js +168 -0
  39. package/dist/controller/jobClient.js +102 -0
  40. package/dist/controller/jobControl.js +613 -0
  41. package/dist/controller/jobSupervisor.js +498 -0
  42. package/dist/controller/providerHookRunFence.js +34 -5
  43. package/dist/controller/resourceCleanupLinux.js +18 -9
  44. package/dist/controller/resourceInventoryLinux.js +90 -39
  45. package/dist/controller/resourceInventoryRpc.js +85 -0
  46. package/dist/controller/resourceInventoryWorker.js +50 -0
  47. package/dist/controller/runtime.js +238 -22
  48. package/dist/controller/runtimeEventInbox.js +234 -57
  49. package/dist/controller/runtimeEventProcessor.js +549 -42
  50. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  51. package/dist/core/boundedRpc.js +475 -0
  52. package/dist/core/controllerServer.js +416 -27
  53. package/dist/core/controllerTelemetry.js +167 -0
  54. package/dist/doctor/doctor.js +113 -16
  55. package/dist/domain/validation.js +9 -0
  56. package/dist/execution/executionGroup.js +40 -3
  57. package/dist/executor/agentExecutor.js +6 -3
  58. package/dist/executor/effectiveLaunch.js +52 -0
  59. package/dist/executor/executorRegistry.js +50 -0
  60. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  61. package/dist/grant/capabilityGrant.js +282 -0
  62. package/dist/integration/changeSet.js +16 -3
  63. package/dist/integration/changeSetManifest.js +46 -0
  64. package/dist/integration/gitIntegrationService.js +528 -147
  65. package/dist/integration/integrationAttempt.js +54 -5
  66. package/dist/integration/integrationQueueEntry.js +221 -0
  67. package/dist/integration/integrationQueueService.js +955 -0
  68. package/dist/integration/manifestTags.js +99 -0
  69. package/dist/integration/overlapDiagnostics.js +211 -0
  70. package/dist/job/durableJob.js +449 -0
  71. package/dist/job/jobRunner.js +350 -0
  72. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  73. package/dist/lifecycle/providerErrorClass.js +126 -0
  74. package/dist/message/message.js +16 -3
  75. package/dist/observability/executionAudit.js +545 -0
  76. package/dist/observability/faultClassification.js +160 -0
  77. package/dist/observability/runtimeIdentity.js +367 -0
  78. package/dist/release/fakeReleasePorts.js +55 -0
  79. package/dist/release/releaseHandover.js +475 -0
  80. package/dist/release/releaseIdempotencyStore.js +165 -0
  81. package/dist/release/releaseWorkflow.js +459 -0
  82. package/dist/release/releaseWorkflowEngine.js +688 -0
  83. package/dist/release/releaseWorkflowPorts.js +1720 -0
  84. package/dist/release/runtimeRelease.js +495 -0
  85. package/dist/release/workflowFileLock.js +218 -0
  86. package/dist/repository/gitWorkspace.js +177 -1
  87. package/dist/repository/projectMaintenanceLock.js +315 -0
  88. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  89. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  90. package/dist/resources/autoResourceGc.js +116 -0
  91. package/dist/resources/liveReferences.js +574 -0
  92. package/dist/resources/resourceDiscovery.js +477 -0
  93. package/dist/resources/resourceGc.js +645 -0
  94. package/dist/resources/resourceRegistrar.js +256 -0
  95. package/dist/resources/resourceRegistry.js +150 -0
  96. package/dist/resources/resourceRegistryStore.js +41 -0
  97. package/dist/resources/resourceTypes.js +42 -0
  98. package/dist/resources/sqliteResourceRegistry.js +111 -0
  99. package/dist/review/reviewConfig.js +10 -0
  100. package/dist/review/reviewFinding.js +240 -0
  101. package/dist/review/reviewFindingLedger.js +545 -0
  102. package/dist/review/reviewOutcomeClassifier.js +61 -0
  103. package/dist/review/reviewRound.js +56 -4
  104. package/dist/run/agentRun.js +80 -4
  105. package/dist/run/providerRetry.js +84 -0
  106. package/dist/run/providerRetryConfig.js +63 -0
  107. package/dist/run/yieldReceipt.js +65 -0
  108. package/dist/runtime/exactControlPlane.js +79 -2
  109. package/dist/runtime/index.js +4 -0
  110. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  111. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  112. package/dist/runtime/sessionReconciliation.js +93 -0
  113. package/dist/runtime/sessionTerminationGuard.js +211 -0
  114. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  115. package/dist/runtime/tmuxAdapters.js +34 -1
  116. package/dist/scheduler/actionability.js +155 -0
  117. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  118. package/dist/scheduler/activeTaskProgress.js +60 -0
  119. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  120. package/dist/scheduler/roleRunStall.js +135 -29
  121. package/dist/scheduler/taskExecutionProjection.js +11 -0
  122. package/dist/setup/setupCommand.js +27 -4
  123. package/dist/storage/compatibleTaskStore.js +112 -5
  124. package/dist/storage/migration/productionRegistry.js +769 -1
  125. package/dist/storage/persistenceWorker.js +194 -0
  126. package/dist/storage/sqliteSchema.js +705 -0
  127. package/dist/storage/sqliteStore.js +1695 -0
  128. package/dist/storage/storageVersions.js +9 -2
  129. package/dist/storage/storeRpc.js +298 -0
  130. package/dist/storage/taskStore.js +982 -21
  131. package/dist/storage/upgrade/homeClassification.js +157 -12
  132. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  133. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  134. package/dist/storage/upgrade/recordVersions.js +10 -1
  135. package/dist/storage/upgrade/sqliteMigrationTarget.js +351 -0
  136. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  137. package/dist/storage/upgrade/sqliteStateMigration.js +713 -0
  138. package/dist/storage/upgrade/upgradeOrchestrator.js +510 -18
  139. package/dist/task/deliveryGuard.js +226 -0
  140. package/dist/task/nextAction.js +343 -0
  141. package/dist/task/repairWave.js +137 -0
  142. package/dist/task/taskRecordReference.js +6 -1
  143. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  144. package/dist/telemetry/telemetryCompaction.js +251 -0
  145. package/dist/telemetry/telemetryConfig.js +64 -0
  146. package/dist/telemetry/telemetryRouter.js +32 -0
  147. package/dist/telemetry/telemetryStore.js +19 -0
  148. package/dist/telemetry/telemetryWiring.js +33 -0
  149. package/dist/tmux/tmuxManager.js +20 -1
  150. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  151. package/dist/verification/gateArtifact.js +216 -0
  152. package/dist/verification/gateArtifactStore.js +87 -0
  153. package/dist/verification/verificationGateService.js +414 -0
  154. package/dist/verification/verificationPlan.js +308 -0
  155. package/dist/workspace/gitChangeSetCapture.js +12 -2
  156. package/dist/workspace/workItemChangeSetManager.js +60 -3
  157. package/package.json +2 -1
@@ -7,27 +7,98 @@ import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_M
7
7
  import { hasRuntimeCleanupObligation, runtimeLifecycleSignalKey, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
8
8
  import { agentProcessReadinessProbe, ExecutorRegistry } from "../executor/executorRegistry.js";
9
9
  import { activeLiveRoleAgentSession, roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
10
- import { effectiveLaunchSnapshotsCompatible, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
10
+ import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
11
11
  import { isTaskOwnedWorkspace } from "../worktree/managedWorkspace.js";
12
12
  import { FileRoleLaunchPlanner } from "../executor/fileRoleLaunchPlanner.js";
13
13
  import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
14
+ import { SqliteTaskStore } from "../storage/sqliteStore.js";
15
+ import { AsyncTaskStoreClient, resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
14
16
  import { FileTaskWorkspacePreparer } from "../repository/taskWorkspacePreparer.js";
15
17
  import { NodeCommandExecutor } from "../tmux/commandExecutor.js";
16
18
  import { TmuxManager, yuiTmuxServerName } from "../tmux/tmuxManager.js";
17
19
  import { FileTaskRuntimeIsolation, TmuxPromptPushAdapter, TmuxSessionHost } from "../runtime/index.js";
18
20
  import { startFileTaskController } from "./controller.js";
19
21
  import { FileSchedulerStoreAdapter } from "./fileSchedulerStoreAdapter.js";
22
+ import { openSchedulerTelemetry } from "../telemetry/telemetryWiring.js";
23
+ import { createFileArtifactPort, createLinuxProcessPort, DurableJobSupervisor } from "./jobSupervisor.js";
24
+ import { createDurableJobControl } from "./jobControl.js";
20
25
  import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
21
- import { FileRuntimeEventProcessor } from "./runtimeEventProcessor.js";
26
+ import { AsyncRuntimeEventProcessor, FileRuntimeEventProcessor, createAsyncRuntimeObserver } from "./runtimeEventProcessor.js";
22
27
  import { RuntimeLaunchCoordinator } from "./runtimeLaunchCoordinator.js";
23
28
  import { ephemeralDomainFromEnvironment, recordEphemeralTmuxTarget } from "./domainIdentity.js";
24
29
  import { createEphemeralResourceReaper } from "./ephemeralResourceReaper.js";
25
30
  import { scanControllerResourceInventory } from "./resourceInventoryLinux.js";
31
+ import { ResourceInventoryClient } from "./resourceInventoryRpc.js";
32
+ import { createResourceAutoGc } from "../resources/autoResourceGc.js";
26
33
  import { createRuntimeResourceActivityTracker } from "./resourceInventory.js";
34
+ import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
35
+ /** Refreshes only the exact Task runtime generation folded by the event transaction. */
36
+ export function refreshAppliedTaskRuntimeDescriptor(store, planner, input) {
37
+ if (input.launchId === undefined)
38
+ return;
39
+ const run = input.runId === undefined
40
+ ? null
41
+ : store.getAgentRun(input.taskId, input.runId);
42
+ if (input.runId !== undefined && run === null) {
43
+ throw new Error("Prepared Task runtime generation is not current.");
44
+ }
45
+ // A terminal completion has already settled this exact Run and no later
46
+ // prompt can use its descriptor. Acknowledge the applied provider fact
47
+ // without republishing a dead generation.
48
+ if (run !== null && run.status !== "active")
49
+ return;
50
+ const session = store.getTaskRoleSessionSet(input.taskId, input.roleName)
51
+ ?.sessions[input.agentId];
52
+ const effective = run?.effective ?? session?.effective;
53
+ if (effective === undefined
54
+ || session === undefined
55
+ || session.agentId !== input.agentId
56
+ || session.adapterId !== input.adapterId
57
+ || session.launchId !== input.launchId
58
+ || session.nativeSessionId !== input.nativeSessionId
59
+ || (run !== null && (run.roleName !== input.roleName
60
+ || run.effective.agentId !== input.agentId
61
+ || run.effective.adapterId !== input.adapterId))) {
62
+ throw new Error("Prepared Task runtime generation is not current.");
63
+ }
64
+ planner.refreshTaskRuntimeDescriptor({
65
+ ...input,
66
+ launchId: input.launchId,
67
+ workspace: effective.workspace.root
68
+ });
69
+ }
27
70
  /** Production composition root for the lean FileTaskStore + tmux Controller. */
28
71
  export async function startFileTaskControllerRuntime(home, options = {}) {
29
- const store = options.store ?? openCompatibleFileTaskStore(home);
30
- const schedulerStore = options.schedulerStore ?? new FileSchedulerStoreAdapter(store);
72
+ // The Home decides the backend (Issue 01): a layout-7 Home runs SQLite with
73
+ // the persistence worker on by default; YUI_STORE_WORKER=0/false forces the
74
+ // in-process SQLite connection. The non-worker path opens the Home-decided
75
+ // backend through the compatibility opener (SQLite for layout 7, file store
76
+ // with normalization for older layouts).
77
+ const useWorker = resolveStoreWorkerEnabledForHome(home, options.environment ?? process.env);
78
+ const store = options.store
79
+ ?? (useWorker
80
+ // Transitional: the scheduler/planner still use a sync store. The worker
81
+ // owns the event-processing hot path; the scheduler migration is the next
82
+ // step (see work-item-5 remaining call sites). Both connections point at
83
+ // the same WAL db and serialize via BEGIN IMMEDIATE + busy_timeout.
84
+ ? new SqliteTaskStore(home)
85
+ : openCompatibleFileTaskStore(home));
86
+ // When the worker backend is active, the db-touching observer folds run in
87
+ // the worker (off the main event loop). The client is closed on shutdown.
88
+ const asyncStoreClient = useWorker
89
+ ? new AsyncTaskStoreClient(home, {
90
+ environment: options.environment,
91
+ observerModule: new URL("./fileSchedulerStoreAdapter.js", import.meta.url)
92
+ })
93
+ : undefined;
94
+ // When the worker backend is active, the blocking /proc inventory scan runs
95
+ // in the inventory worker (off the main event loop); the scheduler and the
96
+ // ephemeral reaper consume the same inventory shape through this client (§3.3).
97
+ const inventoryClient = useWorker
98
+ ? new ResourceInventoryClient()
99
+ : undefined;
100
+ const schedulerStore = options.schedulerStore
101
+ ?? new FileSchedulerStoreAdapter(store, openSchedulerTelemetry(home, options.environment ?? process.env));
31
102
  const domainIdentity = options.domainIdentity
32
103
  ?? ephemeralDomainFromEnvironment(options.environment ?? process.env);
33
104
  const planner = options.planner ?? new FileRoleLaunchPlanner(home, store, {
@@ -45,7 +116,27 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
45
116
  }
46
117
  })
47
118
  });
48
- const sessionHost = options.sessionHost ?? new TmuxSessionHost(planner, tmux);
119
+ const sessionOwners = new SessionOwnerReconciliation({
120
+ home,
121
+ store,
122
+ environment: options.environment,
123
+ tmux,
124
+ onWarning: options.onError
125
+ });
126
+ const sessionHost = options.sessionHost ?? new TmuxSessionHost(planner, tmux, {
127
+ onHostCreated: ({ binding, pane }) => {
128
+ sessionOwners.recordHostOwner({
129
+ owner: binding.owner,
130
+ agentId: binding.agentId,
131
+ adapterId: binding.adapterId,
132
+ launchId: binding.launchId,
133
+ ...(binding.nativeSessionId === undefined
134
+ ? {}
135
+ : { nativeSessionId: binding.nativeSessionId }),
136
+ ...(pane.pid === undefined ? {} : { panePid: pane.pid })
137
+ });
138
+ }
139
+ });
49
140
  const promptPush = options.promptPush
50
141
  ?? new TmuxPromptPushAdapter(tmux, agentProcessReadinessProbe);
51
142
  const runtimeIsolation = options.runtimeIsolation
@@ -67,7 +158,19 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
67
158
  : {
68
159
  inspectOwners: (owners) => sessionHost.inspectOwners(owners)
69
160
  }),
70
- stopOwner: (owner) => (sessionHost.stopOwner(owner)),
161
+ stopOwner: (owner) => {
162
+ // Issue 03: the durable `stopped` transition is gated on physical
163
+ // exit proof. A blocked result keeps the Session non-terminal and
164
+ // preserves owner records for Operator recovery.
165
+ return sessionOwners.terminateOwner(owner).then((result) => {
166
+ if (result.outcome === "stop-blocked") {
167
+ (options.onError ?? (() => undefined))(new Error(`Role runtime cleanup could not prove physical exit: ${result.remaining
168
+ .map(({ record, detail }) => `${record.launchId}: ${detail}`)
169
+ .join("; ")}`));
170
+ }
171
+ return result.outcome === "stop-confirmed";
172
+ });
173
+ },
71
174
  ...(runtimeIsolation.cleanupTaskLaunch === undefined
72
175
  ? {}
73
176
  : {
@@ -95,19 +198,32 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
95
198
  onCleanupRequired: signalRuntimeCleanup,
96
199
  runtimeIsolation
97
200
  });
201
+ // One inventory scan per scheduler pass. When the worker backend is active
202
+ // the blocking /proc scan runs in the inventory worker; otherwise it runs on
203
+ // the main thread (file backend, unchanged).
204
+ const scanInventory = (panes) => inventoryClient !== undefined
205
+ ? inventoryClient.scan({
206
+ currentHome: home,
207
+ scope: "current",
208
+ panes,
209
+ ...(options.environment === undefined
210
+ ? {}
211
+ : { environment: options.environment })
212
+ })
213
+ : scanControllerResourceInventory({
214
+ currentHome: home,
215
+ scope: "current",
216
+ panes,
217
+ ...(options.environment === undefined
218
+ ? {}
219
+ : { environment: options.environment })
220
+ });
98
221
  const delivery = options.delivery ?? new ExecutorRegistry(planner, tmux, agentProcessReadinessProbe, {
99
222
  sessionHost,
100
223
  promptPush,
101
224
  launchCoordinator,
102
225
  roleResourceInventory: async (panes, inputs) => {
103
- const inventory = await scanControllerResourceInventory({
104
- currentHome: home,
105
- scope: "current",
106
- panes,
107
- ...(options.environment === undefined
108
- ? {}
109
- : { environment: options.environment })
110
- });
226
+ const inventory = await scanInventory(panes);
111
227
  return inventory.resources.flatMap((resource) => {
112
228
  if (resource.kind !== "agent-session")
113
229
  return [];
@@ -182,9 +298,76 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
182
298
  // recovery bounded to that domain; cross-home cleanup remains an
183
299
  // explicit `controller cleanup --all` inventory operation.
184
300
  scope: "current",
185
- environment: options.environment
301
+ environment: options.environment,
302
+ // When the worker backend is active, the reaper's scan runs in the
303
+ // inventory worker too (same cadence, same inventory shape).
304
+ ...(inventoryClient === undefined
305
+ ? {}
306
+ : {
307
+ scan: () => inventoryClient.scan({
308
+ currentHome: home,
309
+ scope: "current",
310
+ ...(options.environment === undefined
311
+ ? {}
312
+ : { environment: options.environment })
313
+ })
314
+ })
186
315
  }));
316
+ // Issue 10: automatic Resource GC. The runner self-skips unless
317
+ // resourcesGcMode=quarantine and resourcesGcAutoQuarantine=true, so wiring
318
+ // it unconditionally costs one config read per full pass when disabled.
319
+ const resourceAutoGc = options.resourceAutoGc
320
+ ?? createResourceAutoGc({
321
+ home,
322
+ store,
323
+ environment: options.environment
324
+ });
187
325
  const lifecycleDispatcher = createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, options.dispatcher, signalRuntimeCleanup, launchCoordinator, planner);
326
+ // f7/rr5: Share one inbox between the supervisor's terminal channel and
327
+ // the runtime event processor. When a Job reaches a terminal state, the
328
+ // supervisor enqueues a durable-job-terminal event; the processor drains
329
+ // it on the next pass, waking the Controller immediately instead of
330
+ // waiting for the poll interval.
331
+ const runtimeEventInbox = new FileRuntimeEventInbox(home);
332
+ const jobSupervisor = new DurableJobSupervisor({
333
+ store: schedulerStore,
334
+ process: createLinuxProcessPort(),
335
+ artifacts: createFileArtifactPort(home),
336
+ // rr6/f1: Bounded supervision wake. The supervisor signals the Controller
337
+ // after spawning a runner (queued→running adoption) and when a runner
338
+ // exits (terminal harvest), so a quick job converges without waiting for
339
+ // the recovery interval. Closes over runningRuntime, which is assigned
340
+ // once startFileTaskController resolves; a wake during shutdown is a
341
+ // no-op. The recovery interval stays the cross-restart fallback.
342
+ wake: (taskId) => {
343
+ try {
344
+ runningRuntime?.signal(`task:${taskId}`);
345
+ }
346
+ catch {
347
+ // Controller stopped; the recovery interval remains the fallback.
348
+ }
349
+ },
350
+ terminalEvents: {
351
+ deliverTerminalEvent(notice) {
352
+ try {
353
+ runtimeEventInbox.enqueueDurableJobTerminal({
354
+ scope: "task",
355
+ taskId: notice.taskId,
356
+ jobId: notice.jobId,
357
+ status: notice.status,
358
+ outcome: notice.outcome
359
+ });
360
+ }
361
+ catch (error) {
362
+ // Best-effort terminal channel: the terminal transition already
363
+ // committed. A delivery failure must not fail the reconcile pass.
364
+ (options.onError ?? (() => undefined))(error);
365
+ }
366
+ }
367
+ },
368
+ onError: options.onError
369
+ });
370
+ const jobControl = createDurableJobControl(store);
188
371
  const running = await startFileTaskController(home, schedulerStore, delivery, lifecycleDispatcher, {
189
372
  intervalMs: options.intervalMs
190
373
  ?? reconciliationIntervalMilliseconds(store.getConfig().reconciliationIntervalSeconds),
@@ -194,7 +377,10 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
194
377
  now: options.now,
195
378
  onError: options.onError,
196
379
  lifecycleHost,
380
+ jobSupervisor,
381
+ jobControl,
197
382
  ...(resourceReaper === undefined ? {} : { resourceReaper }),
383
+ resourceAutoGc,
198
384
  onExpiredEphemeralDomain: (domain) => {
199
385
  if (domain.yuiHome !== home)
200
386
  return;
@@ -202,11 +388,17 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
202
388
  },
203
389
  workspacePreparer,
204
390
  runtimeEventProcessor: options.runtimeEventProcessor
205
- ?? new FileRuntimeEventProcessor(new FileRuntimeEventInbox(home), schedulerStore, {
206
- onTaskRuntimeApplied: (input) => {
207
- planner.refreshTaskRuntimeDescriptor(input);
208
- }
209
- }),
391
+ ?? (useWorker && asyncStoreClient !== undefined
392
+ ? new AsyncRuntimeEventProcessor(runtimeEventInbox, createAsyncRuntimeObserver((method, args) => asyncStoreClient.invokeObserver(method, args)), {
393
+ onTaskRuntimeApplied: (input) => {
394
+ refreshAppliedTaskRuntimeDescriptor(store, planner, input);
395
+ }
396
+ })
397
+ : new FileRuntimeEventProcessor(runtimeEventInbox, schedulerStore, {
398
+ onTaskRuntimeApplied: (input) => {
399
+ refreshAppliedTaskRuntimeDescriptor(store, planner, input);
400
+ }
401
+ })),
210
402
  domainIdentity,
211
403
  ...(options.configuration !== undefined
212
404
  ? { configuration: options.configuration }
@@ -220,8 +412,29 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
220
412
  });
221
413
  runningController = running;
222
414
  runningRuntime = running.runtime;
415
+ // Issue 03: read-only startup reconciliation. Surfaces durable/physical
416
+ // Session mismatches (including generations whose durable map was cleared)
417
+ // without changing stop or archive behavior. Cleanup stays an explicit
418
+ // Operator action in exact-owner-cleanup mode.
419
+ try {
420
+ const startupReport = sessionOwners.report();
421
+ if (startupReport.summary.livePhysicalRoots > 0) {
422
+ (options.onError ?? (() => undefined))(new Error(`Session reconciliation: ${startupReport.summary.livePhysicalRoots} `
423
+ + `live physical root(s) across ${startupReport.summary.owners} owner record(s); `
424
+ + "run `yui session reconcile --report` for details."));
425
+ }
426
+ }
427
+ catch (error) {
428
+ (options.onError ?? (() => undefined))(error);
429
+ }
223
430
  return {
224
431
  ...running,
432
+ close: async () => {
433
+ await running.close();
434
+ // Release the worker's database connections when the worker backend is active.
435
+ await asyncStoreClient?.close();
436
+ await inventoryClient?.close();
437
+ },
225
438
  store,
226
439
  schedulerStore,
227
440
  planner,
@@ -325,7 +538,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
325
538
  throw applicationError("INVALID_PARAMS", `Configured Agent does not match Role: ${effective.agentId}.`);
326
539
  }
327
540
  validateLifecycleEnvironment(request.environment, agent);
328
- const mode = roleAgentSessionResumeMode(sessions, effective.agentId, effective);
541
+ const mode = roleAgentSessionResumeMode(sessions, effective.agentId, effective, managedWorkspace);
329
542
  const session = sessions?.sessions[effective.agentId];
330
543
  const owner = request.scope === "task"
331
544
  ? { scope: "task", taskId: request.taskId, roleName: request.roleName }
@@ -447,7 +660,10 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
447
660
  || session.nativeSessionId !== request.nativeSessionId) {
448
661
  throw new Error(`Native session changed: ${request.owner.roleName}.`);
449
662
  }
450
- if (!effectiveLaunchSnapshotsCompatible(session.effective, request.effective)) {
663
+ const sessionEffectiveCompatible = request.owner.scope === "task"
664
+ ? effectiveLaunchSnapshotsCompatibleForTaskMain(session.effective, request.effective, request.managedWorkspace)
665
+ : effectiveLaunchSnapshotsCompatible(session.effective, request.effective);
666
+ if (!sessionEffectiveCompatible) {
451
667
  throw new Error(`Native session effective launch changed: ${request.owner.roleName}.`);
452
668
  }
453
669
  }