@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
@@ -0,0 +1,277 @@
1
+ import { roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
2
+ import { effectiveLaunchSnapshotsCompatibleForTaskSession } from "../executor/effectiveLaunch.js";
3
+ import { currentProviderActivation, currentProviderConversation } from "./providerRuntimeIdentity.js";
4
+ import { blockingProviderContinuations } from "./runtimeContinuationProjection.js";
5
+ import { runtimeObservationFromTaskEvent } from "./runtimeObservation.js";
6
+ export const CONVERSATION_SWITCH_REQUESTED_EVENT = "runtime.conversation-switch-requested";
7
+ export const CONVERSATION_SWITCH_RESOLVED_EVENT = "runtime.conversation-switch-resolved";
8
+ export const CONVERSATION_SWITCH_DETACHED_EVENT = "runtime.conversation-switch-detached";
9
+ export function providerConversationGeneration(sessions) {
10
+ const binding = sessions?.providerBinding;
11
+ if (binding === null || binding === undefined)
12
+ return null;
13
+ const current = currentProviderConversation(binding);
14
+ return `${binding.providerNamespace}:${binding.accountScope}:${current.epoch}:${current.conversationId}`;
15
+ }
16
+ export function projectConversationSwitch(events, roleName, sessions) {
17
+ const requests = events.filter((event) => (event.type === CONVERSATION_SWITCH_REQUESTED_EVENT
18
+ && event.payload.roleName === roleName));
19
+ const request = requests.at(-1);
20
+ if (request === undefined)
21
+ return null;
22
+ const requestId = request.payload.requestId;
23
+ const generation = request.payload.generation;
24
+ const requestedBy = request.payload.requestedBy;
25
+ const reason = request.payload.reason;
26
+ if (requestId === undefined || generation === undefined || reason === undefined
27
+ || (requestedBy !== "user" && requestedBy !== "operator" && requestedBy !== "leader")) {
28
+ return null;
29
+ }
30
+ const resolution = [...events].reverse().find((event) => (event.type === CONVERSATION_SWITCH_RESOLVED_EVENT
31
+ && event.payload.requestId === requestId));
32
+ const explicitStatus = resolution?.payload.status;
33
+ const currentGeneration = providerConversationGeneration(sessions);
34
+ const status = explicitStatus === "applied" || explicitStatus === "obsolete"
35
+ ? explicitStatus
36
+ : currentGeneration !== null && currentGeneration !== generation
37
+ ? "obsolete"
38
+ : "pending";
39
+ return {
40
+ requestId,
41
+ roleName,
42
+ generation,
43
+ requestedBy,
44
+ reason,
45
+ requestedAt: request.createdAt,
46
+ status,
47
+ ...(resolution === undefined ? {} : { resolvedAt: resolution.createdAt })
48
+ };
49
+ }
50
+ export function pendingConversationSwitch(events, roleName, sessions) {
51
+ const projected = projectConversationSwitch(events, roleName, sessions);
52
+ return projected?.status === "pending" ? projected : null;
53
+ }
54
+ export function roleSessionDispatchModeWithConversationSwitch(sessions, events, mailbox, roleName, agentId, effective) {
55
+ const ordinary = roleAgentSessionResumeMode(sessions, agentId, effective);
56
+ if (ordinary !== "resume") {
57
+ if (sessions?.providerBinding !== null && sessions?.providerBinding !== undefined) {
58
+ if (freshConversationLaunchAllowed({ sessions, events, mailbox, roleName })) {
59
+ return "new";
60
+ }
61
+ const existing = sessions.sessions[agentId];
62
+ if (existing?.nativeSessionId !== undefined
63
+ && (existing.status === "stopped" || existing.status === "broken")
64
+ && effectiveLaunchSnapshotsCompatibleForTaskSession(existing.effective, effective)) {
65
+ // A terminal local Activation does not prove the Provider Conversation
66
+ // is gone. Reattach to the same native identity by default.
67
+ return "resume";
68
+ }
69
+ throw new Error(`Fresh Provider Conversation is not yet safe: ${sessions.owner.taskId}/${roleName}.`);
70
+ }
71
+ return ordinary;
72
+ }
73
+ if (sessions?.providerBinding !== null && sessions?.providerBinding !== undefined
74
+ && freshConversationLaunchAllowed({ sessions, events, mailbox, roleName })) {
75
+ return "new";
76
+ }
77
+ return "resume";
78
+ }
79
+ export function conversationDetachmentBasis(input) {
80
+ const { sessions, mailbox } = input;
81
+ const binding = sessions.providerBinding;
82
+ const activation = binding === null ? null : currentProviderActivation(binding);
83
+ if (input.runMode !== "new" || binding === null || activation === null
84
+ || sessions.inFlight?.runId !== input.runId
85
+ || mailbox?.processing?.batchId !== sessions.inFlight.receiptId
86
+ || mailbox.processing.owner !== "controller"
87
+ || mailbox.processing.executionRef?.type !== "run"
88
+ || mailbox.processing.executionRef.taskId !== sessions.owner.taskId
89
+ || mailbox.processing.executionRef.id !== input.runId
90
+ || !actorRequestedSwitchBoundaryReady(sessions, mailbox)
91
+ || currentConversationExecutionBlockers(sessions, input.events, input.roleName).length > 0
92
+ || pendingConversationSwitch(input.events, input.roleName, sessions) === null) {
93
+ return null;
94
+ }
95
+ return "actor-request";
96
+ }
97
+ export function conversationReplacementBasis(input) {
98
+ const { sessions, mailbox } = input;
99
+ const binding = sessions.providerBinding;
100
+ if (input.runMode !== "new" || binding === null
101
+ || sessions.inFlight?.runId !== input.runId
102
+ || mailbox?.processing?.batchId !== sessions.inFlight.receiptId
103
+ || mailbox.processing.owner !== "controller"
104
+ || mailbox?.processing?.executionRef?.type !== "run"
105
+ || mailbox.processing.executionRef.taskId !== sessions.owner.taskId
106
+ || mailbox.processing.executionRef.id !== input.runId
107
+ || mailbox.inputDelivery !== null
108
+ || !conversationIsQuiescent(sessions, mailbox)
109
+ || currentConversationExecutionBlockers(sessions, input.events, input.roleName).length > 0) {
110
+ return null;
111
+ }
112
+ if (currentConversationIsExactlyUnrecoverable(sessions, input.events, input.roleName))
113
+ return "exact-unrecoverable";
114
+ return pendingConversationSwitch(input.events, input.roleName, sessions) === null
115
+ ? null
116
+ : "actor-request";
117
+ }
118
+ export function freshConversationLaunchAllowed(input) {
119
+ return freshConversationLaunchBlockers(input).length === 0;
120
+ }
121
+ /** Exact reasons a fresh Conversation cannot currently be admitted. */
122
+ export function freshConversationLaunchBlockers(input) {
123
+ const { sessions } = input;
124
+ if (sessions === null)
125
+ return [];
126
+ const session = sessions.sessions[sessions.activeAgentId];
127
+ if (sessions.providerBinding === null) {
128
+ return session === undefined
129
+ || session.status === "stopped"
130
+ || session.status === "broken"
131
+ ? []
132
+ : ["native-session-not-terminal"];
133
+ }
134
+ const request = pendingConversationSwitch(input.events, input.roleName, sessions);
135
+ const binding = sessions.providerBinding;
136
+ const blockers = [];
137
+ if (input.mailbox?.inputDelivery != null)
138
+ blockers.push("provider-input-delivery-unsettled");
139
+ if (binding.turn !== null
140
+ && ["submitting", "accepted", "running", "delivery-unknown"].includes(binding.turn.status))
141
+ blockers.push("provider-turn-unsettled");
142
+ blockers.push(...currentConversationExecutionBlockers(sessions, input.events, input.roleName));
143
+ if (blockers.length > 0)
144
+ return blockers;
145
+ if (request !== null && actorRequestedSwitchBoundaryReady(sessions, input.mailbox, input.candidateRunId))
146
+ return [];
147
+ if (currentProviderActivation(binding) !== null)
148
+ blockers.push("provider-activation-active");
149
+ if (binding.authority.owner !== "none")
150
+ blockers.push("provider-writer-authority-owned");
151
+ if (blockers.length > 0)
152
+ return blockers;
153
+ if (currentProviderConversation(binding).recoverability === "unrecoverable") {
154
+ return currentConversationIsExactlyUnrecoverable(sessions, input.events, input.roleName) ? [] : ["exact-unrecoverable-evidence-missing"];
155
+ }
156
+ if (request !== null)
157
+ return ["actor-switch-boundary-not-ready"];
158
+ return ["current-conversation-recoverable"];
159
+ }
160
+ function actorRequestedSwitchBoundaryReady(sessions, mailbox, candidateRunId) {
161
+ const binding = sessions?.providerBinding;
162
+ if (sessions === null || sessions === undefined
163
+ || binding === null || binding === undefined
164
+ || mailbox?.inputDelivery != null)
165
+ return false;
166
+ const turnSettled = binding.turn === null
167
+ || ["completed", "failed", "cancelled", "rejected"].includes(binding.turn.status);
168
+ if (!turnSettled)
169
+ return false;
170
+ const session = sessions.sessions[sessions.activeAgentId];
171
+ if (session?.status === "running")
172
+ return false;
173
+ const activation = currentProviderActivation(binding);
174
+ if (activation === null)
175
+ return binding.authority.owner === "none";
176
+ if (binding.authority.owner !== "controller"
177
+ || binding.authority.holderId !== activation.activationId)
178
+ return false;
179
+ if (session === undefined)
180
+ return false;
181
+ if (sessions.inFlight === null) {
182
+ if (mailbox?.processing === null)
183
+ return true;
184
+ return candidateRunId !== undefined
185
+ && mailbox?.processing?.owner === "controller"
186
+ && mailbox.processing.executionRef?.type === "run"
187
+ && mailbox.processing.executionRef.taskId === sessions.owner.taskId
188
+ && mailbox.processing.executionRef.id === candidateRunId;
189
+ }
190
+ return mailbox?.processing?.batchId === sessions.inFlight.receiptId
191
+ && mailbox.processing.owner === "controller"
192
+ && mailbox.processing.executionRef?.type === "run"
193
+ && mailbox.processing.executionRef.taskId === sessions.owner.taskId
194
+ && mailbox.processing.executionRef.id === sessions.inFlight.runId;
195
+ }
196
+ function conversationIsQuiescent(sessions, mailbox) {
197
+ const binding = sessions?.providerBinding;
198
+ if (binding === null || binding === undefined)
199
+ return false;
200
+ const turnSettled = binding.turn === null
201
+ || ["completed", "failed", "cancelled", "rejected"].includes(binding.turn.status);
202
+ return turnSettled
203
+ && currentProviderActivation(binding) === null
204
+ && binding.authority.owner === "none"
205
+ && mailbox?.inputDelivery == null;
206
+ }
207
+ /**
208
+ * A projected recoverability flag is necessary but not sufficient to replace
209
+ * a Conversation. Require the exact structured missing-Conversation fact for
210
+ * the current Provider identity and latest Activation generation, so an old
211
+ * observation cannot authorize a later fresh Conversation. The binding's Run
212
+ * id is deliberately excluded: replacement work rebinds that fence before
213
+ * launching, while the missing fact necessarily came from the preceding Run's
214
+ * exact attempt to resume this same Activation.
215
+ */
216
+ function currentConversationIsExactlyUnrecoverable(sessions, events, roleName) {
217
+ const binding = sessions.providerBinding;
218
+ if (binding === null)
219
+ return false;
220
+ const conversation = currentProviderConversation(binding);
221
+ if (conversation.recoverability !== "unrecoverable")
222
+ return false;
223
+ const activation = [...binding.activations].reverse().find((entry) => (entry.conversationId === conversation.conversationId));
224
+ const generationStartedAt = activation?.startedAt ?? conversation.createdAt;
225
+ return events.some((event) => {
226
+ const observation = runtimeObservationFromTaskEvent(event);
227
+ if (observation === null
228
+ || observation.kind !== "conversation.observed"
229
+ || observation.payload.recoverability !== "unrecoverable"
230
+ || (observation.authority !== "provider-structured"
231
+ && observation.authority !== "controller"))
232
+ return false;
233
+ const fence = observation.fence;
234
+ if (fence.taskId !== sessions.owner.taskId
235
+ || fence.roleName !== roleName
236
+ || fence.agentId !== binding.accountScope
237
+ || fence.driverId !== binding.providerNamespace
238
+ || fence.conversationId !== conversation.conversationId
239
+ || (activation !== undefined && fence.activationId !== activation.activationId)) {
240
+ return false;
241
+ }
242
+ return Date.parse(observation.observedAt ?? observation.receivedAt)
243
+ >= Date.parse(generationStartedAt);
244
+ });
245
+ }
246
+ /** Current Provider-owned work that may outlive a foreground Turn. */
247
+ function currentConversationExecutionBlockers(sessions, events, roleName) {
248
+ const binding = sessions.providerBinding;
249
+ if (binding === null)
250
+ return [];
251
+ const conversation = currentProviderConversation(binding);
252
+ const activation = [...binding.activations].reverse().find((entry) => (entry.conversationId === conversation.conversationId));
253
+ if (activation === undefined)
254
+ return ["provider-activation-identity-missing"];
255
+ const activeOperation = events.some((event) => {
256
+ const observation = runtimeObservationFromTaskEvent(event);
257
+ if (observation?.kind !== "operation.started")
258
+ return false;
259
+ const fence = observation.fence;
260
+ return fence.taskId === sessions.owner.taskId
261
+ && fence.roleName === roleName
262
+ && fence.agentId === binding.accountScope
263
+ && fence.driverId === binding.providerNamespace
264
+ && (fence.conversationId ?? fence.nativeSessionId) === conversation.conversationId
265
+ && (fence.activationId ?? fence.launchId) === activation.activationId;
266
+ });
267
+ const blockingContinuation = blockingProviderContinuations(events).some((entry) => (entry.taskId === sessions.owner.taskId
268
+ && entry.roleName === roleName
269
+ && entry.identity.providerNamespace === binding.providerNamespace
270
+ && entry.identity.accountScope === binding.accountScope
271
+ && entry.identity.conversationId === conversation.conversationId
272
+ && entry.identity.activationId === activation.activationId));
273
+ return [
274
+ ...(activeOperation ? ["provider-operation-active"] : []),
275
+ ...(blockingContinuation ? ["provider-continuation-writer-owned"] : [])
276
+ ];
277
+ }
@@ -1,5 +1,6 @@
1
1
  export { createPromptEnvelope } from "./promptEnvelope.js";
2
2
  export { codexTranscriptUsage, codexTranscriptUsageReport, claudeTranscriptUsage, claudeTranscriptUsageReport } from "./builtinTranscriptUsage.js";
3
+ export { projectSessionTokenMetrics, resolveSessionTokenIdentity, unobservedSessionTokenMetrics } from "./sessionTokenMetrics.js";
3
4
  export { createRuntimeBinding } from "./runtimeBinding.js";
4
5
  export { normalizeRuntimeOwner } from "./runtimeOwner.js";
5
6
  export { createSessionLaunchRequest } from "./sessionLaunchRequest.js";
@@ -13,7 +14,7 @@ export { formatRuntimeLaunchDiagnostic, redactLaunchArgument, redactLaunchText,
13
14
  export { DEFAULT_FORCED_GRACE_MS, DEFAULT_GRACEFUL_GRACE_MS, terminateSessionOwners } from "./sessionTerminationGuard.js";
14
15
  export { ProviderContinuationReconciliationService } from "./providerContinuationReconciliationService.js";
15
16
  export { codexNotificationBoundary, codexAppServerErrorIsMissing, CodexAppServerRequestError, CodexAppServerRuntime } from "./codexAppServerRuntime.js";
16
- export { createProviderRuntimeBinding, acceptProviderTurn, beginProviderTurn, currentProviderActivation, currentProviderAuthority, currentProviderConversation, endProviderActivation, markProviderTurnDeliveryUnknown, rebindProviderRuntimeRun, rejectProviderTurn, settleProviderTurn, startProviderActivation, supersedeProviderConversation, transferProviderAuthority, updateProviderConversationRecoverability, validateProviderRuntimeBinding } from "./providerRuntimeIdentity.js";
17
+ export { createProviderRuntimeBinding, acceptProviderTurn, beginProviderTurn, currentProviderActivation, currentProviderAuthority, currentProviderConversation, endProviderActivation, markProviderTurnDeliveryUnknown, rebindProviderRuntimeRun, rejectProviderTurn, settleProviderTurnSubmission, settleProviderTurn, startProviderActivation, supersedeProviderConversation, transferProviderAuthority, updateProviderConversationRecoverability, validateProviderRuntimeBinding } from "./providerRuntimeIdentity.js";
17
18
  export { FencedProviderControl } from "./providerControl.js";
18
19
  export { sameProviderAuthorityFence, validateProviderAuthorityFence } from "./providerAuthorityFence.js";
19
20
  export { decideProviderRecovery } from "./providerRecoveryDecision.js";
@@ -93,6 +93,18 @@ function validateProviderControl(control) {
93
93
  if (control.mode !== "new" && control.mode !== "resume") {
94
94
  throw new Error("Agent Host Provider control mode is invalid.");
95
95
  }
96
+ if (control.kind !== "new" && control.kind !== "resume" && control.kind !== "ensure") {
97
+ throw new Error("Agent Host Provider control kind is invalid.");
98
+ }
99
+ if ((control.kind === "new") !== (control.mode === "new")) {
100
+ throw new Error("Agent Host Provider control kind does not match its transport mode.");
101
+ }
102
+ if (control.kind !== "ensure" && control.initialTurn === undefined) {
103
+ throw new Error("Managed Provider new/resume launch requires its initial Turn.");
104
+ }
105
+ if (control.kind === "ensure" && control.initialTurn !== undefined) {
106
+ throw new Error("Managed Provider ensure launch cannot carry a new Turn.");
107
+ }
96
108
  const requiresNativeSessionId = control.mode === "resume" || control.adapterId === "claude";
97
109
  if (requiresNativeSessionId !== (control.nativeSessionId !== undefined)) {
98
110
  throw new Error("Agent Host Provider resume identity is inconsistent.");
@@ -0,0 +1,88 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, readFile, unlink } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+ import { writeTextFileAtomically } from "../storage/durableFile.js";
5
+ import { validateRuntimeProcessExitObservation } from "./processExitObservation.js";
6
+ /**
7
+ * Persist before attempting Controller delivery. The Controller also drains
8
+ * this outbox before it begins serving after a restart, so a Session does not
9
+ * have to remain alive merely to bridge a short handover window.
10
+ */
11
+ export async function persistRuntimeProcessExitObservation(home, observation, submit) {
12
+ const validated = validateRuntimeProcessExitObservation(observation);
13
+ const directory = outboxDirectory(home);
14
+ const identity = createHash("sha256").update(validated.observationId).digest("hex");
15
+ writeTextFileAtomically(join(directory, `${identity}.json`), `${JSON.stringify(validated)}\n`);
16
+ await replayRuntimeProcessExitOutbox(home, submit);
17
+ }
18
+ /**
19
+ * Replays every record through an idempotent observation boundary. A failed
20
+ * record remains durable and is considered again by the next replay.
21
+ */
22
+ export async function replayRuntimeProcessExitOutbox(home, submit) {
23
+ const directory = outboxDirectory(home);
24
+ let entries;
25
+ try {
26
+ entries = await readdir(directory);
27
+ }
28
+ catch (error) {
29
+ if (error.code === "ENOENT")
30
+ return;
31
+ throw error;
32
+ }
33
+ for (const name of entries.filter((name) => name.endsWith(".json")).sort()) {
34
+ const path = join(directory, name);
35
+ let raw;
36
+ try {
37
+ raw = await readFile(path, "utf8");
38
+ }
39
+ catch (error) {
40
+ // Another Agent Host or the replacement Controller may have submitted
41
+ // and removed this immutable record after our directory snapshot.
42
+ if (error.code === "ENOENT")
43
+ continue;
44
+ throw error;
45
+ }
46
+ const observation = validateRuntimeProcessExitObservation(JSON.parse(raw));
47
+ await submit(observation);
48
+ await unlink(path).catch((error) => {
49
+ if (error.code !== "ENOENT")
50
+ throw error;
51
+ });
52
+ }
53
+ // One-generation bridge for observations written by an Agent Host that was
54
+ // already running before this release. New writers use one immutable file
55
+ // per observation above, so they never share an append/unlink boundary.
56
+ for (const name of entries.filter(isLegacyOutboxEntry).sort()) {
57
+ const path = join(directory, name);
58
+ let raw;
59
+ try {
60
+ raw = await readFile(path, "utf8");
61
+ }
62
+ catch (error) {
63
+ // The old writer removes its own append log only after the Controller
64
+ // acknowledges delivery. If it won that race, there is nothing to replay.
65
+ if (error.code === "ENOENT")
66
+ continue;
67
+ throw error;
68
+ }
69
+ // Only consume newline-terminated records. A legacy writer can be between
70
+ // append bytes and its trailing newline while this snapshot is read.
71
+ const completeLength = raw.lastIndexOf("\n") + 1;
72
+ const lines = raw.slice(0, completeLength).split("\n").filter(Boolean);
73
+ for (const line of lines) {
74
+ await submit(validateRuntimeProcessExitObservation(JSON.parse(line)));
75
+ }
76
+ // Deliberately leave the legacy path in place. A pre-upgrade Agent Host may
77
+ // already hold an append descriptor for this inode; renaming/unlinking it
78
+ // would let a later append disappear into an unlinked file. The old writer
79
+ // removes the path after a successful direct submission. Otherwise the
80
+ // durable file remains for the next idempotent replay.
81
+ }
82
+ }
83
+ function outboxDirectory(home) {
84
+ return resolve(join(home, "runtime", "agent-host-outbox"));
85
+ }
86
+ function isLegacyOutboxEntry(name) {
87
+ return name.endsWith(".jsonl");
88
+ }
@@ -236,6 +236,30 @@ export function rejectProviderTurn(raw, input) {
236
236
  }
237
237
  });
238
238
  }
