@zq-silk/yui 0.11.3 → 0.12.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 (108) hide show
  1. package/ARCHITECTURE.md +23 -4
  2. package/README.md +44 -22
  3. package/dist/cli/commandCatalog.js +13 -10
  4. package/dist/cli/interactionPolicy.js +5 -6
  5. package/dist/cli/updateCommand.js +3 -1
  6. package/dist/cli/updateOrchestrator.js +173 -28
  7. package/dist/cli/updatePorts.js +137 -8
  8. package/dist/cli/upgradeCommand.js +19 -9
  9. package/dist/cli.js +156 -57
  10. package/dist/commands/configCommands.js +13 -46
  11. package/dist/commands/executionAuditCommands.js +3 -2
  12. package/dist/commands/jobCommands.js +4 -10
  13. package/dist/commands/taskCommands.js +234 -200
  14. package/dist/commands/taskCompletionGate.js +29 -54
  15. package/dist/commands/taskContextCommand.js +61 -13
  16. package/dist/commands/taskInputCommands.js +8 -5
  17. package/dist/commands/taskNextActionCommand.js +56 -6
  18. package/dist/commands/taskOverviewCommand.js +17 -31
  19. package/dist/commands/taskRoleRuntimeStatus.js +55 -7
  20. package/dist/commands/taskWorkspaceCommands.js +5 -2
  21. package/dist/config/configCatalog.js +3 -3
  22. package/dist/config/yuiConfig.js +3 -7
  23. package/dist/context/wakeNotification.js +20 -5
  24. package/dist/controller/agentRuntimeObserver.js +247 -51
  25. package/dist/controller/clientRuntime.js +39 -3
  26. package/dist/controller/controller.js +31 -27
  27. package/dist/controller/fileSchedulerStoreAdapter.js +390 -332
  28. package/dist/controller/runtime.js +58 -3
  29. package/dist/controller/runtimeEventInbox.js +17 -7
  30. package/dist/controller/runtimeEventProcessor.js +12 -19
  31. package/dist/controller/runtimeHookRunFence.js +19 -4
  32. package/dist/controller/runtimeObservationHook.js +45 -0
  33. package/dist/controller/structuredProviderObservation.js +20 -3
  34. package/dist/core/controllerClient.js +20 -2
  35. package/dist/core/controllerServer.js +1 -0
  36. package/dist/execution/resourceBroker.js +5 -4
  37. package/dist/executor/agentExecutor.js +52 -50
  38. package/dist/executor/effectiveLaunch.js +8 -48
  39. package/dist/executor/fileRoleLaunchPlanner.js +129 -45
  40. package/dist/executor/workspacePreflightClassification.js +23 -2
  41. package/dist/interaction/operatorPresentation.js +33 -89
  42. package/dist/lifecycle/exactRunTerminalization.js +69 -5
  43. package/dist/observability/executionAudit.js +5 -0
  44. package/dist/observability/orchestrationMetrics.js +8 -3
  45. package/dist/profile/agentProfile.js +1 -1
  46. package/dist/release/cliHomeReleaseFence.js +123 -0
  47. package/dist/release/runtimeRelease.js +20 -0
  48. package/dist/repository/taskWorkspacePreparer.js +176 -60
  49. package/dist/resources/sqliteResourceRegistry.js +1 -1
  50. package/dist/review/deltaRecheck.js +12 -51
  51. package/dist/review/reviewAcceptance.js +26 -0
  52. package/dist/review/reviewConfig.js +0 -31
  53. package/dist/review/reviewDecision.js +113 -0
  54. package/dist/review/reviewOutcomeClassifier.js +1 -1
  55. package/dist/review/reviewRound.js +1 -1
  56. package/dist/review/reviewerAvailability.js +69 -0
  57. package/dist/run/recoveryProjection.js +45 -6
  58. package/dist/runtime/agentDriverObservation.js +24 -10
  59. package/dist/runtime/agentHost.js +159 -85
  60. package/dist/runtime/builtinAgentDrivers.js +4 -3
  61. package/dist/runtime/builtinTranscriptObserver.js +301 -64
  62. package/dist/runtime/builtinTranscriptUsage.js +9 -7
  63. package/dist/runtime/conversationSwitch.js +277 -0
  64. package/dist/runtime/index.js +2 -1
  65. package/dist/runtime/launchBroker.js +12 -0
  66. package/dist/runtime/processExitOutbox.js +88 -0
  67. package/dist/runtime/providerRuntimeIdentity.js +29 -1
  68. package/dist/runtime/runtimeHealthPolicy.js +5 -5
  69. package/dist/runtime/runtimeObservation.js +28 -0
  70. package/dist/runtime/runtimeProjection.js +22 -32
  71. package/dist/runtime/sessionTokenMetrics.js +181 -0
  72. package/dist/runtime/structuredProviderHost.js +7 -1
  73. package/dist/runtime/tmuxAdapters.js +4 -1
  74. package/dist/scheduler/activeRoleRunDelivery.js +34 -199
  75. package/dist/scheduler/activeTaskProgress.js +7 -6
  76. package/dist/scheduler/leaderWakeupProcessor.js +42 -138
  77. package/dist/scheduler/operatorEvent.js +34 -0
  78. package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
  79. package/dist/scheduler/roleRunStall.js +53 -31
  80. package/dist/scheduler/taskExecutionProjection.js +97 -76
  81. package/dist/scheduler/taskObservabilityProjection.js +10 -11
  82. package/dist/storage/migration/productionRegistry.js +379 -0
  83. package/dist/storage/sqliteSchema.js +66 -23
  84. package/dist/storage/sqliteStore.js +46 -23
  85. package/dist/storage/storeRpc.js +0 -1
  86. package/dist/storage/taskStore.js +15 -30
  87. package/dist/storage/upgrade/homeClassification.js +52 -0
  88. package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
  89. package/dist/storage/upgrade/recordVersions.js +3 -2
  90. package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
  91. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
  92. package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
  93. package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
  94. package/dist/task/completionReadiness.js +10 -6
  95. package/dist/task/nextAction.js +82 -58
  96. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  97. package/dist/web/assets/client/components.js +13 -3
  98. package/dist/web/assets/client/i18n.js +6 -2
  99. package/dist/web/webSnapshot.js +7 -1
  100. package/i18n/README.zh-CN.md +2 -2
  101. package/package.json +1 -1
  102. package/skills/yui-leader/SKILL.md +60 -20
  103. package/skills/yui-operator/SKILL.md +13 -4
  104. package/skills/yui-reviewer/SKILL.md +35 -11
  105. package/dist/context/sessionContextBudget.js +0 -71
  106. package/dist/lifecycle/contextBudgetRollover.js +0 -81
  107. package/dist/lifecycle/taskRoleSessionReset.js +0 -126
  108. package/dist/scheduler/operatorNotification.js +0 -59
