@zq-silk/yui 0.6.2 → 0.6.3
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 +28 -4
- package/README.md +62 -24
- package/dist/agent/argumentPolicy.js +1 -1
- package/dist/agent/managedRuntimeEnvironment.js +1 -0
- package/dist/cli/commandCatalog.js +19 -9
- package/dist/cli/interactionPolicy.js +4 -2
- package/dist/cli.js +77 -32
- package/dist/commands/taskCommands.js +46 -11
- package/dist/commands/taskContextCommand.js +1 -1
- package/dist/commands/taskRoleRuntimeStatus.js +170 -10
- package/dist/controller/agentRuntimeObserver.js +210 -0
- package/dist/controller/clientRuntime.js +3 -21
- package/dist/controller/controller.js +47 -7
- package/dist/controller/fileSchedulerStoreAdapter.js +522 -388
- package/dist/controller/runtime.js +9 -3
- package/dist/controller/runtimeEventInbox.js +49 -295
- package/dist/controller/runtimeEventProcessor.js +184 -321
- package/dist/controller/runtimeHookRunFence.js +226 -0
- package/dist/controller/runtimeLaunchCoordinator.js +91 -26
- package/dist/controller/runtimeObservationHook.js +112 -0
- package/dist/core/controllerServer.js +5 -0
- package/dist/executor/agentAdapter.js +18 -3
- package/dist/executor/fileRoleLaunchPlanner.js +64 -15
- package/dist/executor/managedClaudeRunner.js +121 -0
- package/dist/observability/executionAudit.js +6 -3
- package/dist/repository/taskWorkspacePreparer.js +1 -4
- package/dist/run/providerRetryConfig.js +8 -3
- package/dist/runtime/agentDriver.js +229 -0
- package/dist/runtime/agentDriverObservation.js +57 -0
- package/dist/runtime/builtinAgentDrivers.js +235 -0
- package/dist/runtime/builtinTranscriptObserver.js +290 -0
- package/dist/runtime/builtinTranscriptUsage.js +97 -0
- package/dist/runtime/exactControlPlane.js +2 -2
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/ports.js +12 -1
- package/dist/runtime/runtimeObservation.js +297 -0
- package/dist/runtime/runtimeProjection.js +277 -0
- package/dist/runtime/sessionTerminationGuard.js +78 -22
- package/dist/runtime/tmuxAdapters.js +35 -0
- package/dist/scheduler/activeRoleRunDelivery.js +28 -13
- package/dist/scheduler/leaderWakeupProcessor.js +21 -2
- package/dist/scheduler/roleRunLiveness.js +2 -2
- package/dist/scheduler/roleRunStall.js +62 -114
- package/dist/storage/migration/productionRegistry.js +41 -0
- package/dist/storage/sqliteStore.js +3 -3
- package/dist/storage/storageVersions.js +1 -1
- package/dist/telemetry/sqliteTelemetryStore.js +0 -28
- package/dist/telemetry/telemetryCompaction.js +1 -0
- package/dist/telemetry/telemetryConfig.js +4 -5
- package/dist/tmux/tmuxManager.js +136 -22
- package/dist/web/assets/client/view.js +1 -1
- package/dist/web/tmuxWebTerminal.js +17 -12
- package/dist/web/webSnapshot.js +1 -1
- package/dist/worktree/managedWorkspace.js +14 -0
- package/i18n/README.zh-CN.md +7 -5
- package/package.json +1 -1
- package/dist/controller/claudeLifecycleHook.js +0 -203
- package/dist/controller/codexLifecycleHook.js +0 -108
- package/dist/controller/providerHookRunFence.js +0 -156
- package/dist/lifecycle/providerLifecycleMapping.js +0 -190
- package/dist/telemetry/telemetryRouter.js +0 -32
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
|
|
2
|
+
import { hasRuntimeCleanupObligation, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
3
|
+
import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
|
|
4
|
+
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
5
|
+
import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeEnvironment, exactControlPlaneDigest, parseExactControlPlaneDescriptor, refreshReusedTaskRuntimeDescriptorSource } from "../runtime/exactControlPlane.js";
|
|
6
|
+
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
7
|
+
/**
|
|
8
|
+
* Resolves turn identity from the current durable in-flight fence. The one
|
|
9
|
+
* exception is a Driver-declared startup Session Hook, which can arrive before
|
|
10
|
+
* Session projection and is fenced by the exact Run-bound launch reservation.
|
|
11
|
+
* Preallocated identities are additionally checked against Yui's deterministic
|
|
12
|
+
* launch identity. The immutable event is revalidated by the inbox fold.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveRuntimeHookRunFence(environment, adapterId, payloadNativeSessionId, options = {}) {
|
|
15
|
+
if (environment.YUI_SESSION_SCOPE !== "task") {
|
|
16
|
+
throw new Error("Runtime observation Hook requires a Task session scope.");
|
|
17
|
+
}
|
|
18
|
+
if (environment.YUI_ADAPTER_ID !== adapterId) {
|
|
19
|
+
throw new Error(`Runtime observation Hook requires the ${adapterId} adapter.`);
|
|
20
|
+
}
|
|
21
|
+
const home = requireIdentity(environment.YUI_HOME, "YUI_HOME");
|
|
22
|
+
const taskId = requireIdentity(environment.YUI_TASK_ID, "Task id");
|
|
23
|
+
const roleName = requireIdentity(environment.YUI_ROLE, "Role name");
|
|
24
|
+
const agentId = requireIdentity(environment.YUI_AGENT_ID, "Agent id");
|
|
25
|
+
const workspace = requireIdentity(environment.YUI_WORKSPACE, "YUI workspace");
|
|
26
|
+
const runtimeSource = environment[YUI_TASK_RUNTIME_DESCRIPTOR];
|
|
27
|
+
const runtime = runtimeSource === undefined
|
|
28
|
+
? undefined
|
|
29
|
+
: assertExactTaskRuntimeEnvironment(runtimeSource, environment, exactControlPlaneDigest(parseExactControlPlaneDescriptor(requireIdentity(environment[YUI_CONTROL_PLANE_DESCRIPTOR], "Exact control-plane descriptor"))), home);
|
|
30
|
+
const launchId = requireIdentity(runtime?.launchId ?? environment.YUI_LAUNCH_ID, "Launch id");
|
|
31
|
+
const nativeSessionId = requireIdentity(payloadNativeSessionId, "Provider session id");
|
|
32
|
+
const expectedNativeSessionId = runtime?.nativeSessionId ?? environment.YUI_NATIVE_SESSION_ID;
|
|
33
|
+
if (expectedNativeSessionId !== undefined
|
|
34
|
+
&& nativeSessionId !== requireIdentity(expectedNativeSessionId, "YUI native session id")) {
|
|
35
|
+
throw new Error("Runtime observation Hook native Session does not match its launch envelope.");
|
|
36
|
+
}
|
|
37
|
+
const store = openCompatibleFileTaskStore(home);
|
|
38
|
+
const task = store.getTask(taskId);
|
|
39
|
+
if (task === null || task.status !== "active") {
|
|
40
|
+
throw new Error("Runtime observation Hook Task is not current and active.");
|
|
41
|
+
}
|
|
42
|
+
const role = store.getRole(taskId, roleName);
|
|
43
|
+
if (role === null || role.activeAgentId !== agentId) {
|
|
44
|
+
throw new Error("Runtime observation Hook Role or Agent is not current.");
|
|
45
|
+
}
|
|
46
|
+
const sessions = store.getTaskRoleSessionSet(taskId, roleName);
|
|
47
|
+
if (sessions !== null && sessions.activeAgentId !== agentId) {
|
|
48
|
+
throw new Error("Runtime observation Hook Session Agent is not current.");
|
|
49
|
+
}
|
|
50
|
+
const inFlight = sessions?.inFlight;
|
|
51
|
+
const session = sessions?.sessions[agentId];
|
|
52
|
+
const acceptedBinding = options.nativeTurnId === undefined
|
|
53
|
+
? null
|
|
54
|
+
: acceptedTurnBinding(store.listEvents(taskId), {
|
|
55
|
+
taskId,
|
|
56
|
+
roleName,
|
|
57
|
+
agentId,
|
|
58
|
+
nativeSessionId,
|
|
59
|
+
nativeTurnId: options.nativeTurnId
|
|
60
|
+
});
|
|
61
|
+
const mailbox = store.getWorkMailbox(runtimeLifecycleTarget({
|
|
62
|
+
scope: "task",
|
|
63
|
+
taskId,
|
|
64
|
+
roleName
|
|
65
|
+
}));
|
|
66
|
+
const exactReservation = isRuntimeLaunchReservation(mailbox?.processing, launchId)
|
|
67
|
+
&& !hasRuntimeCleanupObligation(mailbox);
|
|
68
|
+
const executionRef = mailbox?.processing?.executionRef;
|
|
69
|
+
const startupRunId = options.startupSession === undefined
|
|
70
|
+
? undefined
|
|
71
|
+
: requireIdentity(runtime?.runId
|
|
72
|
+
?? environment.YUI_RUN_ID
|
|
73
|
+
?? (executionRef?.type === "run" && executionRef.taskId === taskId
|
|
74
|
+
? executionRef.id
|
|
75
|
+
: undefined), "Run id");
|
|
76
|
+
const startupReservation = startupRunId !== undefined
|
|
77
|
+
&& exactReservation
|
|
78
|
+
&& executionRef?.type === "run"
|
|
79
|
+
&& executionRef.taskId === taskId
|
|
80
|
+
&& executionRef.id === startupRunId;
|
|
81
|
+
const preallocatedStartup = options.startupSession === "preallocated"
|
|
82
|
+
&& expectedNativeSessionId !== undefined
|
|
83
|
+
&& session === undefined
|
|
84
|
+
&& startupReservation
|
|
85
|
+
&& nativeSessionId === nativeSessionIdForLaunch(home, launchId, agentId, adapterId);
|
|
86
|
+
const discoveredStartup = options.startupSession === "discovered"
|
|
87
|
+
&& expectedNativeSessionId === undefined
|
|
88
|
+
&& session === undefined
|
|
89
|
+
&& startupReservation;
|
|
90
|
+
const terminalRunId = options.terminal === true && acceptedBinding === null
|
|
91
|
+
? requireIdentity(environment.YUI_RUN_ID ?? runtime?.runId, "Run id")
|
|
92
|
+
: undefined;
|
|
93
|
+
const terminalRun = acceptedBinding !== null
|
|
94
|
+
? store.getAgentRun(taskId, acceptedBinding.fence.runId)
|
|
95
|
+
: terminalRunId === undefined
|
|
96
|
+
? null
|
|
97
|
+
: store.getAgentRun(taskId, terminalRunId);
|
|
98
|
+
const exactTerminal = terminalRun !== null
|
|
99
|
+
&& terminalRun.status !== "active"
|
|
100
|
+
&& terminalRun.roleName === roleName
|
|
101
|
+
&& terminalRun.effective.agentId === agentId
|
|
102
|
+
&& terminalRun.effective.adapterId === adapterId
|
|
103
|
+
&& session !== undefined
|
|
104
|
+
&& (acceptedBinding !== null || inFlight === null || inFlight === undefined);
|
|
105
|
+
if ((inFlight === null || inFlight === undefined)
|
|
106
|
+
&& !preallocatedStartup
|
|
107
|
+
&& !discoveredStartup
|
|
108
|
+
&& !exactTerminal
|
|
109
|
+
&& acceptedBinding === null) {
|
|
110
|
+
throw new Error("Runtime observation Hook has no matching durable in-flight Run.");
|
|
111
|
+
}
|
|
112
|
+
if (acceptedBinding === null
|
|
113
|
+
&& inFlight !== null && inFlight !== undefined && inFlight.agentId !== agentId) {
|
|
114
|
+
throw new Error("Runtime observation Hook has no matching durable in-flight Run.");
|
|
115
|
+
}
|
|
116
|
+
if ((preallocatedStartup || discoveredStartup)
|
|
117
|
+
&& inFlight !== null
|
|
118
|
+
&& inFlight !== undefined
|
|
119
|
+
&& (inFlight.runId !== startupRunId
|
|
120
|
+
|| inFlight.receiptId !== formatAgentRunReceiptId(taskId, startupRunId))) {
|
|
121
|
+
throw new Error("Runtime observation Hook has no matching durable in-flight Run.");
|
|
122
|
+
}
|
|
123
|
+
const runId = acceptedBinding?.fence.runId
|
|
124
|
+
?? inFlight?.runId
|
|
125
|
+
?? startupRunId
|
|
126
|
+
?? terminalRunId;
|
|
127
|
+
let effectiveRuntime = runtime;
|
|
128
|
+
let effectiveLaunchId = acceptedBinding?.fence.launchId ?? launchId;
|
|
129
|
+
const sessionLaunchId = session?.launchId;
|
|
130
|
+
if (acceptedBinding === null
|
|
131
|
+
&& runtime !== undefined
|
|
132
|
+
&& session !== undefined
|
|
133
|
+
&& sessionLaunchId !== undefined
|
|
134
|
+
&& typeof runtimeSource === "string"
|
|
135
|
+
&& !runtimeSource.trimStart().startsWith("{")
|
|
136
|
+
&& (runtime.runId !== runId
|
|
137
|
+
|| runtime.launchId !== sessionLaunchId
|
|
138
|
+
|| runtime.nativeSessionId !== session.nativeSessionId)) {
|
|
139
|
+
// A reused native pane keeps its original descriptor source. Advance only
|
|
140
|
+
// that Hook-owned source to the current durable generation before the
|
|
141
|
+
// volatile fence; the Controller no longer scans history to keep it fresh.
|
|
142
|
+
effectiveRuntime = refreshReusedTaskRuntimeDescriptorSource(runtimeSource, home, store, {
|
|
143
|
+
runId,
|
|
144
|
+
launchId: sessionLaunchId,
|
|
145
|
+
nativeSessionId: session.nativeSessionId
|
|
146
|
+
});
|
|
147
|
+
effectiveLaunchId = effectiveRuntime.launchId;
|
|
148
|
+
}
|
|
149
|
+
if (acceptedBinding === null
|
|
150
|
+
&& effectiveRuntime?.runId !== undefined && effectiveRuntime.runId !== runId) {
|
|
151
|
+
throw new Error("Runtime observation Hook Run does not match its current descriptor.");
|
|
152
|
+
}
|
|
153
|
+
const run = acceptedBinding !== null || exactTerminal
|
|
154
|
+
? terminalRun
|
|
155
|
+
: store.getActiveAgentRun(taskId, roleName);
|
|
156
|
+
if (run === null
|
|
157
|
+
|| run.id !== runId
|
|
158
|
+
|| (acceptedBinding === null && !exactTerminal && run.status !== "active")
|
|
159
|
+
|| run.effective.agentId !== agentId
|
|
160
|
+
|| run.effective.adapterId !== adapterId) {
|
|
161
|
+
throw new Error("Runtime observation Hook Run does not match durable active state.");
|
|
162
|
+
}
|
|
163
|
+
if (run.effective.workspace.root !== workspace) {
|
|
164
|
+
throw new Error("Runtime observation Hook workspace does not match the durable Run snapshot.");
|
|
165
|
+
}
|
|
166
|
+
if (session !== undefined) {
|
|
167
|
+
if (session.adapterId !== adapterId
|
|
168
|
+
|| (acceptedBinding === null && session.launchId !== effectiveLaunchId)
|
|
169
|
+
|| session.nativeSessionId !== nativeSessionId
|
|
170
|
+
|| session.effective.workspace.root !== workspace) {
|
|
171
|
+
throw new Error("Runtime observation Hook Session does not match its durable generation.");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
if (!discoveredStartup && !preallocatedStartup) {
|
|
176
|
+
throw new Error("Runtime observation Hook launch is not durably reserved.");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
taskId,
|
|
181
|
+
roleName,
|
|
182
|
+
agentId,
|
|
183
|
+
launchId: effectiveLaunchId,
|
|
184
|
+
runId,
|
|
185
|
+
...(acceptedBinding?.fence.receiptId === undefined
|
|
186
|
+
? inFlight?.receiptId === undefined ? {} : { receiptId: inFlight.receiptId }
|
|
187
|
+
: { receiptId: acceptedBinding.fence.receiptId }),
|
|
188
|
+
nativeSessionId,
|
|
189
|
+
workspace
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function acceptedTurnBinding(events, expected) {
|
|
193
|
+
const matches = events
|
|
194
|
+
.map(runtimeObservationFromTaskEvent)
|
|
195
|
+
.filter((observation) => observation !== null
|
|
196
|
+
&& observation.kind === "turn.accepted"
|
|
197
|
+
&& observation.fence.taskId === expected.taskId
|
|
198
|
+
&& observation.fence.roleName === expected.roleName
|
|
199
|
+
&& observation.fence.agentId === expected.agentId
|
|
200
|
+
&& observation.fence.nativeSessionId === expected.nativeSessionId
|
|
201
|
+
&& observation.fence.nativeTurnId === expected.nativeTurnId
|
|
202
|
+
&& observation.fence.runId !== undefined)
|
|
203
|
+
.sort((left, right) => (left.receivedAt.localeCompare(right.receivedAt)
|
|
204
|
+
|| (left.sequence ?? -1) - (right.sequence ?? -1)
|
|
205
|
+
|| (left.ordinal ?? -1) - (right.ordinal ?? -1)
|
|
206
|
+
|| left.eventId.localeCompare(right.eventId)));
|
|
207
|
+
const binding = matches.at(-1) ?? null;
|
|
208
|
+
if (binding === null)
|
|
209
|
+
return null;
|
|
210
|
+
if (matches.some((candidate) => candidate.fence.runId !== binding.fence.runId
|
|
211
|
+
|| candidate.fence.launchId !== binding.fence.launchId
|
|
212
|
+
|| candidate.fence.receiptId !== binding.fence.receiptId)) {
|
|
213
|
+
throw new Error("Runtime observation Hook native Turn has conflicting durable Run bindings.");
|
|
214
|
+
}
|
|
215
|
+
return binding;
|
|
216
|
+
}
|
|
217
|
+
function requireIdentity(value, label) {
|
|
218
|
+
if (typeof value !== "string" || value.includes("\0")) {
|
|
219
|
+
throw new Error(`${label} is required.`);
|
|
220
|
+
}
|
|
221
|
+
const normalized = value.trim();
|
|
222
|
+
if (normalized.length === 0 || normalized.length > 1_024) {
|
|
223
|
+
throw new Error(`${label} is invalid.`);
|
|
224
|
+
}
|
|
225
|
+
return normalized;
|
|
226
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
3
|
-
import { createRuntimeBinding, RuntimeLaunchError } from "../runtime/index.js";
|
|
3
|
+
import { createRuntimeBinding, RuntimeHostContentionError, RuntimeLaunchError } from "../runtime/index.js";
|
|
4
4
|
import { effectiveLaunchSnapshotsCompatible, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
|
|
5
5
|
class RuntimeBindingContractError extends Error {
|
|
6
6
|
constructor(message, options) {
|
|
@@ -8,9 +8,22 @@ 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
|
+
class RuntimeLaunchStateChangedError extends Error {
|
|
18
|
+
constructor(message, options) {
|
|
19
|
+
super(message, options);
|
|
20
|
+
this.name = "RuntimeLaunchStateChangedError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
11
23
|
/**
|
|
12
24
|
* One application service owns the reservation -> host -> persistence
|
|
13
|
-
* protocol for
|
|
25
|
+
* protocol for Controller/scheduler-driven Role launches. Foreground Task
|
|
26
|
+
* attach is deliberately outside this lifecycle boundary.
|
|
14
27
|
*/
|
|
15
28
|
export class RuntimeLaunchCoordinator {
|
|
16
29
|
reservations;
|
|
@@ -90,7 +103,12 @@ export class RuntimeLaunchCoordinator {
|
|
|
90
103
|
...(request.runId === undefined ? {} : { runId: request.runId })
|
|
91
104
|
}, assertLaunchCurrent, this.#now());
|
|
92
105
|
let reusedConfirmedRunningHost = false;
|
|
93
|
-
|
|
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;
|
|
94
112
|
if (reservation.status === "existing") {
|
|
95
113
|
if (!reservation.launchId.startsWith(generationPrefix)) {
|
|
96
114
|
this.#requireCleanup(request.owner);
|
|
@@ -149,13 +167,12 @@ export class RuntimeLaunchCoordinator {
|
|
|
149
167
|
throw new Error(`Runtime launch reservation belongs to another Run whose host is still running: ${reservation.runId}.`);
|
|
150
168
|
}
|
|
151
169
|
reusedConfirmedRunningHost = true;
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
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.
|
|
156
174
|
launchPromptAcknowledgementRequired = sameRunReservation
|
|
157
|
-
&&
|
|
158
|
-
&& request.adapterId === "codex";
|
|
175
|
+
&& launchCarriesExactRunPrompt;
|
|
159
176
|
runtimeIsolation = this.#preflightRuntimeIsolation(request, reservation.launchId, reservation.launchId.slice(generationPrefix.length), true);
|
|
160
177
|
assertLaunchCurrent();
|
|
161
178
|
}
|
|
@@ -212,12 +229,53 @@ export class RuntimeLaunchCoordinator {
|
|
|
212
229
|
: { environment: request.environment }),
|
|
213
230
|
nativeSessionId: requireText(request.nativeSessionId, "Native session id")
|
|
214
231
|
}, beforeHostStart === undefined ? undefined : observePreflight);
|
|
215
|
-
|
|
232
|
+
binding = requireMatchingRuntimeBinding(rawBinding, request, launchId, launchPromptAcknowledgementRequired, reusedConfirmedRunningHost);
|
|
233
|
+
if (!preflightObserved
|
|
234
|
+
&& !(binding.hostCreated === false
|
|
235
|
+
&& request.adapterId === "claude"
|
|
236
|
+
&& launchCarriesExactRunPrompt)) {
|
|
216
237
|
throw new Error("Runtime session host did not expose a pre-host-start launch fence.");
|
|
217
238
|
}
|
|
218
|
-
binding = requireMatchingRuntimeBinding(rawBinding, request, launchId, launchPromptAcknowledgementRequired, reusedConfirmedRunningHost);
|
|
219
239
|
}
|
|
220
240
|
catch (error) {
|
|
241
|
+
if (error instanceof RuntimeHostContentionError && reusedConfirmedRunningHost) {
|
|
242
|
+
// The exact recovered generation remains authoritative. A late human
|
|
243
|
+
// writer is transient backpressure and must not settle, clean, or
|
|
244
|
+
// terminalize that reservation.
|
|
245
|
+
throw new RuntimeLaunchError(true, launchId, error.message, error.reason);
|
|
246
|
+
}
|
|
247
|
+
if ((error instanceof LaunchSubmittedHostBusyError
|
|
248
|
+
|| error instanceof RuntimeHostContentionError)
|
|
249
|
+
&& !reusedConfirmedRunningHost) {
|
|
250
|
+
let exactCleanupAttempted = false;
|
|
251
|
+
let completed = false;
|
|
252
|
+
try {
|
|
253
|
+
completed = this.reservations.completeRuntimeLaunchReservation(request.owner, launchId, undefined, runtimeIsolation === undefined
|
|
254
|
+
? undefined
|
|
255
|
+
: () => {
|
|
256
|
+
exactCleanupAttempted = true;
|
|
257
|
+
this.#runtimeIsolation.cleanup(runtimeIsolation, "failure");
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch (cleanupError) {
|
|
261
|
+
if (exactCleanupAttempted)
|
|
262
|
+
this.#requireCleanup(request.owner);
|
|
263
|
+
throw cleanupError;
|
|
264
|
+
}
|
|
265
|
+
if (!completed) {
|
|
266
|
+
this.#requireCleanup(request.owner);
|
|
267
|
+
throw new Error("Busy managed runtime launch reservation changed during retry release.");
|
|
268
|
+
}
|
|
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
|
+
throw new RuntimeLaunchError(true, launchId, error.message, error instanceof RuntimeHostContentionError
|
|
276
|
+
? error.reason
|
|
277
|
+
: "previous-process");
|
|
278
|
+
}
|
|
221
279
|
if (error instanceof RuntimeBindingContractError) {
|
|
222
280
|
// Never pass an untrusted hostRef to stop(). Reconcile the requested
|
|
223
281
|
// owner through the durable, owner-addressed cleanup lane instead.
|
|
@@ -259,17 +317,15 @@ export class RuntimeLaunchCoordinator {
|
|
|
259
317
|
catch (error) {
|
|
260
318
|
await this.#compensateStartedHost(request.owner, binding, launchId, runtimeIsolation, error);
|
|
261
319
|
}
|
|
262
|
-
if (
|
|
263
|
-
&& request.adapterId === "codex"
|
|
264
|
-
&& request.runId !== undefined
|
|
320
|
+
if (launchCarriesExactRunPrompt
|
|
265
321
|
&& binding.hostCreated !== false
|
|
266
322
|
&& reservationConfirmation !== "provider-bound"
|
|
267
323
|
&& binding.initialPromptRunId !== request.runId) {
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
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.
|
|
273
329
|
this.#requireCleanup(request.owner);
|
|
274
330
|
throw new RuntimeBindingContractError(`Session host cannot acknowledge the exact launch-carried prompt: ${request.owner.roleName}.`);
|
|
275
331
|
}
|
|
@@ -342,7 +398,8 @@ export class RuntimeLaunchCoordinator {
|
|
|
342
398
|
}, this.#now())) {
|
|
343
399
|
this.#requireCleanup(owner);
|
|
344
400
|
}
|
|
345
|
-
|
|
401
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
402
|
+
throw new RuntimeLaunchStateChangedError(`Runtime launch was compensated after state changed: ${detail}`, { cause });
|
|
346
403
|
}
|
|
347
404
|
#preflightRuntimeIsolation(request, launchId, generationId, allowExactActive) {
|
|
348
405
|
if (this.#runtimeIsolation === undefined || request.owner.scope !== "task") {
|
|
@@ -426,11 +483,6 @@ function requireMatchingRuntimeBinding(raw, request, launchId, launchPromptAckno
|
|
|
426
483
|
&& binding.initialPromptRunId !== request.runId) {
|
|
427
484
|
throw new RuntimeBindingContractError(`Session host returned a launch-carried prompt for another Run: ${request.owner.roleName}.`);
|
|
428
485
|
}
|
|
429
|
-
if (launchPromptAcknowledgementRequired
|
|
430
|
-
&& binding.initialPromptRunId !== request.runId
|
|
431
|
-
&& !(launchPromptUncertaintyAllowed && binding.hostCreated === false)) {
|
|
432
|
-
throw new RuntimeBindingContractError(`Session host cannot acknowledge the exact launch-carried prompt: ${request.owner.roleName}.`);
|
|
433
|
-
}
|
|
434
486
|
if (binding.launchId !== launchId
|
|
435
487
|
|| !ownerMatches
|
|
436
488
|
|| binding.agentId !== request.agentId
|
|
@@ -439,13 +491,20 @@ function requireMatchingRuntimeBinding(raw, request, launchId, launchPromptAckno
|
|
|
439
491
|
&& binding.nativeSessionId !== request.nativeSessionId)) {
|
|
440
492
|
throw new RuntimeBindingContractError(`Session host returned a binding that does not match the requested runtime: ${request.owner.roleName}.`);
|
|
441
493
|
}
|
|
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
|
+
}
|
|
442
501
|
if (launchPromptAcknowledgementRequired
|
|
443
502
|
&& launchPromptUncertaintyAllowed
|
|
444
503
|
&& binding.hostCreated === false
|
|
445
504
|
&& request.runId !== undefined
|
|
446
505
|
&& binding.initialPromptRunId !== request.runId) {
|
|
447
506
|
// A Controller restart can lose only the in-memory fact that a still-
|
|
448
|
-
// running
|
|
507
|
+
// running generation carried this Run at process launch. Keep the
|
|
449
508
|
// uncertainty explicitly tied to the exact reservation/Run; the matching
|
|
450
509
|
// Provider Hook remains the sole acceptance authority.
|
|
451
510
|
return {
|
|
@@ -455,6 +514,12 @@ function requireMatchingRuntimeBinding(raw, request, launchId, launchPromptAckno
|
|
|
455
514
|
}
|
|
456
515
|
return binding;
|
|
457
516
|
}
|
|
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
|
+
}
|
|
458
523
|
function defaultLaunchFingerprint(request) {
|
|
459
524
|
return createHash("sha256").update(JSON.stringify([
|
|
460
525
|
request.owner,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { callController } from "../core/controllerClient.js";
|
|
2
|
+
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
3
|
+
import { normalizeAgentDriverHookClassification } from "../runtime/agentDriver.js";
|
|
4
|
+
import { mapAgentDriverHook } from "../runtime/agentDriverObservation.js";
|
|
5
|
+
import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
|
|
6
|
+
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
7
|
+
import { FileRuntimeEventInbox, MAX_RUNTIME_EVENT_FILE_BYTES } from "./runtimeEventInbox.js";
|
|
8
|
+
import { resolveRuntimeHookRunFence } from "./runtimeHookRunFence.js";
|
|
9
|
+
/**
|
|
10
|
+
* Single hidden ingress for every structured CLI Driver Hook. Native event
|
|
11
|
+
* names and payload shapes terminate here; the durable inbox contains only a
|
|
12
|
+
* provider-independent RuntimeObservation with an exact generation/Run fence.
|
|
13
|
+
*/
|
|
14
|
+
export async function runRuntimeObservationHookCommand(stdinJson, environment = process.env, call = callController, now = new Date(), dependencies = {}) {
|
|
15
|
+
const parsed = parseRuntimeObservationHook(stdinJson, environment, now, dependencies);
|
|
16
|
+
const inbox = new FileRuntimeEventInbox(parsed.home);
|
|
17
|
+
for (const observation of parsed.observations)
|
|
18
|
+
inbox.enqueueObservation(observation);
|
|
19
|
+
// The immutable inbox write is authoritative. The socket call only reduces
|
|
20
|
+
// observation latency when the Controller is currently available.
|
|
21
|
+
await call(parsed.home, "scheduler.signal", {
|
|
22
|
+
key: runtimeLifecycleSignalKey({
|
|
23
|
+
scope: "task",
|
|
24
|
+
taskId: parsed.taskId,
|
|
25
|
+
roleName: parsed.roleName
|
|
26
|
+
})
|
|
27
|
+
}, { timeoutMs: 100 }).catch(() => { });
|
|
28
|
+
}
|
|
29
|
+
export function parseRuntimeObservationHook(stdinJson, environment, now = new Date(), dependencies = {}) {
|
|
30
|
+
const payload = parseObject(stdinJson);
|
|
31
|
+
const drivers = dependencies.drivers ?? builtinAgentDriverRegistry();
|
|
32
|
+
const driverId = requireIdentity(environment.YUI_DRIVER_ID, "Agent Driver id");
|
|
33
|
+
const driver = drivers.require(driverId);
|
|
34
|
+
const hookEventName = requireIdentity(payload.hook_event_name, "Agent Driver Hook event name");
|
|
35
|
+
const receivedAt = now.toISOString();
|
|
36
|
+
const sequence = (dependencies.sequence ?? monotonicSequence)();
|
|
37
|
+
const occurrenceId = `${receivedAt}:${sequence}`;
|
|
38
|
+
const nativeHook = Object.freeze({ hookEventName, payload, occurrenceId });
|
|
39
|
+
const nativeSessionId = requireIdentity(driver.runtime.nativeSessionId(nativeHook), "Agent Driver native Session id");
|
|
40
|
+
const nativeTurnId = optionalIdentity(driver.runtime.nativeTurnId(nativeHook));
|
|
41
|
+
const classification = normalizeAgentDriverHookClassification(driver.runtime.classifyHook(nativeHook));
|
|
42
|
+
const fence = (dependencies.resolveRunFence ?? resolveRuntimeHookRunFence)(environment, driver.adapterId, nativeSessionId, {
|
|
43
|
+
...(classification.startupSession === undefined
|
|
44
|
+
? {}
|
|
45
|
+
: { startupSession: classification.startupSession }),
|
|
46
|
+
terminal: classification.terminal,
|
|
47
|
+
...(nativeTurnId === undefined ? {} : { nativeTurnId })
|
|
48
|
+
});
|
|
49
|
+
const driverInput = {
|
|
50
|
+
driver,
|
|
51
|
+
hookEventName,
|
|
52
|
+
receivedAt,
|
|
53
|
+
// Hook commands run in separate short-lived processes. CLOCK_MONOTONIC is
|
|
54
|
+
// shared by those processes on the host, so this preserves their order
|
|
55
|
+
// when wall-clock timestamps land in the same millisecond.
|
|
56
|
+
sequence,
|
|
57
|
+
occurrenceId,
|
|
58
|
+
fence: {
|
|
59
|
+
taskId: fence.taskId,
|
|
60
|
+
roleName: fence.roleName,
|
|
61
|
+
runId: fence.runId,
|
|
62
|
+
agentId: fence.agentId,
|
|
63
|
+
driverId,
|
|
64
|
+
launchId: fence.launchId,
|
|
65
|
+
sessionGenerationId: fence.launchId,
|
|
66
|
+
nativeSessionId: fence.nativeSessionId,
|
|
67
|
+
nativeTurnId: nativeTurnId ?? fence.runId,
|
|
68
|
+
receiptId: fence.receiptId ?? formatAgentRunReceiptId(fence.taskId, fence.runId)
|
|
69
|
+
},
|
|
70
|
+
payload
|
|
71
|
+
};
|
|
72
|
+
const observations = [mapAgentDriverHook({ ...driverInput, ordinal: 0 })];
|
|
73
|
+
return {
|
|
74
|
+
home: requireIdentity(environment.YUI_HOME, "YUI_HOME"),
|
|
75
|
+
taskId: fence.taskId,
|
|
76
|
+
roleName: fence.roleName,
|
|
77
|
+
observations: Object.freeze(observations)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function monotonicSequence() {
|
|
81
|
+
return Number(process.hrtime.bigint() / 1000n);
|
|
82
|
+
}
|
|
83
|
+
function parseObject(value) {
|
|
84
|
+
if (value === undefined
|
|
85
|
+
|| Buffer.byteLength(value, "utf8") > MAX_RUNTIME_EVENT_FILE_BYTES) {
|
|
86
|
+
throw new Error("Agent Driver lifecycle Hook stdin JSON is invalid.");
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(value);
|
|
90
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
91
|
+
throw new Error("shape");
|
|
92
|
+
return parsed;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
throw new Error("Agent Driver lifecycle Hook stdin JSON is invalid.");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function optionalIdentity(value) {
|
|
99
|
+
if (value === undefined || value === null)
|
|
100
|
+
return undefined;
|
|
101
|
+
return requireIdentity(value, "Agent Driver native Turn id");
|
|
102
|
+
}
|
|
103
|
+
function requireIdentity(value, label) {
|
|
104
|
+
if (typeof value !== "string" || value.includes("\0")) {
|
|
105
|
+
throw new Error(`${label} is required.`);
|
|
106
|
+
}
|
|
107
|
+
const normalized = value.trim();
|
|
108
|
+
if (normalized.length === 0 || normalized.length > 1_024) {
|
|
109
|
+
throw new Error(`${label} is invalid.`);
|
|
110
|
+
}
|
|
111
|
+
return normalized;
|
|
112
|
+
}
|
|
@@ -732,6 +732,11 @@ function safeDispatcherError(error) {
|
|
|
732
732
|
}
|
|
733
733
|
return { code: "SERVICE_ERROR", message };
|
|
734
734
|
}
|
|
735
|
+
case "RuntimeLaunchStateChangedError":
|
|
736
|
+
// The launch coordinator already stopped the exact host and bounded
|
|
737
|
+
// the detail to its currentness/reservation contract. Surface that
|
|
738
|
+
// actionable diagnosis instead of collapsing it to INTERNAL_ERROR.
|
|
739
|
+
return { code: "SERVICE_ERROR", message };
|
|
735
740
|
default:
|
|
736
741
|
return undefined;
|
|
737
742
|
}
|
|
@@ -6,7 +6,7 @@ import { ownedArgumentsForAdapter, validateAgentAdvancedArguments, validateAgent
|
|
|
6
6
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
7
7
|
import { inspectCodexDeveloperInstructions } from "./codexConfigConflict.js";
|
|
8
8
|
import { discoverClaudeConfiguration, discoverCodexConfiguration } from "./agentConfigurationProbe.js";
|
|
9
|
-
import {
|
|
9
|
+
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
10
10
|
const SANDBOXES = ["read-only", "workspace-write", "danger-full-access"];
|
|
11
11
|
const APPROVALS = ["untrusted", "on-request", "never"];
|
|
12
12
|
const PROBE_TIMEOUT_MS = 2_000;
|
|
@@ -57,7 +57,7 @@ class CodexAdapter extends BaseAdapter {
|
|
|
57
57
|
recover: true,
|
|
58
58
|
interrupt: true,
|
|
59
59
|
nativeSessionDiscovery: "runtime",
|
|
60
|
-
preInputReadiness:
|
|
60
|
+
preInputReadiness: driverPreInputReadiness("codex")
|
|
61
61
|
};
|
|
62
62
|
discoverConfiguration(input) {
|
|
63
63
|
return discoverCodexConfiguration(input);
|
|
@@ -166,7 +166,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
166
166
|
recover: true,
|
|
167
167
|
interrupt: true,
|
|
168
168
|
nativeSessionDiscovery: "preallocated",
|
|
169
|
-
preInputReadiness:
|
|
169
|
+
preInputReadiness: driverPreInputReadiness("claude")
|
|
170
170
|
};
|
|
171
171
|
discoverConfiguration(input) {
|
|
172
172
|
return discoverClaudeConfiguration(input);
|
|
@@ -257,6 +257,21 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
257
257
|
return { ...launch, argv: [...launch.argv, "--resume", nativeId(input.nativeSessionId)] };
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
|
+
function driverPreInputReadiness(adapterId) {
|
|
261
|
+
const capability = builtinAgentDriverRegistry().requireByAdapterId(adapterId)
|
|
262
|
+
.capabilities.observation.preInputReadiness;
|
|
263
|
+
return capability === "exact"
|
|
264
|
+
? Object.freeze({
|
|
265
|
+
status: "supported",
|
|
266
|
+
nativeEvent: "Agent Driver session.ready",
|
|
267
|
+
note: "The registered Agent Driver supplies an exact pre-input readiness observation."
|
|
268
|
+
})
|
|
269
|
+
: Object.freeze({
|
|
270
|
+
status: "unsupported",
|
|
271
|
+
reason: "not-available",
|
|
272
|
+
note: "The registered Agent Driver does not expose exact pre-input readiness."
|
|
273
|
+
});
|
|
274
|
+
}
|
|
260
275
|
const ADAPTERS = {
|
|
261
276
|
codex: new CodexAdapter(), claude: new ClaudeAdapter()
|
|
262
277
|
};
|