@zq-silk/yui 0.7.1 → 0.8.2
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 +27 -28
- package/README.md +79 -71
- package/dist/cli/commandCatalog.js +283 -136
- package/dist/cli/completion.js +3 -3
- package/dist/cli/helpRenderer.js +3 -0
- package/dist/cli/interactionPolicy.js +48 -33
- package/dist/cli/interactiveSelection.js +1 -1
- package/dist/cli/invocationRouter.js +3 -2
- package/dist/cli/roleWizard.js +8 -8
- package/dist/cli.js +189 -93
- package/dist/commands/agentCommands.js +5 -5
- package/dist/commands/configCommands.js +351 -104
- package/dist/commands/configOverview.js +60 -0
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/globalRoleCommands.js +9 -9
- package/dist/commands/profileCommands.js +8 -8
- package/dist/commands/resourcesCommands.js +6 -5
- package/dist/commands/taskCommands.js +111 -59
- package/dist/commands/taskRoleRuntimeStatus.js +3 -1
- package/dist/commands/telemetryCommands.js +11 -6
- package/dist/config/configCatalog.js +42 -0
- package/dist/config/yuiConfig.js +80 -35
- package/dist/context/sessionBootstrapManifest.js +1 -1
- package/dist/controller/clientRuntime.js +0 -2
- package/dist/controller/controller.js +21 -9
- package/dist/controller/fileSchedulerStoreAdapter.js +409 -79
- package/dist/controller/resourceInventory.js +9 -5
- package/dist/controller/runtime.js +112 -25
- package/dist/controller/runtimeLaunchCoordinator.js +18 -78
- package/dist/controller/structuredProviderObservation.js +273 -0
- package/dist/doctor/doctor.js +2 -2
- package/dist/executor/agentAdapter.js +40 -0
- package/dist/executor/agentExecutor.js +31 -7
- package/dist/executor/executorRegistry.js +11 -49
- package/dist/executor/fileRoleLaunchPlanner.js +115 -37
- package/dist/lifecycle/canonicalLifecycleEvent.js +5 -2
- package/dist/resources/autoResourceGc.js +3 -1
- package/dist/review/reviewConfig.js +0 -2
- package/dist/run/agentRun.js +2 -2
- package/dist/run/providerRetry.js +29 -16
- package/dist/run/providerRetryConfig.js +5 -3
- package/dist/runtime/agentHost.js +767 -158
- package/dist/runtime/builtinAgentDrivers.js +1 -5
- package/dist/runtime/codexAppServerRuntime.js +67 -60
- package/dist/runtime/exactControlPlane.js +7 -2
- package/dist/runtime/index.js +6 -2
- package/dist/runtime/launchBroker.js +30 -8
- package/dist/runtime/launchDiagnostics.js +1 -1
- package/dist/runtime/providerAuthorityFence.js +24 -0
- package/dist/runtime/providerControl.js +63 -0
- package/dist/runtime/providerRecoveryDecision.js +55 -0
- package/dist/runtime/providerRuntimeIdentity.js +269 -19
- package/dist/runtime/runtimeBinding.js +20 -11
- package/dist/runtime/structuredProviderHost.js +476 -0
- package/dist/runtime/tmuxAdapters.js +143 -42
- package/dist/scheduler/activeRoleRunDelivery.js +206 -120
- package/dist/scheduler/leaderWakeupProcessor.js +141 -16
- package/dist/scheduler/roleRunStall.js +12 -9
- package/dist/setup/setupCommand.js +153 -492
- package/dist/storage/compatibleTaskStore.js +9 -5
- package/dist/storage/migration/productionRegistry.js +169 -0
- package/dist/storage/taskStore.js +22 -3
- package/dist/telemetry/sqliteTelemetryStore.js +9 -1
- package/dist/telemetry/telemetryConfig.js +1 -18
- package/dist/telemetry/telemetryStore.js +2 -2
- package/dist/telemetry/telemetryWiring.js +6 -5
- package/dist/tmux/tmuxManager.js +1 -1
- package/dist/web/webSnapshot.js +5 -3
- package/i18n/README.zh-CN.md +48 -40
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +12 -5
- package/skills/yui-operator/SKILL.md +44 -6
- package/skills/yui-runtime/SKILL.md +1 -1
|
@@ -113,16 +113,17 @@ export function buildControllerResourceInventory(facts) {
|
|
|
113
113
|
for (const process of paneProcesses)
|
|
114
114
|
claimed.add(process.pid);
|
|
115
115
|
const role = findRole(homeFact.roles, pane);
|
|
116
|
-
const
|
|
116
|
+
const terminalIsolation = role?.taskStatus === "retired"
|
|
117
|
+
|| role?.taskStatus === "archived";
|
|
117
118
|
resources.push(processResource({
|
|
118
119
|
kind: "agent-session",
|
|
119
120
|
state: pane.dead ? "dead" : role === undefined ? "orphaned" : "running",
|
|
120
121
|
disposition: role === undefined
|
|
121
122
|
? pane.dead ? "safe" : "review"
|
|
122
|
-
:
|
|
123
|
+
: terminalIsolation ? "safe" : "protected",
|
|
123
124
|
reasonCode: role === undefined
|
|
124
125
|
? pane.dead ? "dead-orphan-pane" : "orphan-pane"
|
|
125
|
-
:
|
|
126
|
+
: terminalIsolation ? `${role.taskStatus}-task-pane` : "owned-role-pane",
|
|
126
127
|
yuiHome,
|
|
127
128
|
owner: role === undefined ? { kind: "none" } : roleOwner(role),
|
|
128
129
|
processes: paneProcesses,
|
|
@@ -359,7 +360,10 @@ function artifactResource(artifact, yuiHome, domain, homeId) {
|
|
|
359
360
|
...(domain === undefined ? {} : { domain })
|
|
360
361
|
};
|
|
361
362
|
}
|
|
362
|
-
function domainDisposition(base,
|
|
363
|
+
function domainDisposition(base, reason, domain, target) {
|
|
364
|
+
if (reason === "retired-task-pane" || reason === "archived-task-pane") {
|
|
365
|
+
return "safe";
|
|
366
|
+
}
|
|
363
367
|
if (domain === undefined)
|
|
364
368
|
return base;
|
|
365
369
|
if (domain.disposition === "safe") {
|
|
@@ -377,7 +381,7 @@ function domainDisposition(base, _reason, domain, target) {
|
|
|
377
381
|
// A tmux server with panes is protected until the pane resources have
|
|
378
382
|
// been removed and a later bounded pass can revalidate the empty
|
|
379
383
|
// server. This avoids killing a server while a target race is in flight.
|
|
380
|
-
if (base === "protected" &&
|
|
384
|
+
if (base === "protected" && reason === "owned-tmux-server")
|
|
381
385
|
return "protected";
|
|
382
386
|
// `report-only` is deliberately reserved for an unrecognized process;
|
|
383
387
|
// the YUI_HOME environment alone is not a cleanup ownership proof.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { reconciliationIntervalMilliseconds, resolveTmuxBin } from "../config/yuiConfig.js";
|
|
1
|
+
import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveDeliveryTimeoutSeconds, resolveRuntimeHealth, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -17,9 +17,9 @@ import { AsyncTaskStoreClient, resolveStoreWorkerEnabledForHome } from "../stora
|
|
|
17
17
|
import { FileTaskWorkspacePreparer } from "../repository/taskWorkspacePreparer.js";
|
|
18
18
|
import { NodeCommandExecutor } from "../tmux/commandExecutor.js";
|
|
19
19
|
import { TmuxManager, yuiTmuxServerName } from "../tmux/tmuxManager.js";
|
|
20
|
-
import {
|
|
20
|
+
import { AgentHostPromptPushAdapter, FileTaskRuntimeIsolation, TmuxSessionHost, ProviderContinuationReconciliationService } from "../runtime/index.js";
|
|
21
21
|
import { startFileTaskController } from "./controller.js";
|
|
22
|
-
import { FileSchedulerStoreAdapter } from "./fileSchedulerStoreAdapter.js";
|
|
22
|
+
import { AgentHostProviderTurnFenceError, FileSchedulerStoreAdapter } from "./fileSchedulerStoreAdapter.js";
|
|
23
23
|
import { openSchedulerTelemetry } from "../telemetry/telemetryWiring.js";
|
|
24
24
|
import { createFileArtifactPort, createLinuxProcessPort, DurableJobSupervisor } from "./jobSupervisor.js";
|
|
25
25
|
import { createDurableJobControl } from "./jobControl.js";
|
|
@@ -38,7 +38,7 @@ import { launchBrokerForHome } from "../runtime/launchBroker.js";
|
|
|
38
38
|
import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
|
|
39
39
|
import { appendGlobalProcessExitObservation } from "../runtime/globalProcessExitStore.js";
|
|
40
40
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
41
|
-
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
41
|
+
import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
42
42
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
43
43
|
/** Refreshes only the exact Task runtime generation folded by the event transaction. */
|
|
44
44
|
export function refreshAppliedTaskRuntimeDescriptor(store, planner, input) {
|
|
@@ -92,6 +92,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
92
92
|
? new SqliteTaskStore(home)
|
|
93
93
|
: openCompatibleFileTaskStore(home));
|
|
94
94
|
const homeId = store.getHomeIdentity().homeId;
|
|
95
|
+
const durableConfig = store.getConfig();
|
|
95
96
|
// When the worker backend is active, the db-touching observer folds run in
|
|
96
97
|
// the worker (off the main event loop). The client is closed on shutdown.
|
|
97
98
|
const asyncStoreClient = useWorker
|
|
@@ -113,8 +114,9 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
113
114
|
const planner = options.planner ?? new FileRoleLaunchPlanner(home, store, {
|
|
114
115
|
environment: options.environment
|
|
115
116
|
});
|
|
116
|
-
const tmux = options.tmux ?? new TmuxManager(resolveTmuxBin(
|
|
117
|
+
const tmux = options.tmux ?? new TmuxManager(resolveTmuxBin(durableConfig.tmuxBin), new NodeCommandExecutor(), {
|
|
117
118
|
yuiHome: home,
|
|
119
|
+
historyLimit: resolveTmuxHistoryLimit(durableConfig.tmuxHistoryLimit),
|
|
118
120
|
...(domainIdentity === undefined
|
|
119
121
|
? {}
|
|
120
122
|
: {
|
|
@@ -181,8 +183,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
181
183
|
}
|
|
182
184
|
throw new Error("Native session discovery was aborted.");
|
|
183
185
|
},
|
|
184
|
-
inactivityTimeoutMs:
|
|
185
|
-
?? process.env.YUI_LAUNCH_INACTIVITY_TIMEOUT_MS, 300_000),
|
|
186
|
+
inactivityTimeoutMs: resolveAgentLaunchInactivityTimeoutSeconds(durableConfig.agentLaunchInactivityTimeoutSeconds) * 1_000,
|
|
186
187
|
onHostCreated: ({ binding, pane }) => {
|
|
187
188
|
sessionOwners.recordHostOwner({
|
|
188
189
|
owner: binding.owner,
|
|
@@ -197,7 +198,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
197
198
|
}
|
|
198
199
|
});
|
|
199
200
|
const promptPush = options.promptPush
|
|
200
|
-
?? new
|
|
201
|
+
?? new AgentHostPromptPushAdapter(home);
|
|
201
202
|
const runtimeIsolation = options.runtimeIsolation
|
|
202
203
|
?? new FileTaskRuntimeIsolation({
|
|
203
204
|
// A sibling of the exact control Home keeps provider data/cache/tmp out
|
|
@@ -282,9 +283,6 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
282
283
|
sessionHost,
|
|
283
284
|
promptPush,
|
|
284
285
|
launchCoordinator,
|
|
285
|
-
...(options.providerInputRouting === undefined
|
|
286
|
-
? {}
|
|
287
|
-
: { providerInputRouting: options.providerInputRouting }),
|
|
288
286
|
roleResourceInventory: async (panes, inputs) => {
|
|
289
287
|
const inventory = await scanInventory(panes);
|
|
290
288
|
return inventory.resources.flatMap((resource) => {
|
|
@@ -438,9 +436,16 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
438
436
|
intervalMs: options.intervalMs
|
|
439
437
|
?? reconciliationIntervalMilliseconds(store.getConfig().reconciliationIntervalSeconds),
|
|
440
438
|
signalWindowMs: options.signalWindowMs,
|
|
441
|
-
taskConcurrency: options.taskConcurrency
|
|
439
|
+
taskConcurrency: options.taskConcurrency
|
|
440
|
+
?? resolveControllerTaskConcurrency(durableConfig.controllerTaskConcurrency),
|
|
442
441
|
deliveryRetryMs: options.deliveryRetryMs,
|
|
443
442
|
deliveryRetryLimit: options.deliveryRetryLimit,
|
|
443
|
+
deliveryTimeoutMs: options.deliveryTimeoutMs
|
|
444
|
+
?? resolveDeliveryTimeoutSeconds(durableConfig.deliveryTimeoutSeconds) * 1_000,
|
|
445
|
+
stallWindowMs: options.stallWindowMs
|
|
446
|
+
?? resolveRuntimeHealth(durableConfig.runtimeHealth).stallWindowMs,
|
|
447
|
+
diagnosticAfterMs: options.diagnosticAfterMs
|
|
448
|
+
?? resolveRuntimeHealth(durableConfig.runtimeHealth).diagnosticAfterMs,
|
|
444
449
|
now: options.now,
|
|
445
450
|
onError: options.onError,
|
|
446
451
|
lifecycleHost,
|
|
@@ -497,13 +502,28 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
497
502
|
catch (error) {
|
|
498
503
|
(options.onError ?? (() => undefined))(error);
|
|
499
504
|
}
|
|
505
|
+
let resourceClose;
|
|
506
|
+
const closeResources = () => {
|
|
507
|
+
resourceClose ??= Promise.all([
|
|
508
|
+
asyncStoreClient?.close() ?? Promise.resolve(),
|
|
509
|
+
inventoryClient?.close() ?? Promise.resolve()
|
|
510
|
+
]).then(() => undefined);
|
|
511
|
+
return resourceClose;
|
|
512
|
+
};
|
|
513
|
+
const closed = running.closed.then(closeResources);
|
|
500
514
|
return {
|
|
501
515
|
...running,
|
|
516
|
+
closed,
|
|
502
517
|
close: async () => {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
518
|
+
try {
|
|
519
|
+
await running.close();
|
|
520
|
+
}
|
|
521
|
+
finally {
|
|
522
|
+
// RPC-driven Controller stops resolve `running.closed` without calling
|
|
523
|
+
// this wrapper. Share one cleanup promise so both lifecycle paths
|
|
524
|
+
// release the worker connections before the process can linger.
|
|
525
|
+
await closeResources();
|
|
526
|
+
}
|
|
507
527
|
},
|
|
508
528
|
store,
|
|
509
529
|
schedulerStore,
|
|
@@ -527,6 +547,55 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
527
547
|
});
|
|
528
548
|
const lifecycleTails = new Map();
|
|
529
549
|
return async (method, params) => {
|
|
550
|
+
if (method === "runtime.observation-apply") {
|
|
551
|
+
return {
|
|
552
|
+
outcome: schedulerStore.observeRuntimeObservation(createRuntimeObservation(params), new Date())
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
if (method === "runtime.provider-turn-begin") {
|
|
556
|
+
const value = providerTurnControlParams(params);
|
|
557
|
+
try {
|
|
558
|
+
schedulerStore.beginAgentHostProviderTurn({
|
|
559
|
+
taskId: value.taskId,
|
|
560
|
+
roleName: value.roleName,
|
|
561
|
+
runId: value.runId,
|
|
562
|
+
agentId: value.agentId,
|
|
563
|
+
launchId: value.launchId,
|
|
564
|
+
nativeSessionId: value.nativeSessionId,
|
|
565
|
+
attemptId: value.attemptId,
|
|
566
|
+
authorityEpoch: value.authorityEpoch,
|
|
567
|
+
authorityOwner: value.authorityOwner,
|
|
568
|
+
holderId: value.holderId,
|
|
569
|
+
now: value.now
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
if (error instanceof AgentHostProviderTurnFenceError) {
|
|
574
|
+
throw applicationError("INVALID_PARAMS", error.message);
|
|
575
|
+
}
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
return { recorded: true };
|
|
579
|
+
}
|
|
580
|
+
if (method === "runtime.provider-turn-submission-resolve") {
|
|
581
|
+
const value = providerTurnControlParams(params);
|
|
582
|
+
const status = params.status;
|
|
583
|
+
const reason = params.reason;
|
|
584
|
+
if ((status !== "rejected" && status !== "delivery-unknown")
|
|
585
|
+
|| typeof reason !== "string" || reason.trim().length === 0) {
|
|
586
|
+
throw applicationError("INVALID_PARAMS", "Provider Turn resolution is invalid.");
|
|
587
|
+
}
|
|
588
|
+
schedulerStore.resolveAgentHostProviderTurnSubmission({
|
|
589
|
+
taskId: value.taskId,
|
|
590
|
+
roleName: value.roleName,
|
|
591
|
+
runId: value.runId,
|
|
592
|
+
attemptId: value.attemptId,
|
|
593
|
+
status,
|
|
594
|
+
reason,
|
|
595
|
+
now: value.now
|
|
596
|
+
});
|
|
597
|
+
return { recorded: true };
|
|
598
|
+
}
|
|
530
599
|
if (method === "runtime.process-exit-observe") {
|
|
531
600
|
const observation = validateRuntimeProcessExitObservation(params);
|
|
532
601
|
const run = observation.taskId === undefined || observation.runId === undefined
|
|
@@ -1009,6 +1078,33 @@ function requiredParam(value) {
|
|
|
1009
1078
|
}
|
|
1010
1079
|
return value;
|
|
1011
1080
|
}
|
|
1081
|
+
function providerTurnControlParams(params) {
|
|
1082
|
+
if (typeof params !== "object" || params === null || Array.isArray(params)) {
|
|
1083
|
+
throw applicationError("INVALID_PARAMS", "Provider Turn control params are invalid.");
|
|
1084
|
+
}
|
|
1085
|
+
const value = params;
|
|
1086
|
+
const authorityEpoch = value.authorityEpoch;
|
|
1087
|
+
const authorityOwner = value.authorityOwner;
|
|
1088
|
+
const observedAt = requiredParam(value.observedAt);
|
|
1089
|
+
if (!Number.isSafeInteger(authorityEpoch) || authorityEpoch < 1
|
|
1090
|
+
|| (authorityOwner !== "controller" && authorityOwner !== "human")
|
|
1091
|
+
|| !Number.isFinite(Date.parse(observedAt))) {
|
|
1092
|
+
throw applicationError("INVALID_PARAMS", "Provider Turn control fence is invalid.");
|
|
1093
|
+
}
|
|
1094
|
+
return {
|
|
1095
|
+
taskId: requiredParam(value.taskId),
|
|
1096
|
+
roleName: requiredParam(value.roleName),
|
|
1097
|
+
runId: requiredParam(value.runId),
|
|
1098
|
+
agentId: requiredParam(value.agentId),
|
|
1099
|
+
launchId: requiredParam(value.launchId),
|
|
1100
|
+
nativeSessionId: requiredParam(value.nativeSessionId),
|
|
1101
|
+
attemptId: requiredParam(value.attemptId),
|
|
1102
|
+
authorityEpoch: authorityEpoch,
|
|
1103
|
+
authorityOwner,
|
|
1104
|
+
holderId: requiredParam(value.holderId),
|
|
1105
|
+
now: new Date(observedAt)
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1012
1108
|
function abortableDelay(milliseconds, signal) {
|
|
1013
1109
|
return new Promise((resolve, reject) => {
|
|
1014
1110
|
const timer = setTimeout(() => {
|
|
@@ -1026,15 +1122,6 @@ function abortableDelay(milliseconds, signal) {
|
|
|
1026
1122
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1027
1123
|
});
|
|
1028
1124
|
}
|
|
1029
|
-
function positiveIntegerOption(value, fallback) {
|
|
1030
|
-
if (value === undefined || value.trim().length === 0)
|
|
1031
|
-
return fallback;
|
|
1032
|
-
const parsed = Number(value);
|
|
1033
|
-
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
1034
|
-
throw new Error(`Invalid native session discovery timeout: ${value}`);
|
|
1035
|
-
}
|
|
1036
|
-
return parsed;
|
|
1037
|
-
}
|
|
1038
1125
|
function applicationError(code, message) {
|
|
1039
1126
|
const error = Object.assign(new Error(message), { code });
|
|
1040
1127
|
error.name = "CoreApplicationError";
|
|
@@ -8,12 +8,6 @@ class RuntimeBindingContractError extends Error {
|
|
|
8
8
|
this.name = "RuntimeBindingContractError";
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
-
class LaunchSubmittedHostBusyError extends RuntimeBindingContractError {
|
|
12
|
-
constructor(message) {
|
|
13
|
-
super(message);
|
|
14
|
-
this.name = "LaunchSubmittedHostBusyError";
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
11
|
class RuntimeLaunchStateChangedError extends Error {
|
|
18
12
|
constructor(message, options) {
|
|
19
13
|
super(message, options);
|
|
@@ -103,12 +97,6 @@ export class RuntimeLaunchCoordinator {
|
|
|
103
97
|
...(request.runId === undefined ? {} : { runId: request.runId })
|
|
104
98
|
}, assertLaunchCurrent, this.#now());
|
|
105
99
|
let reusedConfirmedRunningHost = false;
|
|
106
|
-
const launchCarriesExactRunPrompt = carriesExactRunPrompt(request);
|
|
107
|
-
// Preserve the existing Codex recovery path; managed Claude is the one
|
|
108
|
-
// finite-process protocol that must never fall back to tmux key delivery
|
|
109
|
-
// when a newly-reserved Run encounters an older live pane.
|
|
110
|
-
let launchPromptAcknowledgementRequired = request.adapterId === "claude"
|
|
111
|
-
&& launchCarriesExactRunPrompt;
|
|
112
100
|
if (reservation.status === "existing") {
|
|
113
101
|
if (!reservation.launchId.startsWith(generationPrefix)) {
|
|
114
102
|
this.#requireCleanup(request.owner);
|
|
@@ -167,12 +155,6 @@ export class RuntimeLaunchCoordinator {
|
|
|
167
155
|
throw new Error(`Runtime launch reservation belongs to another Run whose host is still running: ${reservation.runId}.`);
|
|
168
156
|
}
|
|
169
157
|
reusedConfirmedRunningHost = true;
|
|
170
|
-
// A fresh Codex generation carries the prompt in its launch argv. A
|
|
171
|
-
// managed Claude generation carries it through stream-json stdin for
|
|
172
|
-
// both new and native-resume modes. Only recovery of the exact same
|
|
173
|
-
// reserved Run may bridge a lost in-memory launch acknowledgement.
|
|
174
|
-
launchPromptAcknowledgementRequired = sameRunReservation
|
|
175
|
-
&& launchCarriesExactRunPrompt;
|
|
176
158
|
runtimeIsolation = this.#preflightRuntimeIsolation(request, reservation.launchId, reservation.launchId.slice(generationPrefix.length), true);
|
|
177
159
|
assertLaunchCurrent();
|
|
178
160
|
}
|
|
@@ -229,11 +211,8 @@ export class RuntimeLaunchCoordinator {
|
|
|
229
211
|
: { environment: request.environment }),
|
|
230
212
|
nativeSessionId: requireText(request.nativeSessionId, "Native session id")
|
|
231
213
|
}, beforeHostStart === undefined ? undefined : observePreflight);
|
|
232
|
-
binding = requireMatchingRuntimeBinding(rawBinding, request, launchId
|
|
233
|
-
if (!preflightObserved
|
|
234
|
-
&& !(binding.hostCreated === false
|
|
235
|
-
&& request.adapterId === "claude"
|
|
236
|
-
&& launchCarriesExactRunPrompt)) {
|
|
214
|
+
binding = requireMatchingRuntimeBinding(rawBinding, request, launchId);
|
|
215
|
+
if (!preflightObserved) {
|
|
237
216
|
throw new Error("Runtime session host did not expose a pre-host-start launch fence.");
|
|
238
217
|
}
|
|
239
218
|
}
|
|
@@ -244,8 +223,7 @@ export class RuntimeLaunchCoordinator {
|
|
|
244
223
|
// terminalize that reservation.
|
|
245
224
|
throw new RuntimeLaunchError(true, launchId, error.message, error.reason);
|
|
246
225
|
}
|
|
247
|
-
if (
|
|
248
|
-
|| error instanceof RuntimeHostContentionError)
|
|
226
|
+
if (error instanceof RuntimeHostContentionError
|
|
249
227
|
&& !reusedConfirmedRunningHost) {
|
|
250
228
|
let exactCleanupAttempted = false;
|
|
251
229
|
let completed = false;
|
|
@@ -266,12 +244,6 @@ export class RuntimeLaunchCoordinator {
|
|
|
266
244
|
this.#requireCleanup(request.owner);
|
|
267
245
|
throw new Error("Busy managed runtime launch reservation changed during retry release.");
|
|
268
246
|
}
|
|
269
|
-
if (error instanceof LaunchSubmittedHostBusyError) {
|
|
270
|
-
// A terminal Run no longer owns its finite provider process. Move
|
|
271
|
-
// that exact Role owner through the durable cleanup lane before a
|
|
272
|
-
// later retry creates the successor generation.
|
|
273
|
-
this.#requireCleanup(request.owner);
|
|
274
|
-
}
|
|
275
247
|
throw new RuntimeLaunchError(true, launchId, error.message, error instanceof RuntimeHostContentionError
|
|
276
248
|
? error.reason
|
|
277
249
|
: "previous-process");
|
|
@@ -291,7 +263,6 @@ export class RuntimeLaunchCoordinator {
|
|
|
291
263
|
}
|
|
292
264
|
await this.#compensateStartedHost(request.owner, binding, launchId, runtimeIsolation, new Error(`Runtime host was recreated while recovering an existing generation: ${request.owner.roleName}.`));
|
|
293
265
|
}
|
|
294
|
-
let reservationConfirmation;
|
|
295
266
|
try {
|
|
296
267
|
assertLaunchCurrent();
|
|
297
268
|
if (persistence === "immediate" && binding.nativeSessionId !== undefined) {
|
|
@@ -308,7 +279,7 @@ export class RuntimeLaunchCoordinator {
|
|
|
308
279
|
// Deferred scheduler persistence records a known native identity while
|
|
309
280
|
// retaining the reservation until exact Run delivery. Fresh Codex has
|
|
310
281
|
// no identity yet and keeps it until its matching generation Hook.
|
|
311
|
-
|
|
282
|
+
this.reservations.confirmRuntimeLaunchReservation({
|
|
312
283
|
owner: request.owner,
|
|
313
284
|
launchId
|
|
314
285
|
}, assertLaunchCurrent);
|
|
@@ -317,18 +288,6 @@ export class RuntimeLaunchCoordinator {
|
|
|
317
288
|
catch (error) {
|
|
318
289
|
await this.#compensateStartedHost(request.owner, binding, launchId, runtimeIsolation, error);
|
|
319
290
|
}
|
|
320
|
-
if (launchCarriesExactRunPrompt
|
|
321
|
-
&& binding.hostCreated !== false
|
|
322
|
-
&& reservationConfirmation !== "provider-bound"
|
|
323
|
-
&& binding.initialPromptRunId !== request.runId) {
|
|
324
|
-
// A launch-carried prompt may return before its matching lifecycle Hook,
|
|
325
|
-
// but only an exact Run marker can bridge that asynchronous interval.
|
|
326
|
-
// This is transport evidence, never Provider acceptance; the
|
|
327
|
-
// reservation remains fenced until the matching Hook binds the native
|
|
328
|
-
// Session.
|
|
329
|
-
this.#requireCleanup(request.owner);
|
|
330
|
-
throw new RuntimeBindingContractError(`Session host cannot acknowledge the exact launch-carried prompt: ${request.owner.roleName}.`);
|
|
331
|
-
}
|
|
332
291
|
return binding;
|
|
333
292
|
}
|
|
334
293
|
async #settleFailedStart(request, launchId, reusedConfirmedRunningHost, runtimeIsolation) {
|
|
@@ -461,12 +420,12 @@ function validateRuntimeLaunchPreflight(preflight, request, launchId) {
|
|
|
461
420
|
|| !effectiveLaunchSnapshotsCompatible(preflight.effective, request.effective)
|
|
462
421
|
|| (request.mode === "resume"
|
|
463
422
|
&& preflight.nativeSessionId !== request.nativeSessionId)
|
|
464
|
-
|| (preflight.
|
|
465
|
-
&& preflight.
|
|
423
|
+
|| (preflight.initialTurnRunId !== undefined
|
|
424
|
+
&& preflight.initialTurnRunId !== request.runId)) {
|
|
466
425
|
throw new Error(`Session host pre-start launch fence does not match the requested runtime: ${request.owner.roleName}.`);
|
|
467
426
|
}
|
|
468
427
|
}
|
|
469
|
-
function requireMatchingRuntimeBinding(raw, request, launchId
|
|
428
|
+
function requireMatchingRuntimeBinding(raw, request, launchId) {
|
|
470
429
|
let binding;
|
|
471
430
|
try {
|
|
472
431
|
binding = createRuntimeBinding(raw);
|
|
@@ -479,9 +438,17 @@ function requireMatchingRuntimeBinding(raw, request, launchId, launchPromptAckno
|
|
|
479
438
|
&& (binding.owner.scope === "global"
|
|
480
439
|
|| (request.owner.scope === "task"
|
|
481
440
|
&& binding.owner.taskId === request.owner.taskId));
|
|
482
|
-
if (binding.
|
|
483
|
-
&& binding.
|
|
484
|
-
throw new RuntimeBindingContractError(`Session host returned
|
|
441
|
+
if (binding.initialTurnRunId !== undefined
|
|
442
|
+
&& binding.initialTurnRunId !== request.runId) {
|
|
443
|
+
throw new RuntimeBindingContractError(`Session host returned an initial structured Turn for another Run: ${request.owner.roleName}.`);
|
|
444
|
+
}
|
|
445
|
+
if (binding.initialTurnDeliveryUnknownRunId !== undefined
|
|
446
|
+
&& binding.initialTurnDeliveryUnknownRunId !== request.runId) {
|
|
447
|
+
throw new RuntimeBindingContractError(`Session host returned a delivery-unknown initial structured Turn for another Run: ${request.owner.roleName}.`);
|
|
448
|
+
}
|
|
449
|
+
if (binding.initialTurnRejectedRunId !== undefined
|
|
450
|
+
&& binding.initialTurnRejectedRunId !== request.runId) {
|
|
451
|
+
throw new RuntimeBindingContractError(`Session host returned a rejected initial structured Turn for another Run: ${request.owner.roleName}.`);
|
|
485
452
|
}
|
|
486
453
|
if (binding.launchId !== launchId
|
|
487
454
|
|| !ownerMatches
|
|
@@ -491,35 +458,8 @@ function requireMatchingRuntimeBinding(raw, request, launchId, launchPromptAckno
|
|
|
491
458
|
&& binding.nativeSessionId !== request.nativeSessionId)) {
|
|
492
459
|
throw new RuntimeBindingContractError(`Session host returned a binding that does not match the requested runtime: ${request.owner.roleName}.`);
|
|
493
460
|
}
|
|
494
|
-
if (launchPromptAcknowledgementRequired
|
|
495
|
-
&& binding.initialPromptRunId !== request.runId
|
|
496
|
-
&& !(launchPromptUncertaintyAllowed && binding.hostCreated === false)) {
|
|
497
|
-
throw binding.hostCreated === false
|
|
498
|
-
? new LaunchSubmittedHostBusyError(`An earlier managed runtime is still exiting: ${request.owner.roleName}.`)
|
|
499
|
-
: new RuntimeBindingContractError(`Session host cannot acknowledge the exact launch-carried prompt: ${request.owner.roleName}.`);
|
|
500
|
-
}
|
|
501
|
-
if (launchPromptAcknowledgementRequired
|
|
502
|
-
&& launchPromptUncertaintyAllowed
|
|
503
|
-
&& binding.hostCreated === false
|
|
504
|
-
&& request.runId !== undefined
|
|
505
|
-
&& binding.initialPromptRunId !== request.runId) {
|
|
506
|
-
// A Controller restart can lose only the in-memory fact that a still-
|
|
507
|
-
// running generation carried this Run at process launch. Keep the
|
|
508
|
-
// uncertainty explicitly tied to the exact reservation/Run; the matching
|
|
509
|
-
// Provider Hook remains the sole acceptance authority.
|
|
510
|
-
return {
|
|
511
|
-
...binding,
|
|
512
|
-
launchPromptUncertainRunId: request.runId
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
461
|
return binding;
|
|
516
462
|
}
|
|
517
|
-
function carriesExactRunPrompt(request) {
|
|
518
|
-
if (request.owner.scope !== "task" || request.runId === undefined)
|
|
519
|
-
return false;
|
|
520
|
-
return request.adapterId === "claude"
|
|
521
|
-
|| (request.adapterId === "codex" && request.mode === "new");
|
|
522
|
-
}
|
|
523
463
|
function defaultLaunchFingerprint(request) {
|
|
524
464
|
return createHash("sha256").update(JSON.stringify([
|
|
525
465
|
request.owner,
|