@@ -62,7 +62,7 @@ export function classifyReviewRoundOutcome(round, evidence) {
62
62
  reason: corroborationFailure
63
63
  };
64
64
  }
65
- /** True only when a Round may consume semantic Review budget or feed findings. */
65
+ /** True only when a Round contains semantic Reviewer evidence. */
66
66
  export function isSemanticReviewRound(round, evidence) {
67
67
  return classifyReviewRoundOutcome(round, evidence)?.kind === "semantic";
68
68
  }
@@ -120,7 +120,7 @@ export function finishReviewRound(round, status, summary, now, result = {}) {
120
120
  return terminal;
121
121
  }
122
122
  // Fail closed: a completed delta Round without an explicit, valid
123
- // disposition escalates to a full Review. Uncertainty never accepts.
123
+ // disposition remains non-accepting for Leader routing. Uncertainty never accepts.
124
124
  const disposition = result.deltaDisposition ?? "requires-full-review";
125
125
  const reasoning = result.deltaReasoning ?? result.report ?? summary;
126
126
  return validateReviewRound({
@@ -0,0 +1,69 @@
1
+ import { nextPendingBatch, mailboxHasWork } from "../coordination/workMailbox.js";
2
+ import { runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
3
+ /** One authoritative read-only projection for a Task Reviewer Role slot. */
4
+ export function projectReviewerAvailability(store, taskId, reviewerRoleName) {
5
+ const active = store.getActiveAgentRun(taskId, reviewerRoleName)
6
+ ?? store.listAgentRuns(taskId).find((run) => (run.roleName === reviewerRoleName && run.status === "active"));
7
+ if (active !== null && active !== undefined) {
8
+ return {
9
+ kind: "busy",
10
+ reviewerRoleName,
11
+ phase: "active-run",
12
+ activeRunId: active.id,
13
+ ...(active.reviewRoundId === undefined
14
+ ? {}
15
+ : { activeReviewRoundId: active.reviewRoundId }),
16
+ startedAt: active.createdAt,
17
+ retryable: true,
18
+ retryAfterSeconds: 5
19
+ };
20
+ }
21
+ const activeRound = store.listReviewRounds(taskId).find((round) => ((round.scope ?? "work-item") === "task"
22
+ && (round.status === "pending" || round.status === "running")
23
+ && (round.reviewerRoleName === reviewerRoleName
24
+ || round.executionGroup?.lanes.some(({ roleName }) => roleName === reviewerRoleName) === true)));
25
+ if (activeRound !== undefined) {
26
+ return {
27
+ kind: "busy",
28
+ reviewerRoleName,
29
+ phase: "review-slot",
30
+ activeReviewRoundId: activeRound.id,
31
+ startedAt: activeRound.createdAt,
32
+ retryable: true,
33
+ retryAfterSeconds: 5
34
+ };
35
+ }
36
+ const reviewerMailbox = store.getWorkMailbox({
37
+ kind: "role",
38
+ taskId,
39
+ roleName: reviewerRoleName
40
+ });
41
+ if (reviewerMailbox !== null && mailboxHasWork(reviewerMailbox)) {
42
+ return mailboxBusy(reviewerRoleName, "mailbox", reviewerMailbox);
43
+ }
44
+ const runtimeMailbox = store.getWorkMailbox(runtimeLifecycleTarget({
45
+ scope: "task",
46
+ taskId,
47
+ roleName: reviewerRoleName
48
+ }));
49
+ if (runtimeMailbox !== null && mailboxHasWork(runtimeMailbox)) {
50
+ return mailboxBusy(reviewerRoleName, "runtime-lifecycle", runtimeMailbox);
51
+ }
52
+ return {
53
+ kind: "available",
54
+ reviewerRoleName,
55
+ retryable: true
56
+ };
57
+ }
58
+ function mailboxBusy(reviewerRoleName, phase, mailbox) {
59
+ const pending = nextPendingBatch(mailbox);
60
+ const startedAt = mailbox.processing?.startedAt ?? pending?.firstQueuedAt;
61
+ return {
62
+ kind: "busy",
63
+ reviewerRoleName,
64
+ phase,
65
+ ...(startedAt === undefined ? {} : { startedAt }),
66
+ retryable: true,
67
+ retryAfterSeconds: 5
68
+ };
69
+ }
@@ -1,10 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
3
3
  import { actionableExecutionLaneRecoveries } from "../execution/executionHealth.js";
4
+ import { runOwnsBlockingProviderContinuation } from "../runtime/runtimeContinuationProjection.js";
5
+ import { runHasActiveRuntimeOperations } from "../runtime/runtimeObservation.js";
4
6
  export const RUN_RECOVERY_ACTIONS = [
5
7
  "diagnose",
6
8
  "retry",
7
- "replace-session",
8
9
  "terminate"
9
10
  ];
10
11
  /**
@@ -17,13 +18,32 @@ export function readRunRecoveryFacts(store, taskId, runId) {
17
18
  return null;
18
19
  const task = store.getTask(taskId);
19
20
  const sessionSet = store.getTaskRoleSessionSet(taskId, run.roleName);
21
+ const events = store.listEvents(taskId);
22
+ const roleMailbox = store.getWorkMailbox?.({
23
+ kind: "role",
24
+ taskId,
25
+ roleName: run.roleName
26
+ }) ?? null;
20
27
  const progress = latestRunDurableProgressAt(store, taskId, run.roleName, runId);
21
28
  return {
22
29
  run,
23
30
  task: task === null ? null : { id: task.id, status: task.status },
24
31
  sessionSet,
32
+ inputDeliveryUnsettled: roleMailbox?.inputDelivery != null,
33
+ blockingProviderContinuation: runOwnsBlockingProviderContinuation(events, {
34
+ taskId,
35
+ roleName: run.roleName,
36
+ runId: run.id,
37
+ agentId: run.effective.agentId
38
+ }),
39
+ activeRuntimeOperation: runHasActiveRuntimeOperations(events, {
40
+ taskId,
41
+ roleName: run.roleName,
42
+ runId: run.id,
43
+ agentId: run.effective.agentId
44
+ }),
25
45
  progress,
26
- latestProviderObservation: latestRunProviderObservation(store.listEvents(taskId), runId)
46
+ latestProviderObservation: latestRunProviderObservation(events, runId)
27
47
  };
28
48
  }
29
49
  /**
@@ -72,9 +92,12 @@ export function projectRunRecovery(facts) {
72
92
  ? ["accepted", "ambiguous"]
73
93
  : ["rejected", "ambiguous"];
74
94
  const blocked = recoveryBlocker(facts, session, canonicalProgressAt);
75
- const supportedActions = session?.status === "stopped" || session?.status === "broken"
76
- ? RUN_RECOVERY_ACTIONS.filter((action) => action !== "retry")
77
- : RUN_RECOVERY_ACTIONS;
95
+ const sessionTerminal = session?.status === "stopped" || session?.status === "broken";
96
+ const providerTerminal = sessionSet?.providerBinding?.turn?.status === "failed"
97
+ || sessionSet?.providerBinding?.turn?.status === "cancelled"
98
+ || sessionSet?.providerBinding?.turn?.status === "rejected";
99
+ const supportedActions = RUN_RECOVERY_ACTIONS.filter((action) => ((action !== "retry" || !sessionTerminal)
100
+ && (action !== "terminate" || sessionTerminal || providerTerminal)));
78
101
  const actions = blocked === null
79
102
  ? supportedActions.map((action) => buildActionPlan(facts, action, session, canonicalProgressAt))
80
103
  : [];
@@ -139,6 +162,23 @@ function recoveryBlocker(facts, session, canonicalProgressAt) {
139
162
  return "progress-unavailable";
140
163
  if (session === null)
141
164
  return "session-missing";
165
+ if (facts.inputDeliveryUnsettled)
166
+ return "provider-input-delivery-unsettled";
167
+ if (facts.blockingProviderContinuation)
168
+ return "provider-continuation-writer-owned";
169
+ if (facts.activeRuntimeOperation)
170
+ return "provider-operation-active";
171
+ const binding = facts.sessionSet?.providerBinding;
172
+ if (run.deliveredAt !== undefined
173
+ && (binding === null || binding?.turn === null))
174
+ return "provider-turn-state-missing";
175
+ if (binding !== null && binding !== undefined) {
176
+ if (["submitting", "accepted", "running", "delivery-unknown"].includes(binding.turn?.status ?? ""))
177
+ return "provider-turn-unsettled";
178
+ if (binding.authority.owner === "human" || binding.authority.owner === "unknown") {
179
+ return "provider-writer-authority-unavailable";
180
+ }
181
+ }
142
182
  return null;
143
183
  }
144
184
  function buildActionPlan(facts, action, session, canonicalProgressAt) {
@@ -208,6 +248,5 @@ function actionAcceptance(facts, action) {
208
248
  const ACTION_REASONS = {
209
249
  diagnose: "Collect bounded diagnostics before any state-changing recovery.",
210
250
  retry: "Request another provider turn on the same native Session when the failure is transient.",
211
- "replace-session": "Request a fresh native Session when the current one is unusable.",
212
251
  terminate: "Fail the Run explicitly when recovery is not viable."
213
252
  };
@@ -33,18 +33,24 @@ export function mapAgentDriverUsage(input, usage) {
33
33
  });
34
34
  }
35
35
  function observation(input, mapped) {
36
- const eventId = hookEventId(input, mapped);
36
+ const requestOccurrenceId = mapped.kind === "activity.observed"
37
+ && mapped.payload.usage?.semantics === "request-context"
38
+ ? requireRequestOccurrenceId(mapped.payload.activityId)
39
+ : undefined;
40
+ const eventId = hookEventId(input, mapped, requestOccurrenceId);
37
41
  const fence = { ...input.fence, ...mapped.fence };
38
42
  return createRuntimeObservation({
39
43
  schemaVersion: 2,
40
44
  eventId,
41
- semanticKey: runtimeObservationSemanticKey({
42
- eventId,
43
- kind: mapped.kind,
44
- fence,
45
- ...(input.sequence === undefined ? {} : { sequence: input.sequence }),
46
- payload: mapped.payload
47
- }),
45
+ semanticKey: requestOccurrenceId === undefined
46
+ ? runtimeObservationSemanticKey({
47
+ eventId,
48
+ kind: mapped.kind,
49
+ fence,
50
+ ...(input.sequence === undefined ? {} : { sequence: input.sequence }),
51
+ payload: mapped.payload
52
+ })
53
+ : `provider-request:${eventId}`,
48
54
  kind: mapped.kind,
49
55
  authority: "provider-structured",
50
56
  receivedAt: input.receivedAt,
@@ -60,13 +66,21 @@ function requireMatchingDriver(input) {
60
66
  throw new Error("Agent Driver Hook fence does not match the selected Driver.");
61
67
  }
62
68
  }
63
- function hookEventId(input, mapped) {
69
+ function hookEventId(input, mapped, requestOccurrenceId) {
64
70
  const digest = createHash("sha256").update(JSON.stringify([
65
71
  input.driver.id,
66
72
  input.hookEventName,
67
73
  input.fence,
68
74
  mapped.kind,
69
- mapped.payload
75
+ ...(requestOccurrenceId === undefined
76
+ ? [mapped.payload]
77
+ : [{ requestOccurrenceId }])
70
78
  ])).digest("hex");
71
79
  return `runtime-observation-${digest}`;
72
80
  }
81
+ function requireRequestOccurrenceId(value) {
82
+ if (value === undefined || value.trim().length === 0 || value.includes("\0")) {
83
+ throw new Error("Request-context usage requires a stable activity id.");
84
+ }
85
+ return value.trim();
86
+ }
@@ -1,18 +1,20 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { appendFileSync, chmodSync, closeSync, fsyncSync, mkdirSync, openSync, rmSync } from "node:fs";
3
- import { readdir, readFile, rename, unlink } from "node:fs/promises";
2
+ import { chmodSync, mkdirSync, rmSync } from "node:fs";
4
3
  import { tmpdir } from "node:os";
5
4
  import { dirname, join, resolve } from "node:path";
6
5
  import { createConnection, createServer } from "node:net";
7
6
  import { createInterface } from "node:readline";
8
- import { callController, ControllerClientError } from "../core/controllerClient.js";
7
+ import { callController, controllerCallMayHaveApplied, ControllerClientError } from "../core/controllerClient.js";
9
8
  import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
9
+ import { callFileTaskController } from "../controller/clientRuntime.js";
10
+ import { isForeignHandoverLockHeld } from "../release/runtimeRelease.js";
10
11
  import { publishStructuredProviderAccepted, publishStructuredProviderActivationTerminal, publishStructuredConversationRecoverability, publishStructuredProviderOpened, publishStructuredProviderTerminal } from "../controller/structuredProviderObservation.js";
11
12
  import { validateAgentHostLaunchPayload } from "./launchBroker.js";
12
13
  import { ProviderDeliveryUnknownError, ProviderConversationMissingError, ProviderTurnRejectedError, startStructuredProviderSession } from "./structuredProviderHost.js";
13
14
  import { sameProviderAuthorityFence, validateProviderAuthorityFence } from "./providerAuthorityFence.js";
14
15
  import { validateRuntimeProcessExitObservation } from "./processExitObservation.js";
15
- import { readRuntimeStopReceipt, removeRuntimeStopReceipt } from "./runtimeStopReceipt.js";
16
+ import { persistRuntimeProcessExitObservation, replayRuntimeProcessExitOutbox } from "./processExitOutbox.js";
17
+ import { readRuntimeStopReceipt, removeRuntimeStopReceipt, writeRuntimeStopReceipt } from "./runtimeStopReceipt.js";
16
18
  import { AGENT_HOST_CONTROL_TIMEOUT_MS, AGENT_HOST_READY_TIMEOUT_MS } from "./runtimeDeadlines.js";
17
19
  export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v2";
18
20
  const HOST_CONTROL_MAX_BYTES = 32 * 1024;
@@ -26,6 +28,7 @@ export async function runAgentHost(input) {
26
28
  let session;
27
29
  let sessionPayload;
28
30
  let activeTurnPayload;
31
+ const switchDetachedSessions = new WeakSet();
29
32
  let activationId;
30
33
  let conversationRecoverability = "unknown";
31
34
  let authority;
@@ -116,19 +119,21 @@ export async function runAgentHost(input) {
116
119
  const stopReceipt = readRuntimeStopReceipt(input.home, currentPayload.launchId);
117
120
  const observedAt = new Date().toISOString();
118
121
  const failures = [];
119
- try {
120
- await publishStructuredProviderActivationTerminal({
121
- home: input.home,
122
- environment: currentPayload.environment,
123
- conversationId: providerSession.conversationId,
124
- nativeSessionId: providerSession.nativeSessionId,
125
- activationId: currentActivationId,
126
- status: stopReceipt !== null || hostStopRequested ? "ended" : "failed",
127
- observedAt
128
- });
129
- }
130
- catch (error) {
131
- failures.push(`activation terminal: ${errorText(error)}`);
122
+ if (!switchDetachedSessions.has(providerSession)) {
123
+ try {
124
+ await publishStructuredProviderActivationTerminal({
125
+ home: input.home,
126
+ environment: currentPayload.environment,
127
+ conversationId: providerSession.conversationId,
128
+ nativeSessionId: providerSession.nativeSessionId,
129
+ activationId: currentActivationId,
130
+ status: stopReceipt !== null || hostStopRequested ? "ended" : "failed",
131
+ observedAt
132
+ });
133
+ }
134
+ catch (error) {
135
+ failures.push(`activation terminal: ${errorText(error)}`);
136
+ }
132
137
  }
133
138
  let exitPersisted = false;
134
139
  try {
@@ -163,7 +168,7 @@ export async function runAgentHost(input) {
163
168
  if (stopReceipt !== null && exitPersisted) {
164
169
  removeRuntimeStopReceipt(input.home, currentPayload.launchId);
165
170
  }
166
- if (hostStopRequested)
171
+ if (hostStopRequested || !ownsCurrentSession)
167
172
  return;
168
173
  updateSnapshot(hostSnapshot(failures.length === 0 ? "exited" : "failed", {
169
174
  launchId: currentPayload.launchId,
@@ -196,9 +201,12 @@ export async function runAgentHost(input) {
196
201
  throw new Error("Agent Host still owns an unsettled Provider Turn.");
197
202
  }
198
203
  const requestedAuthority = validateProviderAuthorityFence(providerControl.authority);
199
- if (authority === undefined)
204
+ const replacesCurrentConversation = session !== undefined && providerControl.kind === "new";
205
+ if (!replacesCurrentConversation && authority === undefined)
200
206
  authority = requestedAuthority;
201
- else if (!sameProviderAuthorityFence(authority, requestedAuthority)) {
207
+ else if (!replacesCurrentConversation
208
+ && authority !== undefined
209
+ && !sameProviderAuthorityFence(authority, requestedAuthority)) {
202
210
  throw new Error("Agent Host launch carries a stale Provider authority fence.");
203
211
  }
204
212
  updateSnapshot(hostSnapshot("starting", {
@@ -217,6 +225,43 @@ export async function runAgentHost(input) {
217
225
  let providerAcceptedAttemptId;
218
226
  let durableInitialTurn;
219
227
  try {
228
+ if (replacesCurrentConversation) {
229
+ const previousSession = session;
230
+ const previousPayload = sessionPayload;
231
+ const previousActivationId = activationId ?? previousPayload?.launchId;
232
+ if (previousPayload === undefined || previousActivationId === undefined
233
+ || authority?.owner !== "controller") {
234
+ throw new Error("Agent Host cannot detach an inexact Provider Activation.");
235
+ }
236
+ await detachDurableProviderForConversationSwitch(input.home, {
237
+ taskId: requiredEnvironment(next.environment.YUI_TASK_ID, "Task id"),
238
+ roleName: requiredEnvironment(next.environment.YUI_ROLE, "Role name"),
239
+ runId: requiredEnvironment(next.environment.YUI_RUN_ID, "Run id"),
240
+ agentId: requiredEnvironment(next.environment.YUI_AGENT_ID, "Agent id"),
241
+ launchId: next.launchId,
242
+ previousConversationId: previousSession.conversationId,
243
+ previousNativeSessionId: previousSession.nativeSessionId,
244
+ previousActivationId,
245
+ nextAuthorityEpoch: requestedAuthority.epoch,
246
+ nextAuthorityHolderId: requestedAuthority.holderId,
247
+ observedAt: new Date().toISOString()
248
+ });
249
+ switchDetachedSessions.add(previousSession);
250
+ // The persistent Provider child belongs to the Activation that first
251
+ // created it, not to the most recent resume Run payload. The exit
252
+ // observer carries that Activation launch id, so the stop receipt must
253
+ // use the same identity or the expected switch detach is misclassified
254
+ // as an abnormal child exit and generic cleanup can kill the Host.
255
+ writeRuntimeStopReceipt(input.home, previousActivationId, new Date());
256
+ authority = undefined;
257
+ await terminateProviderSessionForConversationSwitch(previousSession);
258
+ if (session === previousSession)
259
+ session = undefined;
260
+ sessionPayload = undefined;
261
+ activationId = undefined;
262
+ conversationRecoverability = "unknown";
263
+ authority = requestedAuthority;
264
+ }
220
265
  if (session !== undefined) {
221
266
  if (session.adapterId !== providerControl.adapterId
222
267
  || providerControl.mode !== "resume"
@@ -493,30 +538,12 @@ export async function runAgentHost(input) {
493
538
  }
494
539
  };
495
540
  const durableTurn = hostTurnControlParams(currentPayload, currentSession.nativeSessionId, currentAuthority, attemptId);
496
- try {
497
- await beginDurableProviderTurn(input.home, durableTurn);
498
- }
499
- catch (error) {
500
- await callController(input.home, "runtime.provider-turn-submission-resolve", {
501
- ...durableTurn,
502
- status: "rejected",
503
- reason: `Human Turn intent acknowledgement failed before Provider write: ${errorText(error)}`,
504
- observedAt: new Date().toISOString()
505
- }).catch(() => { });
506
- throw error;
507
- }
541
+ await beginDurableProviderTurn(input.home, durableTurn);
508
542
  try {
509
543
  await submitTurn(turnControl);
510
544
  }
511
545
  catch (error) {
512
- await callController(input.home, "runtime.provider-turn-submission-resolve", {
513
- ...durableTurn,
514
- status: error instanceof ProviderDeliveryUnknownError
515
- ? "delivery-unknown"
516
- : "rejected",
517
- reason: errorText(error),
518
- observedAt: new Date().toISOString()
519
- }).catch(() => { });
546
+ await resolveProviderTurnSubmission(input.home, durableTurn, error);
520
547
  throw error;
521
548
  }
522
549
  process.stdout.write("Provider accepted the human Turn; waiting for its terminal boundary.\n");
@@ -670,50 +697,19 @@ async function redeem(home, launchId, ticket) {
670
697
  return validateAgentHostLaunchPayload(result);
671
698
  }
672
699
  async function persistAndSubmitExit(home, observation) {
673
- const directory = resolve(join(home, "runtime", "agent-host-outbox"));
674
- mkdirSync(directory, { recursive: true, mode: 0o700 });
675
- const path = join(directory, `${observation.hostInstanceId}.jsonl`);
676
- const descriptor = openSync(path, "a", 0o600);
677
700
  try {
678
- appendFileSync(descriptor, `${JSON.stringify(observation)}\n`, "utf8");
679
- fsyncSync(descriptor);
680
- }
681
- finally {
682
- closeSync(descriptor);
683
- }
684
- chmodSync(path, 0o600);
685
- await callController(home, "runtime.process-exit-observe", observation);
686
- rmSync(path, { force: true });
687
- }
688
- async function replayExitOutbox(home) {
689
- const directory = resolve(join(home, "runtime", "agent-host-outbox"));
690
- let entries;
691
- try {
692
- entries = await readdir(directory);
701
+ await persistRuntimeProcessExitObservation(home, observation, (value) => callController(home, "runtime.process-exit-observe", value).then(() => undefined));
693
702
  }
694
703
  catch (error) {
695
- if (error.code === "ENOENT")
704
+ // The durable outbox is the acknowledgement during a planned Controller
705
+ // gap. The replacement Controller drains it before accepting new work.
706
+ if (isForeignHandoverLockHeld(home))
696
707
  return;
697
708
  throw error;
698
709
  }
699
- for (const name of entries.filter((value) => value.includes(".jsonl")).sort()) {
700
- const path = join(directory, name);
701
- const claimed = `${path}.claim-${process.pid}-${randomUUID()}`;
702
- try {
703
- await rename(path, claimed);
704
- }
705
- catch (error) {
706
- if (error.code === "ENOENT")
707
- continue;
708
- throw error;
709
- }
710
- const lines = (await readFile(claimed, "utf8")).split("\n").filter(Boolean);
711
- for (const line of lines) {
712
- const observation = validateRuntimeProcessExitObservation(JSON.parse(line));
713
- await callController(home, "runtime.process-exit-observe", observation);
714
- }
715
- await unlink(claimed);
716
- }
710
+ }
711
+ async function replayExitOutbox(home) {
712
+ await replayRuntimeProcessExitOutbox(home, (observation) => callController(home, "runtime.process-exit-observe", observation).then(() => undefined));
717
713
  }
718
714
  /** Internal socket boundary exported for transport-level verification. */
719
715
  export async function openAgentHostControl(home, payload, snapshot, dispatch) {
@@ -906,26 +902,104 @@ function hostTurnControlParams(payload, nativeSessionId, authority, attemptId) {
906
902
  }
907
903
  async function beginDurableProviderTurn(home, durableTurn) {
908
904
  try {
909
- await callController(home, "runtime.provider-turn-begin", durableTurn);
905
+ await callControllerIdempotently(home, "runtime.provider-turn-begin", durableTurn);
906
+ }
907
+ catch (error) {
908
+ if (!(error instanceof ControllerAcknowledgementUnknownError))
909
+ throw error;
910
+ await resolveProviderTurnSubmission(home, durableTurn, new Error(`Provider Turn intent acknowledgement failed before Provider write: ${errorText(error)}`));
911
+ throw error;
912
+ }
913
+ }
914
+ async function detachDurableProviderForConversationSwitch(home, request) {
915
+ await callControllerIdempotently(home, "runtime.conversation-switch-detach", request);
916
+ }
917
+ async function callControllerIdempotently(home, method, request) {
918
+ try {
919
+ await callAgentController(home, method, request);
910
920
  }
911
921
  catch (error) {
912
- if (!(error instanceof ControllerClientError) || error.code !== "INTERNAL_ERROR") {
922
+ if (!(error instanceof ControllerClientError)
923
+ || (error.code !== "INTERNAL_ERROR" && !controllerCallMayHaveApplied(error))) {
913
924
  throw error;
914
925
  }
915
- // The exact attempt is idempotent, so one lost acknowledgement can be
916
- // retried without creating a second Provider write.
917
- await callController(home, "runtime.provider-turn-begin", durableTurn);
926
+ const firstCallMayHaveApplied = controllerCallMayHaveApplied(error);
927
+ // These methods carry exact attempt, launch, and authority fences. A
928
+ // bounded replay confirms a commit whose acknowledgement may have been lost.
929
+ try {
930
+ await callAgentController(home, method, request);
931
+ }
932
+ catch (replayError) {
933
+ if (firstCallMayHaveApplied || controllerCallMayHaveApplied(replayError)) {
934
+ throw new ControllerAcknowledgementUnknownError(`${method} may have been committed, but its acknowledgement could not be confirmed: ${errorText(replayError)}`);
935
+ }
936
+ throw replayError;
937
+ }
938
+ }
939
+ }
940
+ class ControllerAcknowledgementUnknownError extends Error {
941
+ name = "ControllerAcknowledgementUnknownError";
942
+ }
943
+ async function terminateProviderSessionForConversationSwitch(providerSession) {
944
+ providerSession.terminate("SIGTERM");
945
+ const forceKill = setTimeout(() => providerSession.terminate("SIGKILL"), 3_000);
946
+ forceKill.unref();
947
+ let hardTimeout;
948
+ const timeout = new Promise((_resolve, reject) => {
949
+ hardTimeout = setTimeout(() => reject(new Error("Old Provider Activation did not exit after its switch authority was revoked.")), 8_000);
950
+ hardTimeout.unref();
951
+ });
952
+ try {
953
+ await Promise.race([providerSession.waitForExit(), timeout]);
954
+ }
955
+ finally {
956
+ clearTimeout(forceKill);
957
+ if (hardTimeout !== undefined)
958
+ clearTimeout(hardTimeout);
918
959
  }
919
960
  }
920
961
  async function resolveProviderTurnSubmission(home, durableTurn, error) {
921
- await callController(home, "runtime.provider-turn-submission-resolve", {
962
+ const attemptId = durableTurn.attemptId;
963
+ if (typeof attemptId !== "string") {
964
+ throw new Error("Provider Turn resolution has no attempt id.");
965
+ }
966
+ const request = {
922
967
  ...durableTurn,
923
968
  status: error instanceof ProviderDeliveryUnknownError
924
969
  ? "delivery-unknown"
925
970
  : "rejected",
926
971
  reason: errorText(error),
927
972
  observedAt: new Date().toISOString()
928
- }).catch(() => { });
973
+ };
974
+ try {
975
+ await callControllerIdempotently(home, "runtime.provider-turn-submission-resolve", request);
976
+ }
977
+ catch (resolutionError) {
978
+ throw new ProviderDeliveryUnknownError(`Provider submission outcome could not be durably resolved: ${errorText(resolutionError)}. Original outcome: ${errorText(error)}`, attemptId);
979
+ }
980
+ }
981
+ /**
982
+ * Existing in-flight launches keep using the old Controller while it drains.
983
+ * If the socket is already gone under an explicit handover fence, wait for
984
+ * the replacement and retry the domain-idempotent Agent Host operation.
985
+ */
986
+ async function callAgentController(home, method, params) {
987
+ try {
988
+ await callController(home, method, params);
989
+ return;
990
+ }
991
+ catch (error) {
992
+ if (!isControllerUnavailable(error) || !isForeignHandoverLockHeld(home)) {
993
+ throw error;
994
+ }
995
+ }
996
+ await callFileTaskController(home, method, params);
997
+ }
998
+ function isControllerUnavailable(error) {
999
+ return error instanceof ControllerClientError
1000
+ && (error.code === "CONTROLLER_NOT_RUNNING"
1001
+ || error.code === "CONTROLLER_UNAVAILABLE"
1002
+ || error.code === "CONTROLLER_DRAINING");
929
1003
  }
930
1004
  function requiredEnvironment(value, label) {
931
1005
  if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
@@ -51,8 +51,8 @@ const STRUCTURED_CLI_CAPABILITIES = Object.freeze({
51
51
  preInputReadiness: "unavailable",
52
52
  promptAcceptance: "exact",
53
53
  turnLifecycle: "exact",
54
- // Model activity is inferred from usage deltas; these are the operation
55
- // boundaries the current Hook surfaces actually expose.
54
+ // Transcript sources may emit an explicit activity identity separately
55
+ // from usage snapshots. Numeric token values never decide runtime health.
56
56
  operations: Object.freeze(["tool", "subagent"]),
57
57
  waiting: Object.freeze(["permission"]),
58
58
  usage: "streaming-cumulative",
@@ -70,7 +70,8 @@ export const BUILTIN_AGENT_DRIVERS = Object.freeze([
70
70
  observation: Object.freeze({
71
71
  ...STRUCTURED_CLI_CAPABILITIES.observation,
72
72
  sessionBootstrap: "preallocated",
73
- preInputReadiness: "exact"
73
+ preInputReadiness: "exact",
74
+ usage: "event-snapshot"
74
75
  })
75
76
  }),
76
77
  runtime: Object.freeze({