@zq-silk/yui 2.21.0 → 2.23.0

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 (145) hide show
  1. package/README.md +5 -0
  2. package/dist/cli/commandCatalog.js +87 -27
  3. package/dist/cli/dynamicCompletion.js +4 -1
  4. package/dist/cli/helpRenderer.js +12 -1
  5. package/dist/cli/interactionPolicy.js +2 -0
  6. package/dist/cli/invocationRouter.js +5 -5
  7. package/dist/cli/roleWizard.js +5 -1
  8. package/dist/cli/updateCommand.js +20 -3
  9. package/dist/cli/updateOrchestrator.js +13 -0
  10. package/dist/cli/updatePorts.js +27 -0
  11. package/dist/commands/globalRoleCommands.js +33 -8
  12. package/dist/commands/inboxCommands.js +39 -12
  13. package/dist/commands/operatorCommands.js +2 -5
  14. package/dist/commands/releaseCommands.js +16 -2
  15. package/dist/commands/roleConfiguration.js +25 -2
  16. package/dist/commands/sessionCommands.js +5 -2
  17. package/dist/commands/taskCommands.js +217 -64
  18. package/dist/commands/taskContextCommand.js +26 -4
  19. package/dist/commands/taskFactCommands.js +15 -2
  20. package/dist/commands/taskIntegrationCommands.js +3 -3
  21. package/dist/commands/taskRoleRuntimeStatus.js +19 -5
  22. package/dist/commands/textInput.js +2 -2
  23. package/dist/context/contextFamilies.js +10 -0
  24. package/dist/context/currentSkills.js +37 -4
  25. package/dist/context/runContextPack.js +44 -5
  26. package/dist/context/runInputContract.js +1 -1
  27. package/dist/context/sessionBootstrapManifest.js +21 -6
  28. package/dist/context/sessionSkillNotice.js +74 -0
  29. package/dist/context/taskContext.js +100 -16
  30. package/dist/controlPlaneCli.js +17 -7
  31. package/dist/controller/agentRuntimeObserver.js +57 -12
  32. package/dist/controller/clientRuntime.js +66 -18
  33. package/dist/controller/controller.js +19 -2
  34. package/dist/controller/diagnosticJournal.js +10 -2
  35. package/dist/controller/fileSchedulerStoreAdapter.js +74 -24
  36. package/dist/controller/hostLifecycle.js +153 -0
  37. package/dist/controller/hostLifecycleChecks.js +27 -0
  38. package/dist/controller/hostUpgrade.js +344 -0
  39. package/dist/controller/messageHealth.js +4 -3
  40. package/dist/controller/providerRetryAdmission.js +15 -1
  41. package/dist/controller/providerRetryDelivery.js +18 -2
  42. package/dist/controller/runtime.js +82 -21
  43. package/dist/controller/runtimeEventInbox.js +107 -13
  44. package/dist/controller/runtimeEventProcessor.js +3 -3
  45. package/dist/controller/sessionNotify.js +5 -46
  46. package/dist/controller/taskRunRecovery.js +153 -0
  47. package/dist/core/controllerClient.js +16 -2
  48. package/dist/core/controllerServer.js +14 -5
  49. package/dist/core/controllerTelemetry.js +51 -5
  50. package/dist/errors/cliFailure.js +14 -5
  51. package/dist/execution/codexThreadNaming.js +54 -43
  52. package/dist/executor/agentAdapter.js +32 -4
  53. package/dist/executor/agentConfigurationFields.js +2 -0
  54. package/dist/executor/fileRoleLaunchPlanner.js +18 -3
  55. package/dist/integration/gitIntegrationService.js +3 -2
  56. package/dist/integration/integrationSourceApplication.js +12 -1
  57. package/dist/interaction/notificationPresentation.js +11 -16
  58. package/dist/interaction/operatorPresentation.js +1 -1
  59. package/dist/message/messageContinuation.js +3 -1
  60. package/dist/output/boundedRead.js +36 -7
  61. package/dist/output/rolePresentation.js +3 -1
  62. package/dist/runtime/agentEndpoint.js +11 -1
  63. package/dist/runtime/agentEndpointIdentity.js +2 -0
  64. package/dist/runtime/agentHost.js +174 -40
  65. package/dist/runtime/agentHostImplementation.js +3 -1
  66. package/dist/runtime/codexAppServerRuntime.js +113 -24
  67. package/dist/runtime/codexInteractiveHost.js +102 -11
  68. package/dist/runtime/codexNativeAccess.js +1 -1
  69. package/dist/runtime/contextRecovery.js +15 -0
  70. package/dist/runtime/hostSessionLifecycle.js +83 -0
  71. package/dist/runtime/hostUpgradeGate.js +43 -0
  72. package/dist/runtime/lifecycleChecks.js +47 -0
  73. package/dist/runtime/lifecycleReservation.js +9 -1
  74. package/dist/runtime/nativeTitleReceipt.js +31 -0
  75. package/dist/runtime/nativeTitleSync.js +192 -0
  76. package/dist/runtime/providerRetry.js +9 -6
  77. package/dist/runtime/providerRuntimeIdentity.js +1 -1
  78. package/dist/runtime/structuredProviderHost.js +65 -35
  79. package/dist/scheduler/wakeupQueue.js +10 -4
  80. package/dist/storage/sqliteSchema.js +29 -0
  81. package/dist/storage/storageVersions.js +1 -1
  82. package/dist/tmux/tmuxManager.js +4 -2
  83. package/dist/web/webSettings.js +2 -0
  84. package/docs/authorization.md +18 -3
  85. package/docs/cli-information-contract.md +228 -6
  86. package/docs/cli-information-contract.zh-CN.md +95 -5
  87. package/docs/managed-turn-and-session-runtime.md +177 -21
  88. package/docs/managed-turn-and-session-runtime.zh-CN.md +126 -14
  89. package/docs/provider-retry.md +11 -0
  90. package/docs/release-workflow.md +65 -0
  91. package/docs/release-workflow.zh-CN.md +48 -0
  92. package/docs/roles-and-configuration.md +28 -0
  93. package/docs/roles-and-configuration.zh-CN.md +21 -0
  94. package/docs/storage-baseline.md +13 -1
  95. package/docs/storage-baseline.zh-CN.md +5 -1
  96. package/docs/task-delivery.md +7 -0
  97. package/docs/task-delivery.zh-CN.md +5 -0
  98. package/docs/testing/review-evaluation.md +7 -7
  99. package/docs/testing/review-evaluation.zh-CN.md +7 -7
  100. package/docs/testing/verification-levels.md +45 -2
  101. package/docs/testing/verification-levels.zh-CN.md +21 -2
  102. package/i18n/README.zh-CN.md +6 -0
  103. package/package.json +1 -1
  104. package/skills/yui-leader/SKILL.md +98 -193
  105. package/skills/yui-leader/VERSION +1 -1
  106. package/skills/yui-leader/references/artifacts.md +41 -0
  107. package/skills/yui-leader/references/assignment-execution.md +140 -0
  108. package/skills/yui-leader/references/completion.md +79 -0
  109. package/skills/yui-leader/references/delivery-scope.md +26 -0
  110. package/skills/yui-leader/references/execution.md +16 -487
  111. package/skills/yui-leader/references/implementation.md +81 -0
  112. package/skills/yui-leader/references/planning.md +7 -45
  113. package/skills/yui-leader/references/review.md +90 -0
  114. package/skills/yui-leader/references/task-progress.md +23 -0
  115. package/skills/yui-leader/references/work-allocation.md +118 -0
  116. package/skills/yui-operator/SKILL.md +77 -333
  117. package/skills/yui-operator/VERSION +1 -1
  118. package/skills/yui-operator/references/body-transport.md +62 -0
  119. package/skills/yui-operator/references/configuration.md +47 -0
  120. package/skills/yui-operator/references/lifecycle.md +41 -0
  121. package/skills/yui-operator/references/progress.md +39 -0
  122. package/skills/yui-operator/references/routing.md +57 -0
  123. package/skills/yui-operator/references/submission.md +56 -0
  124. package/skills/yui-operator/references/task-delivery.md +1 -1
  125. package/skills/yui-operator/references/web-lifecycle.md +19 -0
  126. package/skills/yui-operator/scripts/verify-submission.mjs +37 -0
  127. package/skills/yui-runtime/SKILL.md +182 -531
  128. package/skills/yui-runtime/VERSION +1 -1
  129. package/skills/yui-runtime/references/archive.md +51 -0
  130. package/skills/yui-runtime/references/command-discovery.md +41 -0
  131. package/skills/yui-runtime/references/completed-followup.md +65 -0
  132. package/skills/yui-runtime/references/context-ordering.md +3 -34
  133. package/skills/yui-runtime/references/context-reads.md +61 -0
  134. package/skills/yui-runtime/references/dispatched-run.md +61 -0
  135. package/skills/yui-runtime/references/execution-failure.md +105 -0
  136. package/skills/yui-runtime/references/global-context.md +18 -0
  137. package/skills/yui-runtime/references/global-input-controls.md +44 -0
  138. package/skills/yui-runtime/references/host-custody.md +30 -0
  139. package/skills/yui-runtime/references/inbox-recovery.md +77 -0
  140. package/skills/yui-runtime/references/incremental-context.md +40 -0
  141. package/skills/yui-runtime/references/publication.md +4 -111
  142. package/skills/yui-runtime/references/recovery.md +32 -192
  143. package/skills/yui-runtime/references/session-identity.md +18 -0
  144. package/skills/yui-runtime/references/session-replacement.md +45 -0
  145. package/skills/yui-runtime/references/uncertain-execution.md +29 -0