239
+ /** Resolves an Agent Host submission exactly once and accepts an exact acknowledgement replay. */
240
+ export function settleProviderTurnSubmission(raw, input) {
241
+ const binding = validateProviderRuntimeBinding(raw);
242
+ const attemptId = identity(input.attemptId, "Provider input attempt id");
243
+ if (binding.turn?.attemptId !== attemptId) {
244
+ throw new Error("Provider Turn does not match a resolvable delivery state.");
245
+ }
246
+ if (binding.turn.status === input.status)
247
+ return binding;
248
+ if (binding.turn.status !== "submitting") {
249
+ throw new Error("Provider Turn does not match a resolvable delivery state.");
250
+ }
251
+ return input.status === "delivery-unknown"
252
+ ? markProviderTurnDeliveryUnknown(binding, {
253
+ attemptId,
254
+ observedAt: input.resolvedAt,
255
+ reason: input.reason
256
+ })
257
+ : rejectProviderTurn(binding, {
258
+ attemptId,
259
+ rejectedAt: input.resolvedAt,
260
+ reason: input.reason
261
+ });
262
+ }
239
263
  export function settleProviderTurn(raw, input) {
240
264
  const binding = validateProviderRuntimeBinding(raw);
241
265
  const turn = binding.turn;
@@ -270,7 +294,11 @@ export function updateProviderConversationRecoverability(raw, recoverability) {
270
294
  export function supersedeProviderConversation(raw, input) {
271
295
  const binding = validateProviderRuntimeBinding(raw);
272
296
  const current = currentProviderConversation(binding);
273
- if (current.recoverability !== "unrecoverable") {
297
+ const basis = input.basis ?? "exact-unrecoverable";
298
+ if (basis !== "actor-request" && basis !== "exact-unrecoverable") {
299
+ throw new Error("Provider Conversation switch basis is invalid.");
300
+ }
301
+ if (basis === "exact-unrecoverable" && current.recoverability !== "unrecoverable") {
274
302
  throw new Error("Current Provider Conversation is not exactly unrecoverable.");
275
303
  }
276
304
  if (!input.noUnsettledInputDelivery) {
@@ -4,14 +4,14 @@
4
4
  *
5
5
  * The layers are deliberately time-based and conservative: short silence is
6
6
  * normal for high-reasoning-effort turns, large reviews, and tool waits.
7
- * Only deterministic dead/broken evidence or the durable semantic stall
8
- * window authorizes recovery; quiet time alone never resets a Run.
7
+ * Time-based layers authorize display hints and coalesced read-only diagnosis
8
+ * only. They never reset a Run or replace a Provider Conversation.
9
9
  */
10
10
  /** Runtime silence after which a live turn is surfaced as "quiet" (hint only). */
11
11
  export const RUNTIME_QUIET_AFTER_MS = 5 * 60_000;
12
- /** No durable semantic progress after which a read-only diagnostic is warranted. */
13
- export const RUNTIME_DIAGNOSTIC_AFTER_MS = 10 * 60_000;
14
- /** No durable semantic progress after which the scheduler raises a stall candidate. */
12
+ /** No durable semantic progress after which the checkpoint is displayed as overdue. */
13
+ export const RUNTIME_DIAGNOSTIC_AFTER_MS = 15 * 60_000;
14
+ /** No durable semantic progress before one coalesced read-only runtime probe is due. */
15
15
  export const SEMANTIC_STALL_WINDOW_MS = 30 * 60_000;
16
16
  export const DEFAULT_RUNTIME_HEALTH_POLICY = Object.freeze({
17
17
  quietAfterMs: RUNTIME_QUIET_AFTER_MS,
@@ -1,6 +1,19 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { requireDriverId } from "./agentDriver.js";
3
3
  export const RUNTIME_OBSERVATION_TASK_EVENT = "runtime.observation";
4
+ /**
5
+ * Numeric usage and value-less partial request boundaries are both durable
6
+ * token-projection evidence. The latter deliberately carries no activityId so
7
+ * it cannot be mistaken for lifecycle progress.
8
+ */
9
+ export function isRuntimeTokenEvidence(observation) {
10
+ return observation.kind === "activity.observed"
11
+ && (observation.payload.usage !== undefined
12
+ || (observation.payload.sourceId !== undefined
13
+ && observation.payload.activityId === undefined
14
+ && (observation.payload.observationQuality === "partial"
15
+ || observation.payload.observationQuality === "unavailable")));
16
+ }
4
17
  const KINDS = [
5
18
  "host.observed",
6
19
  "session.started",
@@ -210,6 +223,21 @@ export function runtimeObservationFromTaskEvent(event) {
210
223
  return null;
211
224
  }
212
225
  }
226
+ /**
227
+ * Runtime observation events are compacted by operation identity: a terminal
228
+ * operation replaces its matching start. Any retained exact start therefore
229
+ * represents Provider-owned work that is still active for this Run.
230
+ */
231
+ export function runHasActiveRuntimeOperations(events, owner) {
232
+ return events.some((event) => {
233
+ const observation = runtimeObservationFromTaskEvent(event);
234
+ return observation?.kind === "operation.started"
235
+ && observation.fence.taskId === owner.taskId
236
+ && observation.fence.roleName === owner.roleName
237
+ && observation.fence.runId === owner.runId
238
+ && observation.fence.agentId === owner.agentId;
239
+ });
240
+ }
213
241
  function normalizeFence(input) {
214
242
  return Object.freeze({
215
243
  ...(input.taskId === undefined ? {} : { taskId: requireIdentity(input.taskId, "Task id") }),
@@ -158,16 +158,23 @@ export function projectRuntimeObservation(current, raw) {
158
158
  }
159
159
  case "activity.observed": {
160
160
  const usage = event.payload.usage;
161
- if (usage !== undefined && !usageAdvanced(current.usage, usage)) {
162
- return next(current, { usage: Object.freeze({ ...usage }) });
163
- }
164
- return withActivity(next(current, {
161
+ const lifecycleActivity = event.payload.activityId !== undefined
162
+ && usage === undefined;
163
+ const projected = next(current, {
165
164
  ...(usage === undefined ? {} : { usage: Object.freeze({ ...usage }) }),
166
- session: current.turn === "waiting" ? "active" : current.session,
167
- turn: current.turn === "waiting" ? "accepted" : current.turn,
168
- waitingReason: undefined,
169
- waitId: undefined
170
- }), event.payload.activity, at);
165
+ ...(lifecycleActivity ? {
166
+ session: current.turn === "waiting" ? "active" : current.session,
167
+ turn: current.turn === "waiting" ? "accepted" : current.turn,
168
+ waitingReason: undefined,
169
+ waitId: undefined
170
+ } : {})
171
+ });
172
+ // Token counters are read-only measurements. Only a separate explicit
173
+ // activity observation may advance runtime health or waiting state;
174
+ // usage activityId values identify requests only for token projection.
175
+ if (!lifecycleActivity)
176
+ return projected;
177
+ return withActivity(projected, event.payload.activity, at);
171
178
  }
172
179
  case "observer.health":
173
180
  return next(current, {
@@ -321,11 +328,9 @@ export function runtimeDisplayStatus(current) {
321
328
  * Integration checkpoints), so token/tool/CPU activity can never masquerade
322
329
  * as business progress.
323
330
  *
324
- * Layers below the durable stall window are advisory: `quiet` and
325
- * `diagnostic-needed` never authorize a reset. The 30-minute semantic stall is
326
- * persisted by the scheduler and surfaced as `stalled-candidate` by the
327
- * caller; this function deliberately stops at `diagnostic-needed` so the
328
- * Leader's waiting-user/waiting-on-workers classification stays authoritative.
331
+ * Time-derived layers are advisory: `quiet` and `diagnostic-needed` never
332
+ * authorize a reset or Session switch. The scheduler owns the separate,
333
+ * coalesced 30-minute read-only diagnostic window.
329
334
  */
330
335
  export function classifyRuntimeHealth(input) {
331
336
  const policy = input.policy ?? DEFAULT_RUNTIME_HEALTH_POLICY;
@@ -380,7 +385,8 @@ function classifyLayer(current, operation, runtimeIdleMs, semanticIdleMs, policy
380
385
  if (current.turn === "waiting") {
381
386
  return `waiting-${current.waitingReason ?? "external"}`;
382
387
  }
383
- // No durable semantic progress past the diagnostic window: read-only look.
388
+ // No durable semantic progress past fifteen minutes: display only. The
389
+ // scheduler's coalesced read-only diagnostic remains a separate 30m clock.
384
390
  if (semanticIdleMs >= policy.diagnosticAfterMs)
385
391
  return "diagnostic-needed";
386
392
  // A live turn with no recent structured activity is quiet, not dead.
@@ -413,7 +419,7 @@ function runtimeHealthReason(layer, current) {
413
419
  if (current.observer.status === "degraded" || current.observer.status === "unavailable") {
414
420
  return `the runtime observer is ${current.observer.status}; read-only diagnostic recommended`;
415
421
  }
416
- return "no durable semantic progress in the diagnostic window; read-only diagnostic recommended";
422
+ return "no durable semantic checkpoint in the overdue window; the runtime remains undisturbed";
417
423
  case "quiet":
418
424
  return "the Agent turn is active but has reported no structured runtime activity recently";
419
425
  case "active-quiet":
@@ -488,22 +494,6 @@ function next(current, patch) {
488
494
  runActivity
489
495
  });
490
496
  }
491
- function usageAdvanced(previous, current) {
492
- if (current.semantics === "remaining-context")
493
- return false;
494
- if (current.semantics === "request-context")
495
- return usageTotal(current) > 0;
496
- // A first cumulative snapshot may contain history from a resumed native
497
- // Session. It establishes a baseline but cannot prove current progress.
498
- if (previous === undefined || previous.semantics !== "cumulative-session")
499
- return false;
500
- return usageTotal(current) > usageTotal(previous);
501
- }
502
- function usageTotal(usage) {
503
- // cachedInputTokens and reasoningTokens are breakdowns of the normalized
504
- // input/output totals, not additional usage.
505
- return usage.inputTokens + usage.outputTokens;
506
- }
507
497
  function dominantOperation(operations) {
508
498
  const kinds = Object.values(operations).map(({ kind }) => kind);
509
499
  if (kinds.includes("subagent"))