@zq-silk/yui 0.13.4 → 0.13.6
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.
- package/ARCHITECTURE.md +13 -13
- package/README.md +19 -20
- package/dist/cli/commandCatalog.js +22 -21
- package/dist/cli/interactionPolicy.js +0 -14
- package/dist/cli.js +42 -0
- package/dist/commands/executionAuditCommands.js +1 -1
- package/dist/commands/taskCommands.js +60 -326
- package/dist/commands/taskContextCommand.js +3 -14
- package/dist/commands/taskExecutionCommands.js +254 -0
- package/dist/commands/taskNextActionCommand.js +1 -3
- package/dist/commands/taskOverviewCommand.js +9 -2
- package/dist/commands/taskRoleRuntimeStatus.js +2 -25
- package/dist/controller/agentRuntimeObserver.js +4 -2
- package/dist/controller/clientRuntime.js +45 -2
- package/dist/controller/controller.js +6 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +59 -188
- package/dist/controller/jobControl.js +3 -2
- package/dist/controller/runtime.js +6 -33
- package/dist/controller/runtimeEventProcessor.js +8 -4
- package/dist/controller/runtimeHookRunFence.js +4 -10
- package/dist/execution/executionHealth.js +8 -16
- package/dist/executor/agentAdapter.js +3 -8
- package/dist/executor/agentExecutor.js +13 -14
- package/dist/executor/fileRoleLaunchPlanner.js +10 -26
- package/dist/lifecycle/exactRunTerminalization.js +24 -322
- package/dist/repository/taskWorkspaceCoordinator.js +0 -9
- package/dist/runtime/agentHost.js +22 -83
- package/dist/runtime/builtinAgentDrivers.js +1 -1
- package/dist/runtime/exactControlPlane.js +15 -9
- package/dist/runtime/launchBroker.js +1 -11
- package/dist/runtime/providerContinuationReconciliationService.js +1 -1
- package/dist/runtime/providerRecoveryDecision.js +1 -1
- package/dist/runtime/providerRuntimeIdentity.js +25 -15
- package/dist/runtime/structuredProviderHost.js +0 -57
- package/dist/scheduler/activeRoleRunDelivery.js +1 -19
- package/dist/scheduler/leaderWakeupProcessor.js +12 -63
- package/dist/scheduler/ports.js +3 -2
- package/dist/scheduler/roleRunLiveness.js +4 -1
- package/dist/scheduler/roleRunStall.js +0 -2
- package/dist/scheduler/taskExecutionProjection.js +18 -1
- package/dist/scheduler/wakeupQueue.js +2 -1
- package/dist/storage/migration/productionRegistry.js +65 -0
- package/dist/storage/sqliteStore.js +10 -2
- package/dist/storage/taskStore.js +11 -3
- package/dist/task/completionReadiness.js +0 -67
- package/dist/task/nextAction.js +16 -32
- package/dist/task/task.js +38 -3
- package/dist/web/assets/client/i18n.js +0 -4
- package/dist/web/assets/client/view.js +0 -18
- package/dist/web/webSnapshot.js +7 -13
- package/i18n/README.zh-CN.md +4 -4
- package/package.json +1 -1
- package/dist/run/recoveryProjection.js +0 -252
- package/dist/runtime/conversationSwitch.js +0 -277
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import { retireConfirmedAbsentInactiveTaskRolePlaceholders } from "../executor/agentExecutor.js";
|
|
3
3
|
import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
4
|
-
import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
5
4
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
6
5
|
import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
|
|
7
6
|
import { WorkspaceCleanupBlockedError } from "./taskWorkspacePreparer.js";
|
|
@@ -159,7 +158,6 @@ export class TaskWorkspaceCoordinator {
|
|
|
159
158
|
if (task.status !== "completed" && task.status !== "retired") {
|
|
160
159
|
throw new Error(`Task must be completed or retired before archive cleanup: ${task.id}.`);
|
|
161
160
|
}
|
|
162
|
-
this.#assertNoProviderContinuationWriters(task.id);
|
|
163
161
|
const managedWorkspaces = [...this.store.listManagedWorkspaces(task.id)]
|
|
164
162
|
.sort((left, right) => managedWorkspaceKey(left.owner)
|
|
165
163
|
.localeCompare(managedWorkspaceKey(right.owner)));
|
|
@@ -326,13 +324,6 @@ export class TaskWorkspaceCoordinator {
|
|
|
326
324
|
if (current === null || !isDeepStrictEqual(current, expected)) {
|
|
327
325
|
throw new WorkspaceCleanupBlockedError("task-changed", `task:${expected.id}`, true, `Task changed during archive cleanup: ${expected.id}.`);
|
|
328
326
|
}
|
|
329
|
-
this.#assertNoProviderContinuationWriters(expected.id);
|
|
330
|
-
}
|
|
331
|
-
#assertNoProviderContinuationWriters(taskId) {
|
|
332
|
-
const blockers = blockingProviderContinuations(this.store.listEvents(taskId));
|
|
333
|
-
if (blockers.length > 0) {
|
|
334
|
-
throw new WorkspaceCleanupBlockedError("active-run", `task:${taskId}`, true, `Task has Provider continuations that may still write its Workspace: ${taskId}.`);
|
|
335
|
-
}
|
|
336
327
|
}
|
|
337
328
|
#assertWorkItemRuntimeQuiescent(item) {
|
|
338
329
|
const activeRun = this.store.listAgentRuns(item.taskId)
|
|
@@ -14,7 +14,7 @@ import { ProviderDeliveryUnknownError, ProviderConversationMissingError, Provide
|
|
|
14
14
|
import { sameProviderAuthorityFence, validateProviderAuthorityFence } from "./providerAuthorityFence.js";
|
|
15
15
|
import { validateRuntimeProcessExitObservation } from "./processExitObservation.js";
|
|
16
16
|
import { persistRuntimeProcessExitObservation, replayRuntimeProcessExitOutbox } from "./processExitOutbox.js";
|
|
17
|
-
import { readRuntimeStopReceipt, removeRuntimeStopReceipt
|
|
17
|
+
import { readRuntimeStopReceipt, removeRuntimeStopReceipt } from "./runtimeStopReceipt.js";
|
|
18
18
|
import { AGENT_HOST_CONTROL_TIMEOUT_MS, AGENT_HOST_READY_TIMEOUT_MS } from "./runtimeDeadlines.js";
|
|
19
19
|
export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v2";
|
|
20
20
|
const HOST_CONTROL_MAX_BYTES = 32 * 1024;
|
|
@@ -34,7 +34,6 @@ export async function runAgentHost(input) {
|
|
|
34
34
|
let activeNativeTurnId;
|
|
35
35
|
let codexClientAttachedAt;
|
|
36
36
|
let consecutiveCodexDisconnects = 0;
|
|
37
|
-
const switchDetachedSessions = new WeakSet();
|
|
38
37
|
let activationId;
|
|
39
38
|
let conversationRecoverability = "unknown";
|
|
40
39
|
let authority;
|
|
@@ -146,7 +145,7 @@ export async function runAgentHost(input) {
|
|
|
146
145
|
providerControl: {
|
|
147
146
|
schemaVersion: 1,
|
|
148
147
|
adapterId: "codex",
|
|
149
|
-
transport: "codex-app-server
|
|
148
|
+
transport: "codex-app-server",
|
|
150
149
|
kind: "ensure",
|
|
151
150
|
mode: "resume",
|
|
152
151
|
nativeSessionId: disconnectedSession.nativeSessionId,
|
|
@@ -154,7 +153,6 @@ export async function runAgentHost(input) {
|
|
|
154
153
|
? {}
|
|
155
154
|
: { sessionTitle: previousControl.sessionTitle }),
|
|
156
155
|
codexThread: previousControl.codexThread,
|
|
157
|
-
codexDaemonStartArgs: previousControl.codexDaemonStartArgs,
|
|
158
156
|
...(ownedTurn === undefined ? {} : { ownedTurn }),
|
|
159
157
|
authority
|
|
160
158
|
}
|
|
@@ -228,8 +226,7 @@ export async function runAgentHost(input) {
|
|
|
228
226
|
const reconnectableCodexClient = ownsCurrentSession
|
|
229
227
|
&& providerSession.adapterId === "codex"
|
|
230
228
|
&& !hostStopRequested
|
|
231
|
-
&& stopReceipt === null
|
|
232
|
-
&& !switchDetachedSessions.has(providerSession);
|
|
229
|
+
&& stopReceipt === null;
|
|
233
230
|
if (reconnectableCodexClient) {
|
|
234
231
|
session = undefined;
|
|
235
232
|
if (codexClientAttachedAt !== undefined
|
|
@@ -260,7 +257,7 @@ export async function runAgentHost(input) {
|
|
|
260
257
|
...(activeTurnAttemptId === undefined ? {} : { attemptId: activeTurnAttemptId }),
|
|
261
258
|
...(activeNativeTurnId === undefined ? {} : { nativeTurnId: activeNativeTurnId }),
|
|
262
259
|
...exitAuthority,
|
|
263
|
-
detail: "Codex
|
|
260
|
+
detail: "Codex App Server disconnected; starting a replacement process."
|
|
264
261
|
}));
|
|
265
262
|
await reconnectCodexClient(providerSession, currentPayload);
|
|
266
263
|
return;
|
|
@@ -274,21 +271,19 @@ export async function runAgentHost(input) {
|
|
|
274
271
|
hostSequence += 1;
|
|
275
272
|
const observedAt = new Date().toISOString();
|
|
276
273
|
const failures = [];
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
failures.push(`activation terminal: ${errorText(error)}`);
|
|
291
|
-
}
|
|
274
|
+
try {
|
|
275
|
+
await publishStructuredProviderActivationTerminal({
|
|
276
|
+
home: input.home,
|
|
277
|
+
environment: currentPayload.environment,
|
|
278
|
+
conversationId: providerSession.conversationId,
|
|
279
|
+
nativeSessionId: providerSession.nativeSessionId,
|
|
280
|
+
activationId: currentActivationId,
|
|
281
|
+
status: stopReceipt !== null || hostStopRequested ? "ended" : "failed",
|
|
282
|
+
observedAt
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
failures.push(`activation terminal: ${errorText(error)}`);
|
|
292
287
|
}
|
|
293
288
|
let exitPersisted = false;
|
|
294
289
|
try {
|
|
@@ -357,10 +352,12 @@ export async function runAgentHost(input) {
|
|
|
357
352
|
}
|
|
358
353
|
const requestedAuthority = validateProviderAuthorityFence(providerControl.authority);
|
|
359
354
|
const replacesCurrentConversation = session !== undefined && providerControl.kind === "new";
|
|
360
|
-
if (
|
|
355
|
+
if (replacesCurrentConversation) {
|
|
356
|
+
throw new Error("Agent Host cannot replace a live Provider Session; stop it before starting a fresh Session.");
|
|
357
|
+
}
|
|
358
|
+
if (authority === undefined)
|
|
361
359
|
authority = requestedAuthority;
|
|
362
|
-
else if (
|
|
363
|
-
&& authority !== undefined
|
|
360
|
+
else if (authority !== undefined
|
|
364
361
|
&& !sameProviderAuthorityFence(authority, requestedAuthority)) {
|
|
365
362
|
throw new Error("Agent Host launch carries a stale Provider authority fence.");
|
|
366
363
|
}
|
|
@@ -380,43 +377,6 @@ export async function runAgentHost(input) {
|
|
|
380
377
|
let providerAcceptedAttemptId;
|
|
381
378
|
let durableInitialTurn;
|
|
382
379
|
try {
|
|
383
|
-
if (replacesCurrentConversation) {
|
|
384
|
-
const previousSession = session;
|
|
385
|
-
const previousPayload = sessionPayload;
|
|
386
|
-
const previousActivationId = activationId ?? previousPayload?.launchId;
|
|
387
|
-
if (previousPayload === undefined || previousActivationId === undefined
|
|
388
|
-
|| authority?.owner !== "controller") {
|
|
389
|
-
throw new Error("Agent Host cannot detach an inexact Provider Activation.");
|
|
390
|
-
}
|
|
391
|
-
await detachDurableProviderForConversationSwitch(input.home, {
|
|
392
|
-
taskId: requiredEnvironment(next.environment.YUI_TASK_ID, "Task id"),
|
|
393
|
-
roleName: requiredEnvironment(next.environment.YUI_ROLE, "Role name"),
|
|
394
|
-
runId: requiredEnvironment(next.environment.YUI_RUN_ID, "Run id"),
|
|
395
|
-
agentId: requiredEnvironment(next.environment.YUI_AGENT_ID, "Agent id"),
|
|
396
|
-
launchId: next.launchId,
|
|
397
|
-
previousConversationId: previousSession.conversationId,
|
|
398
|
-
previousNativeSessionId: previousSession.nativeSessionId,
|
|
399
|
-
previousActivationId,
|
|
400
|
-
nextAuthorityEpoch: requestedAuthority.epoch,
|
|
401
|
-
nextAuthorityHolderId: requestedAuthority.holderId,
|
|
402
|
-
observedAt: new Date().toISOString()
|
|
403
|
-
});
|
|
404
|
-
switchDetachedSessions.add(previousSession);
|
|
405
|
-
// The persistent Provider child belongs to the Activation that first
|
|
406
|
-
// created it, not to the most recent resume Run payload. The exit
|
|
407
|
-
// observer carries that Activation launch id, so the stop receipt must
|
|
408
|
-
// use the same identity or the expected switch detach is misclassified
|
|
409
|
-
// as an abnormal child exit and generic cleanup can kill the Host.
|
|
410
|
-
writeRuntimeStopReceipt(input.home, previousActivationId, new Date());
|
|
411
|
-
authority = undefined;
|
|
412
|
-
await terminateProviderSessionForConversationSwitch(previousSession);
|
|
413
|
-
if (session === previousSession)
|
|
414
|
-
session = undefined;
|
|
415
|
-
sessionPayload = undefined;
|
|
416
|
-
activationId = undefined;
|
|
417
|
-
conversationRecoverability = "unknown";
|
|
418
|
-
authority = requestedAuthority;
|
|
419
|
-
}
|
|
420
380
|
if (session !== undefined) {
|
|
421
381
|
if (session.adapterId !== providerControl.adapterId
|
|
422
382
|
|| providerControl.mode !== "resume"
|
|
@@ -1123,9 +1083,6 @@ async function beginDurableProviderTurn(home, durableTurn) {
|
|
|
1123
1083
|
throw error;
|
|
1124
1084
|
}
|
|
1125
1085
|
}
|
|
1126
|
-
async function detachDurableProviderForConversationSwitch(home, request) {
|
|
1127
|
-
await callControllerIdempotently(home, "runtime.conversation-switch-detach", request);
|
|
1128
|
-
}
|
|
1129
1086
|
async function callControllerIdempotently(home, method, request) {
|
|
1130
1087
|
try {
|
|
1131
1088
|
await callAgentController(home, method, request);
|
|
@@ -1152,24 +1109,6 @@ async function callControllerIdempotently(home, method, request) {
|
|
|
1152
1109
|
class ControllerAcknowledgementUnknownError extends Error {
|
|
1153
1110
|
name = "ControllerAcknowledgementUnknownError";
|
|
1154
1111
|
}
|
|
1155
|
-
async function terminateProviderSessionForConversationSwitch(providerSession) {
|
|
1156
|
-
providerSession.terminate("SIGTERM");
|
|
1157
|
-
const forceKill = setTimeout(() => providerSession.terminate("SIGKILL"), 3_000);
|
|
1158
|
-
forceKill.unref();
|
|
1159
|
-
let hardTimeout;
|
|
1160
|
-
const timeout = new Promise((_resolve, reject) => {
|
|
1161
|
-
hardTimeout = setTimeout(() => reject(new Error("Old Provider Activation did not exit after its switch authority was revoked.")), 8_000);
|
|
1162
|
-
hardTimeout.unref();
|
|
1163
|
-
});
|
|
1164
|
-
try {
|
|
1165
|
-
await Promise.race([providerSession.waitForExit(), timeout]);
|
|
1166
|
-
}
|
|
1167
|
-
finally {
|
|
1168
|
-
clearTimeout(forceKill);
|
|
1169
|
-
if (hardTimeout !== undefined)
|
|
1170
|
-
clearTimeout(hardTimeout);
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
1112
|
async function resolveProviderTurnSubmission(home, durableTurn, error) {
|
|
1174
1113
|
const attemptId = durableTurn.attemptId;
|
|
1175
1114
|
if (typeof attemptId !== "string") {
|
|
@@ -121,7 +121,7 @@ export const BUILTIN_AGENT_DRIVERS = Object.freeze([
|
|
|
121
121
|
}),
|
|
122
122
|
observation: Object.freeze({
|
|
123
123
|
...STRUCTURED_CLI_CAPABILITIES.observation,
|
|
124
|
-
// Managed Codex uses
|
|
124
|
+
// Managed Codex uses its Yui-owned App Server event stream.
|
|
125
125
|
// Turn lifecycle is exact; Yui does not install per-thread Hooks merely
|
|
126
126
|
// to manufacture tool/wait/usage observations.
|
|
127
127
|
operations: Object.freeze([]),
|
|
@@ -268,7 +268,9 @@ export function assertExactTaskRuntimeEnvironment(runtimeSource, environment, ex
|
|
|
268
268
|
/** Fences a descriptor to the one currently active durable Task runtime. */
|
|
269
269
|
export function assertExactTaskRuntimeState(runtime, store, options = {}) {
|
|
270
270
|
const task = store.getTask(runtime.taskId);
|
|
271
|
-
if (task === null
|
|
271
|
+
if (task === null
|
|
272
|
+
|| task.status !== "active"
|
|
273
|
+
|| task.executionGate.state !== "enabled") {
|
|
272
274
|
throw new Error("Exact Task runtime Task is not current and active.");
|
|
273
275
|
}
|
|
274
276
|
const role = store.getRole(runtime.taskId, runtime.roleName);
|
|
@@ -304,17 +306,21 @@ export function assertExactTaskRuntimeState(runtime, store, options = {}) {
|
|
|
304
306
|
&& session?.launchId === runtime.launchId;
|
|
305
307
|
const executionRef = lifecycleMailbox?.processing?.executionRef;
|
|
306
308
|
const preallocated = options.preallocatedDriverSessionReservation;
|
|
307
|
-
const
|
|
308
|
-
&& runtime.adapterId === preallocated.adapterId
|
|
309
|
-
&& runtime.runId !== undefined
|
|
309
|
+
const exactRunLaunchReservation = runtime.runId !== undefined
|
|
310
310
|
&& runtime.launchId !== undefined
|
|
311
|
-
&& runtime.nativeSessionId !== undefined
|
|
312
|
-
&& session === undefined
|
|
313
311
|
&& reservation
|
|
314
312
|
&& !hasRuntimeCleanupObligation(lifecycleMailbox)
|
|
315
313
|
&& executionRef?.type === "run"
|
|
316
314
|
&& executionRef.taskId === runtime.taskId
|
|
317
|
-
&& executionRef.id === runtime.runId
|
|
315
|
+
&& executionRef.id === runtime.runId;
|
|
316
|
+
const terminalSessionReplacementReservation = exactRunLaunchReservation
|
|
317
|
+
&& session !== undefined
|
|
318
|
+
&& (session.status === "stopped" || session.status === "broken");
|
|
319
|
+
const exactPreallocatedReservation = preallocated !== undefined
|
|
320
|
+
&& runtime.adapterId === preallocated.adapterId
|
|
321
|
+
&& exactRunLaunchReservation
|
|
322
|
+
&& runtime.nativeSessionId !== undefined
|
|
323
|
+
&& (session === undefined || terminalSessionReplacementReservation)
|
|
318
324
|
&& runtime.nativeSessionId === nativeSessionIdForLaunch(preallocated.yuiHome, runtime.launchId, runtime.agentId, runtime.adapterId);
|
|
319
325
|
const preallocatedBeforeInFlightProjection = exactPreallocatedReservation
|
|
320
326
|
&& (sessions?.inFlight === null || sessions?.inFlight === undefined);
|
|
@@ -327,11 +333,11 @@ export function assertExactTaskRuntimeState(runtime, store, options = {}) {
|
|
|
327
333
|
throw new Error("Exact Task runtime launch fence is not current.");
|
|
328
334
|
}
|
|
329
335
|
if (runtime.nativeSessionId === undefined) {
|
|
330
|
-
if (session !== undefined) {
|
|
336
|
+
if (session !== undefined && !terminalSessionReplacementReservation) {
|
|
331
337
|
throw new Error("Exact Task runtime native Session fence is missing.");
|
|
332
338
|
}
|
|
333
339
|
}
|
|
334
|
-
else if (session === undefined) {
|
|
340
|
+
else if (session === undefined || terminalSessionReplacementReservation) {
|
|
335
341
|
if (!exactPreallocatedReservation) {
|
|
336
342
|
throw new Error("Exact Task runtime native Session fence is not current.");
|
|
337
343
|
}
|
|
@@ -86,25 +86,15 @@ function validateProviderControl(control) {
|
|
|
86
86
|
if (control.adapterId !== "codex" && control.adapterId !== "claude") {
|
|
87
87
|
throw new Error("Agent Host Provider control adapter is invalid.");
|
|
88
88
|
}
|
|
89
|
-
if ((control.adapterId === "codex" && control.transport !== "codex-app-server
|
|
89
|
+
if ((control.adapterId === "codex" && control.transport !== "codex-app-server")
|
|
90
90
|
|| (control.adapterId === "claude" && control.transport !== "claude-stream-json")) {
|
|
91
91
|
throw new Error("Agent Host Provider control transport does not match its adapter.");
|
|
92
92
|
}
|
|
93
93
|
if ((control.adapterId === "codex") !== (control.codexThread !== undefined)) {
|
|
94
94
|
throw new Error("Agent Host Provider thread settings do not match its adapter.");
|
|
95
95
|
}
|
|
96
|
-
if ((control.adapterId === "codex") !== (control.codexDaemonStartArgs !== undefined)) {
|
|
97
|
-
throw new Error("Agent Host Provider daemon bootstrap does not match its adapter.");
|
|
98
|
-
}
|
|
99
96
|
if (control.codexThread !== undefined)
|
|
100
97
|
validateCodexThreadOptions(control.codexThread);
|
|
101
|
-
if (control.codexDaemonStartArgs !== undefined) {
|
|
102
|
-
if (!Array.isArray(control.codexDaemonStartArgs)
|
|
103
|
-
|| control.codexDaemonStartArgs.length === 0) {
|
|
104
|
-
throw new Error("Agent Host Codex daemon bootstrap args are invalid.");
|
|
105
|
-
}
|
|
106
|
-
control.codexDaemonStartArgs.forEach((value) => text(value, "Codex daemon argument"));
|
|
107
|
-
}
|
|
108
98
|
if (control.mode !== "new" && control.mode !== "resume") {
|
|
109
99
|
throw new Error("Agent Host Provider control mode is invalid.");
|
|
110
100
|
}
|
|
@@ -20,7 +20,7 @@ export class ProviderContinuationReconciliationService {
|
|
|
20
20
|
}
|
|
21
21
|
async reconcile(now) {
|
|
22
22
|
const changedTaskIds = new Set();
|
|
23
|
-
for (const task of this.store.listTasks().filter((entry) => entry.status === "active")) {
|
|
23
|
+
for (const task of this.store.listTasks().filter((entry) => (entry.status === "active" && entry.executionGate.state === "enabled"))) {
|
|
24
24
|
const events = this.store.listEvents(task.id);
|
|
25
25
|
const groups = groupDetachedContinuations(projectProviderContinuations(events), events.map(runtimeObservationFromTaskEvent)
|
|
26
26
|
.filter((entry) => entry !== null));
|
|
@@ -47,7 +47,7 @@ export function decideProviderRecovery(input) {
|
|
|
47
47
|
reason: "Provider Conversation is missing but its Activation writer has not ended."
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
return { action: "
|
|
50
|
+
return { action: "restart-run", conversationId: conversation.conversationId };
|
|
51
51
|
}
|
|
52
52
|
function providerTurnIsUnsettled(binding) {
|
|
53
53
|
return binding.turn !== null
|
|
@@ -294,21 +294,30 @@ export function updateProviderConversationRecoverability(raw, recoverability) {
|
|
|
294
294
|
export function supersedeProviderConversation(raw, input) {
|
|
295
295
|
const binding = validateProviderRuntimeBinding(raw);
|
|
296
296
|
const current = currentProviderConversation(binding);
|
|
297
|
-
const basis = input.basis
|
|
298
|
-
if (basis !== "
|
|
299
|
-
throw new Error("Provider Conversation
|
|
297
|
+
const basis = input.basis;
|
|
298
|
+
if (basis !== "terminal-session") {
|
|
299
|
+
throw new Error("Provider Conversation replacement basis is invalid.");
|
|
300
300
|
}
|
|
301
|
-
|
|
302
|
-
throw new Error("Current Provider Conversation is not exactly unrecoverable.");
|
|
303
|
-
}
|
|
304
|
-
if (!input.noUnsettledInputDelivery) {
|
|
305
|
-
throw new Error("Cannot replace a Provider Conversation with unsettled input delivery.");
|
|
306
|
-
}
|
|
307
|
-
if (binding.authority.owner !== "none" || currentProviderActivation(binding) !== null) {
|
|
308
|
-
throw new Error("Cannot replace a Provider Conversation while its writer umbrella is owned.");
|
|
309
|
-
}
|
|
310
|
-
const switchedAt = timestamp(input.switchedAt, "Provider Conversation switch timestamp");
|
|
301
|
+
const switchedAt = timestamp(input.switchedAt, "Provider Conversation replacement timestamp");
|
|
311
302
|
const epoch = current.epoch + 1;
|
|
303
|
+
const terminalReason = "terminal-session-replaced";
|
|
304
|
+
const activations = binding.activations.map((entry) => entry.status === "active"
|
|
305
|
+
? {
|
|
306
|
+
...entry,
|
|
307
|
+
status: "failed",
|
|
308
|
+
endedAt: switchedAt,
|
|
309
|
+
terminalReason
|
|
310
|
+
}
|
|
311
|
+
: entry);
|
|
312
|
+
const turn = binding.turn !== null
|
|
313
|
+
&& ["submitting", "accepted", "running", "delivery-unknown"].includes(binding.turn.status)
|
|
314
|
+
? {
|
|
315
|
+
...binding.turn,
|
|
316
|
+
status: binding.turn.turnId === undefined ? "rejected" : "failed",
|
|
317
|
+
updatedAt: switchedAt,
|
|
318
|
+
terminalReason
|
|
319
|
+
}
|
|
320
|
+
: binding.turn;
|
|
312
321
|
return validateProviderRuntimeBinding({
|
|
313
322
|
...binding,
|
|
314
323
|
currentConversationEpoch: epoch,
|
|
@@ -324,7 +333,7 @@ export function supersedeProviderConversation(raw, input) {
|
|
|
324
333
|
createdAt: switchedAt
|
|
325
334
|
}
|
|
326
335
|
],
|
|
327
|
-
activations: [...
|
|
336
|
+
activations: [...activations, {
|
|
328
337
|
activationId: identity(input.activationId, "Provider Activation id"),
|
|
329
338
|
conversationId: input.conversationId,
|
|
330
339
|
generation: 1,
|
|
@@ -336,7 +345,8 @@ export function supersedeProviderConversation(raw, input) {
|
|
|
336
345
|
owner: "controller",
|
|
337
346
|
holderId: input.activationId,
|
|
338
347
|
changedAt: switchedAt
|
|
339
|
-
}
|
|
348
|
+
},
|
|
349
|
+
turn
|
|
340
350
|
});
|
|
341
351
|
}
|
|
342
352
|
export function validateProviderRuntimeBinding(value) {
|
|
@@ -4,8 +4,6 @@ import { CodexAppServerRequestError, CodexAppServerRuntime, codexAppServerErrorI
|
|
|
4
4
|
import { PROVIDER_ACCEPT_TIMEOUT_MS } from "./runtimeDeadlines.js";
|
|
5
5
|
import { YUI_VERSION } from "../version.js";
|
|
6
6
|
const PROVIDER_MESSAGE_MAX_BYTES = 16 * 1024 * 1024;
|
|
7
|
-
const CODEX_DAEMON_START_TIMEOUT_MS = 10_000;
|
|
8
|
-
const CODEX_DAEMON_OUTPUT_MAX_BYTES = 64 * 1024;
|
|
9
7
|
export class ProviderDeliveryUnknownError extends Error {
|
|
10
8
|
attemptId;
|
|
11
9
|
name = "ProviderDeliveryUnknownError";
|
|
@@ -46,9 +44,6 @@ export async function startStructuredProviderSession(payload, input = {}) {
|
|
|
46
44
|
if (control === undefined) {
|
|
47
45
|
throw new Error("Managed Agent Host launch requires Provider control metadata.");
|
|
48
46
|
}
|
|
49
|
-
if (control.adapterId === "codex") {
|
|
50
|
-
await ensureCodexAppServerDaemon(payload, control);
|
|
51
|
-
}
|
|
52
47
|
const child = spawn(payload.command, [...payload.args], {
|
|
53
48
|
cwd: payload.cwd,
|
|
54
49
|
env: { ...payload.environment },
|
|
@@ -78,58 +73,6 @@ export async function startStructuredProviderSession(payload, input = {}) {
|
|
|
78
73
|
throw error;
|
|
79
74
|
}
|
|
80
75
|
}
|
|
81
|
-
async function ensureCodexAppServerDaemon(payload, control) {
|
|
82
|
-
if (payload.args.at(-2) !== "app-server" || payload.args.at(-1) !== "proxy")
|
|
83
|
-
return;
|
|
84
|
-
if (control.adapterId !== "codex")
|
|
85
|
-
return;
|
|
86
|
-
const environment = Object.fromEntries(Object.entries(payload.environment).filter(([key]) => (!key.startsWith("YUI_")
|
|
87
|
-
&& key !== "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"
|
|
88
|
-
&& !["TMPDIR", "XDG_CACHE_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_RUNTIME_DIR"]
|
|
89
|
-
.includes(key))));
|
|
90
|
-
const child = spawn(payload.command, [...control.codexDaemonStartArgs], {
|
|
91
|
-
cwd: payload.cwd,
|
|
92
|
-
env: environment,
|
|
93
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
94
|
-
});
|
|
95
|
-
await new Promise((resolvePromise, reject) => {
|
|
96
|
-
let output = "";
|
|
97
|
-
let settled = false;
|
|
98
|
-
let timer;
|
|
99
|
-
const settle = (error) => {
|
|
100
|
-
if (settled)
|
|
101
|
-
return;
|
|
102
|
-
settled = true;
|
|
103
|
-
if (timer !== undefined)
|
|
104
|
-
clearTimeout(timer);
|
|
105
|
-
if (error === undefined)
|
|
106
|
-
resolvePromise();
|
|
107
|
-
else
|
|
108
|
-
reject(error);
|
|
109
|
-
};
|
|
110
|
-
const capture = (chunk) => {
|
|
111
|
-
output += String(chunk);
|
|
112
|
-
if (Buffer.byteLength(output, "utf8") <= CODEX_DAEMON_OUTPUT_MAX_BYTES)
|
|
113
|
-
return;
|
|
114
|
-
child.kill("SIGTERM");
|
|
115
|
-
settle(new Error("Codex App Server daemon start output exceeded its bound."));
|
|
116
|
-
};
|
|
117
|
-
child.stdout.on("data", capture);
|
|
118
|
-
child.stderr.on("data", capture);
|
|
119
|
-
child.once("error", (error) => settle(error));
|
|
120
|
-
child.once("close", (code, signal) => {
|
|
121
|
-
if (code === 0)
|
|
122
|
-
settle();
|
|
123
|
-
else
|
|
124
|
-
settle(new Error(`Codex App Server daemon start failed (${code ?? signal ?? "unknown"})${output.trim().length === 0 ? "" : `: ${output.trim()}`}`));
|
|
125
|
-
});
|
|
126
|
-
timer = setTimeout(() => {
|
|
127
|
-
child.kill("SIGTERM");
|
|
128
|
-
settle(new Error("Codex App Server daemon start timed out."));
|
|
129
|
-
}, CODEX_DAEMON_START_TIMEOUT_MS);
|
|
130
|
-
timer.unref();
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
76
|
class JsonLineChannel {
|
|
134
77
|
child;
|
|
135
78
|
mirror;
|
|
@@ -8,7 +8,7 @@ import { RuntimeLaunchFailure } from "../runtime/launchDiagnostics.js";
|
|
|
8
8
|
import { mailboxHasWork, nextPendingBatch } from "../coordination/workMailbox.js";
|
|
9
9
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
10
10
|
import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
11
|
-
import {
|
|
11
|
+
import { RuntimeLifecycleBusyError } from "../runtime/lifecycleReservation.js";
|
|
12
12
|
/**
|
|
13
13
|
* Delivers durable Work AgentRuns before liveness reconciliation. Task command
|
|
14
14
|
* handlers only record intent; this Controller path is the sole automated
|
|
@@ -48,24 +48,6 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
|
|
|
48
48
|
});
|
|
49
49
|
continue;
|
|
50
50
|
}
|
|
51
|
-
// Single-flight: a Role runtime lifecycle lane that already holds a
|
|
52
|
-
// launch reservation or cleanup obligation must not be entered by a
|
|
53
|
-
// second delivery. The Run stays active-unpushed and is retried after
|
|
54
|
-
// the lane settles; the contention is never terminalized as a failure.
|
|
55
|
-
if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget({
|
|
56
|
-
scope: "task",
|
|
57
|
-
taskId: task.id,
|
|
58
|
-
roleName: role.name
|
|
59
|
-
})))) {
|
|
60
|
-
results.push({
|
|
61
|
-
taskId: task.id,
|
|
62
|
-
roleName: role.name,
|
|
63
|
-
runId: run.id,
|
|
64
|
-
status: "skipped",
|
|
65
|
-
reason: "runtime-unavailable"
|
|
66
|
-
});
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
51
|
const existingSession = store.getRoleSession(task.id, role.name, run.effective.agentId);
|
|
70
52
|
const receiptId = agentRunDeliveryReceiptId(run);
|
|
71
53
|
const target = { kind: "role", taskId: task.id, roleName: role.name };
|
|
@@ -2,11 +2,11 @@ import { createAgentRun } from "../run/agentRun.js";
|
|
|
2
2
|
import { createRunAssignment } from "../context/runContextContract.js";
|
|
3
3
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
4
4
|
import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskSession } from "../executor/effectiveLaunch.js";
|
|
5
|
-
import {
|
|
5
|
+
import { roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
|
|
6
|
+
import { RuntimeLifecycleBusyError } from "../runtime/lifecycleReservation.js";
|
|
6
7
|
import { recordLeaderFailure } from "./leaderFailure.js";
|
|
7
8
|
import { isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
8
9
|
import { RuntimeLaunchError } from "../runtime/ports.js";
|
|
9
|
-
import { roleSessionDispatchModeWithConversationSwitch } from "../runtime/conversationSwitch.js";
|
|
10
10
|
export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
11
11
|
const results = [];
|
|
12
12
|
const wakeups = selection === undefined || selection.full
|
|
@@ -20,7 +20,10 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
20
20
|
for (const wakeup of wakeups) {
|
|
21
21
|
const task = store.getTask(wakeup.taskId);
|
|
22
22
|
const role = store.getRole(wakeup.taskId, "leader");
|
|
23
|
-
if (task === null
|
|
23
|
+
if (task === null
|
|
24
|
+
|| task.status !== "active"
|
|
25
|
+
|| task.executionGate.state !== "enabled"
|
|
26
|
+
|| role === null) {
|
|
24
27
|
results.push({ taskId: wakeup.taskId, status: "skipped", reason: "unavailable" });
|
|
25
28
|
continue;
|
|
26
29
|
}
|
|
@@ -29,10 +32,6 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
29
32
|
results.push({ taskId: task.id, status: "skipped", reason: "workspace-not-ready" });
|
|
30
33
|
continue;
|
|
31
34
|
}
|
|
32
|
-
if (store.getLeaderFailure(task.id) !== null) {
|
|
33
|
-
results.push({ taskId: task.id, status: "skipped", reason: "recovery-blocked" });
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
35
|
if (store.hasOpenInputRequest(task.id)) {
|
|
37
36
|
results.push({ taskId: task.id, status: "skipped", reason: "waiting-input" });
|
|
38
37
|
continue;
|
|
@@ -48,20 +47,6 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
48
47
|
results.push({ taskId: task.id, status: "skipped", reason: "busy" });
|
|
49
48
|
continue;
|
|
50
49
|
}
|
|
51
|
-
// Single-flight: a Role runtime lifecycle lane that already holds a
|
|
52
|
-
// launch reservation or cleanup obligation must not be entered by a
|
|
53
|
-
// second wake. The wake stays durable (pendingWakeup is not consumed) and
|
|
54
|
-
// is retried after the lane settles; the suppression is recorded for the
|
|
55
|
-
// audit instead of manufacturing a failed Run.
|
|
56
|
-
if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget({
|
|
57
|
-
scope: "task",
|
|
58
|
-
taskId: task.id,
|
|
59
|
-
roleName: role.name
|
|
60
|
-
})))) {
|
|
61
|
-
store.recordWakeSuppression?.(task.id, "lifecycle-busy", now);
|
|
62
|
-
results.push({ taskId: task.id, status: "skipped", reason: "recovery-blocked" });
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
50
|
const reopening = wakeup.reasons.includes("task-reopened");
|
|
66
51
|
let existingSession = store.getRoleSession(task.id, role.name, reopening ? undefined : role.effective.agentId);
|
|
67
52
|
let effectiveSession = existingSession;
|
|
@@ -71,43 +56,6 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
71
56
|
let prepared;
|
|
72
57
|
let preStartFencePersisted = false;
|
|
73
58
|
try {
|
|
74
|
-
if (existingSession !== null && !hasNativeSession(existingSession)) {
|
|
75
|
-
const sessionIsTerminal = existingSession.status === "stopped"
|
|
76
|
-
|| existingSession.status === "broken";
|
|
77
|
-
if (!sessionIsTerminal) {
|
|
78
|
-
// An opaque live Session has no provider identity that can be safely
|
|
79
|
-
// rebound. A host absence observation is not a verified stop; keep
|
|
80
|
-
// the wake durable until an explicit exact cleanup/reset settles it.
|
|
81
|
-
if (existingSession.launchId !== undefined) {
|
|
82
|
-
await delivery.inspectRole({
|
|
83
|
-
taskId: task.id,
|
|
84
|
-
roleName: role.name,
|
|
85
|
-
agentId: existingSession.agentId,
|
|
86
|
-
adapterId: existingSession.adapterId
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
results.push({
|
|
90
|
-
taskId: task.id,
|
|
91
|
-
status: "skipped",
|
|
92
|
-
reason: "recovery-blocked"
|
|
93
|
-
});
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget({
|
|
97
|
-
scope: "task",
|
|
98
|
-
taskId: task.id,
|
|
99
|
-
roleName: role.name
|
|
100
|
-
})))) {
|
|
101
|
-
// A stopped/broken Session is eligible for a fresh mode only after
|
|
102
|
-
// its exact runtime cleanup/reservation lane has settled.
|
|
103
|
-
results.push({
|
|
104
|
-
taskId: task.id,
|
|
105
|
-
status: "skipped",
|
|
106
|
-
reason: "recovery-blocked"
|
|
107
|
-
});
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
59
|
const compatibleSession = existingSession !== null
|
|
112
60
|
&& (reopening
|
|
113
61
|
? effectiveLaunchSnapshotsCompatible(existingSession.effective, role.effective)
|
|
@@ -123,15 +71,14 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
123
71
|
const resumableSession = hasNativeSession(existingSession)
|
|
124
72
|
&& existingSession.status !== "stopped"
|
|
125
73
|
&& existingSession.status !== "broken";
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// request (or exact terminal/missing evidence) authorizes a fresh one.
|
|
74
|
+
// Reuse a healthy Session. A terminal Session is disposable and the next
|
|
75
|
+
// Run starts fresh without consulting historical recovery records.
|
|
129
76
|
const sessionSet = store.getTaskRoleSessionSet?.(task.id, role.name) ?? null;
|
|
130
77
|
const mode = reopenIdentityDrift
|
|
131
78
|
? "new"
|
|
132
79
|
: sessionSet === null
|
|
133
80
|
? resumableSession && compatibleSession ? "resume" : "new"
|
|
134
|
-
:
|
|
81
|
+
: roleAgentSessionResumeMode(sessionSet, role.effective.agentId, role.effective);
|
|
135
82
|
const runId = store.peekNextAgentRunId(task.id);
|
|
136
83
|
const wakeEnvelope = resolveLeaderWakeEnvelope(store, task.id);
|
|
137
84
|
const contextSnapshot = store.freezeLeaderContextSnapshot?.(task.id, role.name, now);
|
|
@@ -226,7 +173,9 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
226
173
|
|| ready.prepared.turnAcceptedDuringLaunch === true
|
|
227
174
|
|| ready.prepared.turnDeliveryUnknownDuringLaunch === true;
|
|
228
175
|
const latestTask = store.getTask(task.id);
|
|
229
|
-
if (latestTask === null
|
|
176
|
+
if (latestTask === null
|
|
177
|
+
|| latestTask.status !== "active"
|
|
178
|
+
|| latestTask.executionGate.state !== "enabled") {
|
|
230
179
|
delivery.forgetPrepared?.({
|
|
231
180
|
taskId: task.id,
|
|
232
181
|
roleName: role.name,
|