@@ -43,7 +43,7 @@ import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
43
43
  import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_HOST_DETACH_REQUIRED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeCleanupReason, runtimeCleanupDisposition, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
44
44
  import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
45
45
  import { providerContinuationKey } from "../runtime/providerContinuation.js";
46
- import { acceptProviderTurn, beginProviderTurn, cancelQuiescentProviderInput, clearProviderGoal, createProviderRuntimeBinding, currentProviderConversation, managedProviderTurnId, settleProviderTurn, settleProviderTurnSubmission, supersedeProviderConversation, transferProviderAuthority, updateProviderConversationRecoverability, updateProviderGoal } from "../runtime/providerRuntimeIdentity.js";
46
+ import { acceptProviderTurn, beginProviderTurn, cancelQuiescentProviderInput, clearProviderGoal, createProviderRuntimeBinding, currentProviderConversation, managedProviderTurnId, providerTurnIsActive, settleProviderTurn, settleProviderTurnSubmission, supersedeProviderConversation, transferProviderAuthority, updateProviderConversationRecoverability, updateProviderGoal } from "../runtime/providerRuntimeIdentity.js";
47
47
  import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
48
48
  import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation, isRuntimeUsageEvidence, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent, runtimeObservationRunFenceMatches, runtimeObservationTaskEventPayload } from "../runtime/runtimeObservation.js";
49
49
  import { recordLeaderFailure } from "../scheduler/leaderFailure.js";
@@ -106,11 +106,24 @@ export class FileSchedulerStoreAdapter {
106
106
  return errors;
107
107
  }
108
108
  observeAgentHostObservation(event, now = new Date()) {
109
+ let preparation;
110
+ if (isRunTerminalObservation(event.observation)) {
111
+ preparation = {};
112
+ try {
113
+ preparation = this.prepareRuntimeTerminalObservation(resolveAgentHostObservation(this.store, event), now);
114
+ }
115
+ catch (error) {
116
+ if (!(error instanceof AgentHostObservationDeferred || error instanceof RuntimeHookRunFenceError))
117
+ throw error;
118
+ // Re-resolve under the committing fence below; preparation grants no
119
+ // authority and must not settle a deferred/obsolete Host envelope.
120
+ }
121
+ }
109
122
  // Match canonical ingress: archive and Host fact settlement share the
110
123
  // same write boundary, including the original transport envelope.
111
- return this.store.transaction(() => this.foldAgentHostObservation(event, now));
124
+ return this.store.transaction(() => this.foldAgentHostObservation(event, now, preparation));
112
125
  }
113
- foldAgentHostObservation(event, now) {
126
+ foldAgentHostObservation(event, now, preparation) {
114
127
  const raw = createRuntimeObservation(event.observation);
115
128
  const task = raw.fence.taskId === undefined ? null : this.store.getTask(raw.fence.taskId);
116
129
  if (raw.fence.taskId !== undefined && task === null)
@@ -154,7 +167,7 @@ export class FileSchedulerStoreAdapter {
154
167
  // Rejection is durable diagnostic evidence, never business acceptance.
155
168
  return this.recordObsoleteCanonicalObservation(event.observation, error.message, now);
156
169
  }
157
- return this.observeRuntimeObservation(input, now);
170
+ return this.foldRuntimeObservation(input, now, preparation);
158
171
  }
159
172
  /**
160
173
  * Sole provider-independent ingress for structured runtime state. Driver
@@ -163,12 +176,30 @@ export class FileSchedulerStoreAdapter {
163
176
  * transitions, and retains the canonical observation for status projection.
164
177
  */
165
178
  observeRuntimeObservation(raw, now = new Date()) {
179
+ const preparation = this.prepareRuntimeTerminalObservation(raw, now);
166
180
  // Archive and observation settlement share the same write boundary.
167
181
  // An ingress read outside this transaction must never authorize clearing
168
182
  // a claim after another CLI has committed archive.
169
- return this.store.transaction(() => this.foldRuntimeObservation(raw, now));
183
+ return this.store.transaction(() => this.foldRuntimeObservation(raw, now, preparation));
170
184
  }
171
- foldRuntimeObservation(raw, now) {
185
+ prepareRuntimeTerminalObservation(raw, now) {
186
+ const input = createRuntimeObservation(raw);
187
+ if (!isRunTerminalObservation(input))
188
+ return undefined;
189
+ if (input.fence.taskId === undefined || Date.parse(input.receivedAt) > now.getTime())
190
+ return {};
191
+ const adapterId = this.adapterForRuntimeObservation(input);
192
+ if (adapterId === null)
193
+ return {};
194
+ return this.prepareRuntimeTerminal({
195
+ taskId: input.fence.taskId, roleName: input.fence.roleName,
196
+ agentId: input.fence.agentId, adapterId, conversationId: input.fence.conversationId,
197
+ nativeSessionId: input.fence.nativeSessionId, nativeTurnId: input.fence.nativeTurnId,
198
+ attemptId: input.fence.receiptId, runId: input.fence.runId
199
+ }, input.kind === "turn.completed" && input.payload.resultTransportDiagnostic === undefined
200
+ && transportAgentResult(input.payload.output).status === "completed");
201
+ }
202
+ foldRuntimeObservation(raw, now, preparation) {
172
203
  const input = createRuntimeObservation(raw);
173
204
  const taskId = input.fence.taskId;
174
205
  if (taskId === undefined)
@@ -263,13 +294,13 @@ export class FileSchedulerStoreAdapter {
263
294
  outcome = this.observeRuntimePromptAccepted(input, adapterId, now);
264
295
  break;
265
296
  case "turn.completed":
266
- outcome = this.foldProviderTurnBoundary(input, adapterId, "completed", now);
297
+ outcome = this.foldProviderTurnBoundary(input, adapterId, "completed", now, preparation);
267
298
  break;
268
299
  case "turn.failed": {
269
300
  // A Provider Turn is an activation boundary, not a Task outcome.
270
301
  // Keep the AgentRun and Session identity available for Agent-directed
271
302
  // restore + submit, even when a provider marks its AgentRun terminal.
272
- outcome = this.foldProviderTurnBoundary(input, adapterId, "failed", now);
303
+ outcome = this.foldProviderTurnBoundary(input, adapterId, "failed", now, preparation);
273
304
  break;
274
305
  }
275
306
  case "operation.started":
@@ -286,7 +317,7 @@ export class FileSchedulerStoreAdapter {
286
317
  outcome = this.validateCanonicalRunObservation(input, now);
287
318
  break;
288
319
  case "turn.cancelled": {
289
- outcome = this.foldProviderTurnBoundary(input, adapterId, "cancelled", now);
320
+ outcome = this.foldProviderTurnBoundary(input, adapterId, "cancelled", now, preparation);
290
321
  break;
291
322
  }
292
323
  case "conversation.observed":
@@ -617,7 +648,7 @@ export class FileSchedulerStoreAdapter {
617
648
  return "applied";
618
649
  });
619
650
  }
620
- foldProviderTurnBoundary(input, adapterId, providerStatus, now) {
651
+ foldProviderTurnBoundary(input, adapterId, providerStatus, now, preparation) {
621
652
  const outcome = providerStatus === "completed"
622
653
  ? input.payload.resultTransportDiagnostic === undefined
623
654
  ? transportAgentResult(input.payload.output)
@@ -656,7 +687,7 @@ export class FileSchedulerStoreAdapter {
656
687
  const classification = this.classifyRuntimeRunTerminal(completed);
657
688
  if (classification !== "apply")
658
689
  return classification;
659
- const result = this.observeRuntimeRunTerminal(completed, now);
690
+ const result = this.foldRuntimeRunTerminal(completed, now, preparation ?? {});
660
691
  return result.disposition === "obsolete" ? "obsolete" : "applied";
661
692
  }
662
693
  validateCanonicalSessionObservation(input, now) {
@@ -1692,6 +1723,8 @@ export class FileSchedulerStoreAdapter {
1692
1723
  const disposition = runtimeCleanupDisposition(mailbox);
1693
1724
  if (mailbox === null || disposition === null)
1694
1725
  return false;
1726
+ if (disposition === "upgrade-host")
1727
+ return false;
1695
1728
  if (mailbox.processing !== null) {
1696
1729
  if (disposition !== "replace-session" && !mailbox.processing.batch.reasons.every(isRuntimeCleanupReason)) {
1697
1730
  return false;
@@ -1774,8 +1807,7 @@ export class FileSchedulerStoreAdapter {
1774
1807
  listDormantRuntimeOwners() {
1775
1808
  return this.listRuntimeSessionCandidates().flatMap((candidate) => {
1776
1809
  if (hasRuntimeLifecycleWork(this.store.getWorkMailbox(runtimeLifecycleTarget(candidate.owner)))
1777
- || (candidate.owner.scope === "task"
1778
- && this.store.getActiveRun(candidate.owner.taskId, candidate.owner.roleName) !== null)) {
1810
+ || runtimeOwnerHasActiveWork(this.store, candidate.owner)) {
1779
1811
  return [];
1780
1812
  }
1781
1813
  return [{
@@ -2282,17 +2314,24 @@ export class FileSchedulerStoreAdapter {
2282
2314
  classifyRuntimeRunTerminal(input) {
2283
2315
  return resolveTerminalExecution(this.store, input) === null ? "obsolete" : "apply";
2284
2316
  }
2317
+ prepareRuntimeTerminal(input, completed) {
2318
+ const run = resolveTerminalExecution(this.store, input)?.run;
2319
+ const workspace = run?.status === "active"
2320
+ && run.executionGroupId !== undefined
2321
+ && run.executionLaneId !== undefined
2322
+ && run.workspace !== undefined
2323
+ && completed
2324
+ ? this.#observeRead("git-workspace-snapshot", run.taskId, () => this.snapshotExecutionLaneWorkspace(this.store, run.workspace))
2325
+ : undefined;
2326
+ return { run, workspace };
2327
+ }
2285
2328
  observeRuntimeRunTerminal(input, now = new Date()) {
2329
+ return this.foldRuntimeRunTerminal(input, now, this.prepareRuntimeTerminal(input, input.outcome.status === "completed"));
2330
+ }
2331
+ foldRuntimeRunTerminal(input, now, preparation) {
2286
2332
  // Resource inspection is bounded but external to SQLite. Revalidate the
2287
2333
  // immutable AgentRun workspace after acquiring the write transaction.
2288
- const preparedRun = resolveTerminalExecution(this.store, input)?.run;
2289
- const workspace = preparedRun?.status === "active"
2290
- && preparedRun.executionGroupId !== undefined
2291
- && preparedRun.executionLaneId !== undefined
2292
- && preparedRun.workspace !== undefined
2293
- && input.outcome.status === "completed"
2294
- ? this.#observeRead("git-workspace-snapshot", preparedRun.taskId, () => this.snapshotExecutionLaneWorkspace(this.store, preparedRun.workspace))
2295
- : undefined;
2334
+ const { run: preparedRun, workspace } = preparation;
2296
2335
  return this.store.transaction((store) => {
2297
2336
  const task = store.getTask(input.taskId);
2298
2337
  if (task === null)
@@ -2304,7 +2343,8 @@ export class FileSchedulerStoreAdapter {
2304
2343
  if (resolved === null)
2305
2344
  throw new Error("Runtime terminal has no exact execution binding.");
2306
2345
  const { run: observedRun, current: recordedProviderTurn, attemptId } = resolved;
2307
- if (!isDeepStrictEqual(preparedRun?.workspace, observedRun?.workspace)) {
2346
+ if (preparedRun?.id !== observedRun?.id
2347
+ || !isDeepStrictEqual(preparedRun?.workspace, observedRun?.workspace)) {
2308
2348
  throw new Error("Runtime terminal workspace changed during result preparation.");
2309
2349
  }
2310
2350
  if (workspace !== undefined && observedRun?.workspace !== undefined
@@ -3132,8 +3172,7 @@ function sameRuntimeOwner(left, right) {
3132
3172
  }
3133
3173
  function dormantRuntimeCandidateIsCurrent(store, candidate, allowActiveRun = false) {
3134
3174
  const { owner } = candidate;
3135
- if (!allowActiveRun && owner.scope === "task"
3136
- && store.getActiveRun(owner.taskId, owner.roleName) !== null) {
3175
+ if (!allowActiveRun && runtimeOwnerHasActiveWork(store, owner)) {
3137
3176
  return false;
3138
3177
  }
3139
3178
  const sessions = runtimeOwnerSessionSet(store, owner);
@@ -3143,6 +3182,17 @@ function dormantRuntimeCandidateIsCurrent(store, candidate, allowActiveRun = fal
3143
3182
  return active !== undefined && active.agentId === candidate.agentId && active.adapterId === candidate.adapterId
3144
3183
  && active.nativeSessionId === candidate.nativeSessionId && (allowActiveRun || active.updatedAt === candidate.sessionUpdatedAt);
3145
3184
  }
3185
+ function runtimeOwnerHasActiveWork(store, owner) {
3186
+ if (owner.scope === "task")
3187
+ return store.getActiveRun(owner.taskId, owner.roleName) !== null;
3188
+ // Global Turns do not have Task AgentRun records. Use their existing native
3189
+ // delivery binding, including submitting and uncertain acceptance.
3190
+ const sessions = store.getGlobalRoleSessionSet(owner.roleName);
3191
+ const binding = sessions?.providerBinding;
3192
+ return binding != null && (providerTurnIsActive(binding.run)
3193
+ || currentProviderConversation(binding).conversationId
3194
+ !== sessions?.sessions[sessions.activeAgentId]?.nativeSessionId);
3195
+ }
3146
3196
  function endRuntimeOwnerSession(store, owner, now) {
3147
3197
  if (owner.scope === "task") {
3148
3198
  const sessions = store.getTaskRoleSessionSet(owner.taskId, owner.roleName);
@@ -0,0 +1,153 @@
1
+ import { createRuntimeBinding } from "../runtime/runtimeBinding.js";
2
+ import { normalizeRuntimeOwner } from "../runtime/runtimeOwner.js";
3
+ import { RuntimeHostContentionError } from "../runtime/ports.js";
4
+ const states = ["unavailable", "stopped", "starting", "running", "stopping"];
5
+ export const CONTROLLER_HOST_TRANSITIONS = {
6
+ launch: { from: ["unavailable", "stopped", "running"], to: "starting" },
7
+ launched: { from: ["starting"], to: "running" },
8
+ stop: { from: states, to: "stopping" },
9
+ "exit-sent": { from: ["stopping"], to: "stopping" },
10
+ stopped: { from: ["stopping"], to: "stopped" },
11
+ failed: { from: ["starting", "stopping"], to: "unavailable" },
12
+ observed: { from: states, to: (current, observed) => current === "stopping" && (observed.state === "running" || observed.state === "starting")
13
+ ? "stopping" : observed.state }
14
+ };
15
+ /** All physical effects still require their existing durable caller authority.
16
+ * This process-local FSM gates competing effects at the shared Host port.
17
+ * Recovery observes current custody; it never invents a launch or replays one.
18
+ * Observation revisions reject late reads, not external process identities:
19
+ * exact OS/native identities remain owned by SessionOwner/Host fences. */
20
+ export function createControllerHostLifecycle(raw, hooks = {}) {
21
+ const entries = new Map();
22
+ let nextOperation = 0;
23
+ const entryFor = (input) => {
24
+ const owner = normalizeRuntimeOwner(input);
25
+ const key = JSON.stringify(owner);
26
+ let entry = entries.get(key);
27
+ if (!entry) {
28
+ entry = { owner: Object.freeze(owner), state: "unavailable", revision: 0 };
29
+ entries.set(key, entry);
30
+ }
31
+ return entry;
32
+ };
33
+ const send = (entry, event, observed, binding) => {
34
+ const rule = CONTROLLER_HOST_TRANSITIONS[event];
35
+ if (!rule.from.includes(entry.state))
36
+ throw new RuntimeHostContentionError("previous-process", `Host lifecycle cannot ${event} from ${entry.state}.`);
37
+ const to = typeof rule.to === "function" ? rule.to(entry.state, observed) : rule.to;
38
+ const nativeSessionId = to === "stopped" ? undefined
39
+ : binding?.nativeSessionId ?? observed?.nativeSessionId ?? entry.nativeSessionId;
40
+ const bindingId = to === "stopped" ? undefined
41
+ : observed?.nativeSessionId && observed.nativeSessionId !== entry.nativeSessionId
42
+ ? undefined : binding?.id ?? entry.bindingId;
43
+ if (event === "observed" && to === entry.state && nativeSessionId === entry.nativeSessionId)
44
+ return;
45
+ const transition = Object.freeze({ owner: entry.owner, from: entry.state, to, event,
46
+ revision: entry.revision + 1, operationId: entry.operationId, nativeSessionId, bindingId });
47
+ hooks.before?.notify(transition);
48
+ entry.state = to;
49
+ entry.revision = transition.revision;
50
+ entry.nativeSessionId = nativeSessionId;
51
+ entry.bindingId = bindingId;
52
+ hooks.after?.notify(transition);
53
+ };
54
+ const observe = (entry, revision, inspection) => {
55
+ if (entry.revision === revision && entry.operationId === undefined)
56
+ send(entry, "observed", inspection);
57
+ return inspection;
58
+ };
59
+ const effect = async (owner, event, run) => {
60
+ const entry = entryFor(owner);
61
+ if (entry.operationId !== undefined)
62
+ throw new RuntimeHostContentionError("previous-process", `Host lifecycle already has an in-flight operation for ${owner.roleName}.`);
63
+ // Check state before reserving the operation; a rejected edge owns nothing.
64
+ if (!CONTROLLER_HOST_TRANSITIONS[event].from.includes(entry.state))
65
+ throw new RuntimeHostContentionError("previous-process", `Host lifecycle cannot ${event} from ${entry.state}.`);
66
+ entry.operationId = ++nextOperation;
67
+ send(entry, event);
68
+ try {
69
+ return await run(entry);
70
+ }
71
+ catch (error) {
72
+ send(entry, "failed");
73
+ throw error;
74
+ }
75
+ finally {
76
+ entry.operationId = undefined;
77
+ }
78
+ };
79
+ const inspectOwner = async (owner) => {
80
+ const entry = entryFor(owner), revision = entry.revision;
81
+ return observe(entry, revision, await raw.inspectOwner(owner));
82
+ };
83
+ const launch = async (request, before) => {
84
+ await inspectOwner(request.owner);
85
+ return effect(request.owner, "launch", async (entry) => {
86
+ const binding = createRuntimeBinding(request.mode === "new"
87
+ ? await raw.start(request, before) : await raw.restore(request, before));
88
+ if (JSON.stringify(normalizeRuntimeOwner(binding.owner)) !== JSON.stringify(entry.owner)
89
+ || binding.agentId !== request.agentId || binding.adapterId !== request.adapterId
90
+ || request.mode === "resume" && binding.nativeSessionId !== request.nativeSessionId) {
91
+ throw new Error("Host lifecycle launch handshake has mismatched owner/native identity.");
92
+ }
93
+ send(entry, "launched", undefined, binding);
94
+ return binding;
95
+ });
96
+ };
97
+ const port = {
98
+ start: launch, restore: launch, inspectOwner,
99
+ inspect: async (binding) => {
100
+ const entry = entryFor(binding.owner), revision = entry.revision;
101
+ const inspection = await raw.inspect(binding);
102
+ if (entry.bindingId && entry.bindingId !== binding.id
103
+ || entry.nativeSessionId && binding.nativeSessionId !== entry.nativeSessionId)
104
+ return inspection;
105
+ return observe(entry, revision, inspection);
106
+ },
107
+ ...(raw.inspectOwners === undefined ? {} : {
108
+ inspectOwners: async (owners) => {
109
+ const revisions = new Map(owners.map(owner => {
110
+ const entry = entryFor(owner);
111
+ return [entry, entry.revision];
112
+ }));
113
+ const results = await raw.inspectOwners(owners);
114
+ for (const { owner, inspection } of results) {
115
+ const entry = entryFor(owner), revision = revisions.get(entry);
116
+ if (revision !== undefined)
117
+ observe(entry, revision, inspection);
118
+ }
119
+ return results;
120
+ }
121
+ }),
122
+ stop: async (binding) => {
123
+ const current = entryFor(binding.owner);
124
+ if (current.nativeSessionId && binding.nativeSessionId !== current.nativeSessionId
125
+ || current.bindingId && binding.id !== current.bindingId) {
126
+ throw new Error("Host lifecycle stop carries a stale binding identity.");
127
+ }
128
+ return effect(binding.owner, "stop", async (entry) => {
129
+ await raw.stop(binding);
130
+ const observed = await raw.inspect(binding);
131
+ if (observed.state !== "stopped")
132
+ throw new Error("Host lifecycle stop has no physical exit proof.");
133
+ send(entry, "stopped");
134
+ });
135
+ },
136
+ stopOwner: owner => effect(owner, "stop", async (entry) => {
137
+ const stopped = await (hooks.stopOwner ? hooks.stopOwner(owner) : raw.stopOwner(owner));
138
+ send(entry, stopped ? "stopped" : "failed");
139
+ return stopped;
140
+ })
141
+ };
142
+ return {
143
+ port,
144
+ snapshot: (owner) => Object.freeze({ ...entryFor(owner) }),
145
+ // Graceful exit acknowledgement is not exit proof. A later physical
146
+ // observation moves stopping -> stopped; no timer or automatic replay.
147
+ requestExit: (owner, run) => effect(owner, "stop", async (entry) => {
148
+ const result = await run();
149
+ send(entry, "exit-sent");
150
+ return result;
151
+ })
152
+ };
153
+ }
@@ -0,0 +1,27 @@
1
+ import { createNonBlockingChecks } from "../runtime/lifecycleChecks.js";
2
+ /** Checks produce a recommendation; only the separately fenced coordinator
3
+ * can admit/execute it. Hints contain no target identity or restart authority. */
4
+ export function createHostLifecycleChecks(coordinator, onError = () => { }) {
5
+ const checks = createNonBlockingChecks([{
6
+ id: "host-version",
7
+ run: async (_hint, signal) => {
8
+ const observations = await coordinator.status();
9
+ if (signal.aborted)
10
+ return;
11
+ const recommended = coordinator.hasPending() || observations.some(entry => entry.status === "waiting" || entry.status === "unknown" || entry.status === "failed");
12
+ if (!recommended)
13
+ return;
14
+ // This await belongs exclusively to this optional check lane. Neither
15
+ // Host transitions nor Controller event folding wait for this operation.
16
+ const results = await coordinator.request();
17
+ for (const entry of results) {
18
+ if (entry.status === "failed" || entry.status === "unknown") {
19
+ onError(new Error(`Host upgrade ${entry.owner.roleName}/${entry.nativeSessionId}: ${entry.detail}`));
20
+ }
21
+ }
22
+ }
23
+ }], onError);
24
+ const close = checks.close;
25
+ checks.close = () => { close(); coordinator.stop(); };
26
+ return checks;
27
+ }