@zq-silk/yui 0.6.16 → 0.7.0
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/dist/cli/commandCatalog.js +3 -7
- package/dist/cli.js +12 -33
- package/dist/commands/executionAuditCommands.js +19 -0
- package/dist/commands/globalRoleCommands.js +70 -0
- package/dist/commands/taskActor.js +3 -2
- package/dist/commands/taskCommands.js +160 -41
- package/dist/commands/taskContextCommand.js +1 -1
- package/dist/commands/taskInputCommands.js +3 -2
- package/dist/commands/taskRoleRuntimeStatus.js +3 -3
- package/dist/context/contextSnapshot.js +228 -0
- package/dist/context/roleSessionContext.js +3 -1
- package/dist/context/runContextContract.js +162 -0
- package/dist/context/runContextPack.js +322 -0
- package/dist/context/sessionBootstrapManifest.js +81 -0
- package/dist/context/sessionProtocolIdentity.js +23 -0
- package/dist/controller/agentRuntimeObserver.js +6 -1
- package/dist/controller/controller.js +4 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +446 -145
- package/dist/controller/jobControl.js +2 -1
- package/dist/controller/runtime.js +83 -0
- package/dist/controller/runtimeHookRunFence.js +6 -2
- package/dist/controller/sessionOwnerReconciliation.js +5 -0
- package/dist/executor/agentAdapter.js +7 -2
- package/dist/executor/agentExecutor.js +23 -0
- package/dist/executor/effectiveLaunch.js +24 -0
- package/dist/executor/executorRegistry.js +7 -1
- package/dist/executor/fileRoleLaunchPlanner.js +73 -27
- package/dist/lifecycle/exactRunTerminalization.js +2 -3
- package/dist/lifecycle/providerErrorClass.js +8 -3
- package/dist/observability/executionAudit.js +87 -2
- package/dist/repository/taskWorkspacePreparer.js +2 -2
- package/dist/run/agentRun.js +101 -16
- package/dist/run/providerRetry.js +167 -56
- package/dist/run/providerRetryConfig.js +5 -1
- package/dist/run/runControlRequest.js +50 -0
- package/dist/runtime/agentDriver.js +47 -0
- package/dist/runtime/agentHost.js +327 -0
- package/dist/runtime/builtinAgentDrivers.js +23 -1
- package/dist/runtime/builtinTranscriptObserver.js +4 -0
- package/dist/runtime/builtinTranscriptUsage.js +2 -0
- package/dist/runtime/exactControlPlane.js +2 -2
- package/dist/runtime/globalProcessExitStore.js +38 -0
- package/dist/runtime/launchBroker.js +95 -0
- package/dist/runtime/processExitObservation.js +60 -0
- package/dist/runtime/runtimeBinding.js +6 -0
- package/dist/runtime/runtimeObservation.js +27 -6
- package/dist/runtime/runtimeProjection.js +6 -3
- package/dist/runtime/runtimeStopReceipt.js +42 -0
- package/dist/runtime/sessionTerminationGuard.js +13 -0
- package/dist/runtime/tmuxAdapters.js +203 -220
- package/dist/scheduler/activeRoleRunDelivery.js +24 -3
- package/dist/scheduler/leaderWakeupProcessor.js +18 -60
- package/dist/scheduler/roleRunLiveness.js +61 -27
- package/dist/storage/migration/productionRegistry.js +264 -0
- package/dist/storage/sqliteSchema.js +23 -2
- package/dist/storage/sqliteStore.js +39 -2
- package/dist/storage/taskStore.js +54 -5
- package/dist/storage/upgrade/recordVersions.js +3 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +10 -0
- package/dist/task/taskRecordReference.js +1 -0
- package/dist/tmux/tmuxManager.js +15 -4
- package/dist/web/assets/client/components.js +1 -1
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +10 -5
- package/skills/yui-operator/SKILL.md +4 -0
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +61 -0
- package/skills/yui-worker/SKILL.md +82 -218
- package/dist/executor/managedClaudeRunner.js +0 -121
|
@@ -274,8 +274,12 @@ function normalizePayload(kind, input) {
|
|
|
274
274
|
if (!["model", "tool", "subagent", "provider", "resource"].includes(input.activity ?? "")) {
|
|
275
275
|
throw new Error("activity.observed requires an activity kind.");
|
|
276
276
|
}
|
|
277
|
-
if (input.usage !== undefined)
|
|
278
|
-
|
|
277
|
+
if (input.usage !== undefined) {
|
|
278
|
+
const usage = input.usage;
|
|
279
|
+
validateUsage(usage.semantics === undefined
|
|
280
|
+
? { ...usage, semantics: "cumulative-session" }
|
|
281
|
+
: usage);
|
|
282
|
+
}
|
|
279
283
|
}
|
|
280
284
|
if (kind === "observer.health") {
|
|
281
285
|
requireIdentity(input.sourceId, "Runtime observer source id");
|
|
@@ -339,7 +343,12 @@ function normalizePayload(kind, input) {
|
|
|
339
343
|
...(input.activityId === undefined
|
|
340
344
|
? {}
|
|
341
345
|
: { activityId: requireIdentity(input.activityId, "Runtime activity id") }),
|
|
342
|
-
...(input.usage === undefined
|
|
346
|
+
...(input.usage === undefined
|
|
347
|
+
? {}
|
|
348
|
+
: { usage: Object.freeze({
|
|
349
|
+
...input.usage,
|
|
350
|
+
semantics: input.usage.semantics ?? "cumulative-session"
|
|
351
|
+
}) }),
|
|
343
352
|
...(observerSource === undefined ? {} : { observerSource }),
|
|
344
353
|
...(input.sourceId === undefined
|
|
345
354
|
? {}
|
|
@@ -468,7 +477,10 @@ function normalizeFailure(input) {
|
|
|
468
477
|
: { lastOutput: requireText(input.lastOutput, "Runtime failure last output") }),
|
|
469
478
|
...(input.runTerminal === undefined
|
|
470
479
|
? {}
|
|
471
|
-
: { runTerminal: requireBoolean(input.runTerminal, "Runtime failure runTerminal") })
|
|
480
|
+
: { runTerminal: requireBoolean(input.runTerminal, "Runtime failure runTerminal") }),
|
|
481
|
+
...(input.retryAfterMs === undefined
|
|
482
|
+
? {}
|
|
483
|
+
: { retryAfterMs: requirePositiveMilliseconds(input.retryAfterMs) })
|
|
472
484
|
});
|
|
473
485
|
}
|
|
474
486
|
function requireBoolean(value, label) {
|
|
@@ -476,9 +488,18 @@ function requireBoolean(value, label) {
|
|
|
476
488
|
throw new Error(`${label} must be boolean.`);
|
|
477
489
|
return value;
|
|
478
490
|
}
|
|
491
|
+
function requirePositiveMilliseconds(value) {
|
|
492
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
493
|
+
throw new Error("Runtime failure retryAfterMs must be a positive safe integer.");
|
|
494
|
+
}
|
|
495
|
+
return value;
|
|
496
|
+
}
|
|
479
497
|
function validateUsage(input) {
|
|
480
|
-
|
|
481
|
-
|
|
498
|
+
if (!["cumulative-session", "request-context", "remaining-context"].includes(input.semantics)) {
|
|
499
|
+
throw new Error("Runtime usage semantics are invalid.");
|
|
500
|
+
}
|
|
501
|
+
for (const [name, value] of Object.entries(input).filter(([name]) => name !== "semantics")) {
|
|
502
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
482
503
|
throw new Error(`Runtime usage ${name} must be a non-negative safe integer.`);
|
|
483
504
|
}
|
|
484
505
|
}
|
|
@@ -489,10 +489,13 @@ function next(current, patch) {
|
|
|
489
489
|
});
|
|
490
490
|
}
|
|
491
491
|
function usageAdvanced(previous, current) {
|
|
492
|
+
if (current.semantics === "remaining-context")
|
|
493
|
+
return false;
|
|
494
|
+
if (current.semantics === "request-context")
|
|
495
|
+
return usageTotal(current) > 0;
|
|
492
496
|
// A first cumulative snapshot may contain history from a resumed native
|
|
493
|
-
// Session. It establishes
|
|
494
|
-
|
|
495
|
-
if (previous === undefined)
|
|
497
|
+
// Session. It establishes a baseline but cannot prove current progress.
|
|
498
|
+
if (previous === undefined || previous.semantics !== "cumulative-session")
|
|
496
499
|
return false;
|
|
497
500
|
return usageTotal(current) > usageTotal(previous);
|
|
498
501
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
export function writeRuntimeStopReceipt(home, launchId, requestedAt) {
|
|
5
|
+
const receipt = Object.freeze({
|
|
6
|
+
schemaVersion: 1,
|
|
7
|
+
receiptId: `runtime-stop-${createHash("sha256")
|
|
8
|
+
.update(`${launchId}\0${requestedAt.toISOString()}`)
|
|
9
|
+
.digest("hex")}`,
|
|
10
|
+
launchId,
|
|
11
|
+
requestedAt: requestedAt.toISOString()
|
|
12
|
+
});
|
|
13
|
+
const path = stopReceiptPath(home, launchId);
|
|
14
|
+
mkdirSync(resolve(join(home, "runtime", "stop-receipts")), { recursive: true, mode: 0o700 });
|
|
15
|
+
const temporary = `${path}.tmp-${process.pid}`;
|
|
16
|
+
writeFileSync(temporary, `${JSON.stringify(receipt)}\n`, { mode: 0o600 });
|
|
17
|
+
renameSync(temporary, path);
|
|
18
|
+
chmodSync(path, 0o600);
|
|
19
|
+
return receipt;
|
|
20
|
+
}
|
|
21
|
+
export function readRuntimeStopReceipt(home, launchId) {
|
|
22
|
+
try {
|
|
23
|
+
const value = JSON.parse(readFileSync(stopReceiptPath(home, launchId), "utf8"));
|
|
24
|
+
if (value.schemaVersion !== 1 || value.launchId !== launchId
|
|
25
|
+
|| typeof value.receiptId !== "string" || !Number.isFinite(Date.parse(value.requestedAt))) {
|
|
26
|
+
throw new Error("Runtime stop receipt is invalid.");
|
|
27
|
+
}
|
|
28
|
+
return Object.freeze({ ...value });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code === "ENOENT")
|
|
32
|
+
return null;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function removeRuntimeStopReceipt(home, launchId) {
|
|
37
|
+
rmSync(stopReceiptPath(home, launchId), { force: true });
|
|
38
|
+
}
|
|
39
|
+
function stopReceiptPath(home, launchId) {
|
|
40
|
+
const name = createHash("sha256").update(launchId).digest("hex");
|
|
41
|
+
return resolve(join(home, "runtime", "stop-receipts", `${name}.json`));
|
|
42
|
+
}
|
|
@@ -20,6 +20,19 @@ export async function terminateSessionOwners(owner, records, ports, options = {}
|
|
|
20
20
|
const pollMs = positiveDuration(options.pollMs, DEFAULT_TERMINATION_POLL_MS, "pollMs");
|
|
21
21
|
const now = ports.now();
|
|
22
22
|
ports.emit({ stage: "stop-requested", owner, at: now });
|
|
23
|
+
// Persist one exact launch receipt before any graceful stop can kill the
|
|
24
|
+
// Host. The owner-wide event remains for compatibility and summary display.
|
|
25
|
+
for (const record of records) {
|
|
26
|
+
ports.emit({
|
|
27
|
+
stage: "stop-requested",
|
|
28
|
+
owner,
|
|
29
|
+
launchId: record.launchId,
|
|
30
|
+
...(record.nativeSessionId === undefined
|
|
31
|
+
? {}
|
|
32
|
+
: { nativeSessionId: record.nativeSessionId }),
|
|
33
|
+
at: now
|
|
34
|
+
});
|
|
35
|
+
}
|
|
23
36
|
// A launch-fence scan is a point-in-time observation, not a durable owner
|
|
24
37
|
// inventory: /proc entries can disappear between the directory and
|
|
25
38
|
// environment reads. Retain every exact child identity observed during the
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { resolve } from "node:path";
|
|
2
4
|
import { createRuntimeBinding } from "./runtimeBinding.js";
|
|
3
5
|
import { normalizeRuntimeOwner } from "./runtimeOwner.js";
|
|
4
|
-
import { RuntimeHostContentionError
|
|
5
|
-
import { toRuntimeLaunchFailure
|
|
6
|
-
import { builtinAgentDriverRegistry } from "./builtinAgentDrivers.js";
|
|
6
|
+
import { RuntimeHostContentionError } from "./ports.js";
|
|
7
|
+
import { toRuntimeLaunchFailure } from "./launchDiagnostics.js";
|
|
7
8
|
import { requireSafeIdentity } from "./validation.js";
|
|
9
|
+
import { launchBrokerForHome } from "./launchBroker.js";
|
|
10
|
+
import { AGENT_HOST_CONTROL_PROTOCOL, sendAgentHostLaunchControl } from "./agentHost.js";
|
|
8
11
|
const DEFAULT_INACTIVITY_TIMEOUT_MS = 300_000;
|
|
9
12
|
/**
|
|
10
13
|
* Runtime lifecycle adapter for the current tmux host. The returned hostRef is
|
|
@@ -163,16 +166,74 @@ export class TmuxSessionHost {
|
|
|
163
166
|
if (writableHumanAttached) {
|
|
164
167
|
throw new RuntimeHostContentionError("writable-client", `A writable human is attached to ${request.owner.taskId}/${request.owner.roleName}.`);
|
|
165
168
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
169
|
+
const input = {
|
|
170
|
+
roleName: request.owner.roleName,
|
|
171
|
+
agentId: request.agentId,
|
|
172
|
+
adapterId: request.adapterId,
|
|
173
|
+
effective: request.effective,
|
|
174
|
+
launchId: request.launchId,
|
|
175
|
+
mode: request.mode,
|
|
176
|
+
...(request.runId === undefined ? {} : { runId: request.runId }),
|
|
177
|
+
...(request.runtimeIsolation === undefined
|
|
178
|
+
? {}
|
|
179
|
+
: { runtimeIsolation: request.runtimeIsolation }),
|
|
180
|
+
...(request.environment === undefined
|
|
181
|
+
? {}
|
|
182
|
+
: { environment: request.environment }),
|
|
183
|
+
...(request.mode === "resume" ? { nativeSessionId: request.nativeSessionId } : {})
|
|
184
|
+
};
|
|
185
|
+
const planned = request.owner.scope === "task"
|
|
186
|
+
? this.planner.plan({ taskId: request.owner.taskId, ...input })
|
|
187
|
+
: this.planner.planGlobalRole(input);
|
|
188
|
+
if (planned.role.name !== request.owner.roleName) {
|
|
189
|
+
throw new Error("Planned Role does not match the runtime owner.");
|
|
190
|
+
}
|
|
191
|
+
if (planned.role.workspace !== request.workspace) {
|
|
192
|
+
throw new Error("Planned Role workspace does not match the runtime request.");
|
|
193
|
+
}
|
|
194
|
+
const launchContext = diagnosticContext(request, planned);
|
|
195
|
+
if (this.#validateLaunch !== undefined) {
|
|
196
|
+
try {
|
|
197
|
+
await this.#validateLaunch(request);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
throw toRuntimeLaunchFailure(error, "validation", launchContext);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const plannedNativeSessionId = planned.session?.nativeSessionId;
|
|
204
|
+
if (request.mode === "resume"
|
|
205
|
+
&& plannedNativeSessionId !== undefined
|
|
206
|
+
&& plannedNativeSessionId !== request.nativeSessionId) {
|
|
207
|
+
throw new Error("Planned native session does not match the resume request.");
|
|
208
|
+
}
|
|
209
|
+
const nativeSessionId = plannedNativeSessionId
|
|
210
|
+
?? (request.mode === "resume" ? request.nativeSessionId : undefined);
|
|
211
|
+
beforeHostStart?.({
|
|
212
|
+
owner: request.owner,
|
|
213
|
+
launchId: request.launchId,
|
|
214
|
+
...(request.runId === undefined ? {} : { runId: request.runId }),
|
|
215
|
+
agentId: request.agentId,
|
|
216
|
+
adapterId: request.adapterId,
|
|
217
|
+
effective: request.effective,
|
|
218
|
+
...(nativeSessionId === undefined ? {} : { nativeSessionId }),
|
|
219
|
+
...(planned.initialPromptRunId === undefined
|
|
220
|
+
? {}
|
|
221
|
+
: { initialPromptRunId: planned.initialPromptRunId })
|
|
222
|
+
});
|
|
223
|
+
const yuiHome = planned.launch.env.YUI_HOME;
|
|
224
|
+
const childLifecycle = planned.launch.childLifecycle;
|
|
225
|
+
// Custom/legacy planners do not yet expose the Agent Host contract. Keep
|
|
226
|
+
// their direct launch behavior for compatibility while all built-in
|
|
227
|
+
// managed Roles use the persistent Host below.
|
|
228
|
+
if (yuiHome === undefined || childLifecycle === undefined) {
|
|
229
|
+
let hostCreated;
|
|
230
|
+
try {
|
|
231
|
+
hostCreated = await ensureRoleWindow(this.tmux, hostId, planned.role, planned.launch);
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
throw toRuntimeLaunchFailure(error, "host-start", launchContext);
|
|
235
|
+
}
|
|
236
|
+
let binding = createRuntimeBinding({
|
|
176
237
|
id: bindingId,
|
|
177
238
|
launchId: request.launchId,
|
|
178
239
|
owner: request.owner,
|
|
@@ -183,31 +244,99 @@ export class TmuxSessionHost {
|
|
|
183
244
|
hostId,
|
|
184
245
|
roleName: request.owner.roleName
|
|
185
246
|
}),
|
|
186
|
-
hostCreated
|
|
187
|
-
...(
|
|
188
|
-
? {
|
|
189
|
-
: {})
|
|
247
|
+
hostCreated,
|
|
248
|
+
...(hostCreated && planned.initialPromptRunId !== undefined
|
|
249
|
+
? { initialPromptRunId: planned.initialPromptRunId }
|
|
250
|
+
: {}),
|
|
251
|
+
...(nativeSessionId === undefined ? {} : { nativeSessionId })
|
|
190
252
|
});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
253
|
+
if (hostCreated) {
|
|
254
|
+
const pane = await inspectRolePane(this.tmux, hostId, request.owner.roleName);
|
|
255
|
+
if (pane?.dead === true) {
|
|
256
|
+
await deadHostLaunchFailure(this.tmux, hostId, request.owner.roleName, pane, launchContext);
|
|
257
|
+
}
|
|
258
|
+
if (request.mode === "new"
|
|
259
|
+
&& request.owner.scope === "task"
|
|
260
|
+
&& request.runId !== undefined
|
|
261
|
+
&& planned.initialPromptRunId === request.runId
|
|
262
|
+
&& this.#waitForNativeSession !== undefined) {
|
|
263
|
+
binding = createRuntimeBinding({
|
|
264
|
+
...binding,
|
|
265
|
+
nativeSessionId: await this.waitForNativeSessionDiscovery(request, hostId, launchContext)
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (pane !== undefined)
|
|
269
|
+
this.#onHostCreated?.({ binding, pane });
|
|
200
270
|
}
|
|
271
|
+
return binding;
|
|
201
272
|
}
|
|
202
|
-
|
|
273
|
+
const broker = launchBrokerForHome(yuiHome);
|
|
274
|
+
const reservation = broker.reserve(Object.freeze({
|
|
275
|
+
schemaVersion: 1,
|
|
276
|
+
launchId: request.launchId,
|
|
277
|
+
command: planned.launch.command,
|
|
278
|
+
args: [...planned.launch.args],
|
|
279
|
+
environment: { ...planned.launch.env },
|
|
280
|
+
cwd: planned.role.cwd ?? planned.role.workspace,
|
|
281
|
+
childLifecycle,
|
|
282
|
+
startMode: planned.launch.deferProviderStart === true ? "idle" : "provider",
|
|
283
|
+
...(planned.launch.providerInput === undefined
|
|
284
|
+
? {}
|
|
285
|
+
: { providerInput: planned.launch.providerInput })
|
|
286
|
+
}));
|
|
287
|
+
const hostLaunch = {
|
|
288
|
+
command: process.execPath,
|
|
289
|
+
args: [
|
|
290
|
+
fileURLToPath(new URL("../cli.js", import.meta.url)),
|
|
291
|
+
"internal",
|
|
292
|
+
"agent-host",
|
|
293
|
+
reservation.launchId,
|
|
294
|
+
reservation.ticket
|
|
295
|
+
],
|
|
296
|
+
env: {
|
|
297
|
+
YUI_HOME: resolve(yuiHome),
|
|
298
|
+
YUI_SESSION_SCOPE: request.owner.scope,
|
|
299
|
+
...(request.owner.scope === "task" ? { YUI_TASK_ID: request.owner.taskId } : {}),
|
|
300
|
+
YUI_ROLE: request.owner.roleName,
|
|
301
|
+
YUI_LAUNCH_ID: request.launchId
|
|
302
|
+
}
|
|
303
|
+
};
|
|
203
304
|
let hostCreated;
|
|
305
|
+
let providerChildLaunched = false;
|
|
306
|
+
let launchPromptAlreadySubmitted = false;
|
|
204
307
|
try {
|
|
205
|
-
hostCreated = await ensureRoleWindow(this.tmux, hostId, planned.role,
|
|
308
|
+
hostCreated = await ensureRoleWindow(this.tmux, hostId, planned.role, hostLaunch);
|
|
309
|
+
providerChildLaunched = hostCreated && planned.launch.deferProviderStart !== true;
|
|
310
|
+
if (!hostCreated) {
|
|
311
|
+
const controlResult = await sendAgentHostLaunchControl({
|
|
312
|
+
home: yuiHome,
|
|
313
|
+
scope: request.owner.scope,
|
|
314
|
+
...(request.owner.scope === "task" ? { taskId: request.owner.taskId } : {}),
|
|
315
|
+
roleName: request.owner.roleName,
|
|
316
|
+
control: {
|
|
317
|
+
protocol: AGENT_HOST_CONTROL_PROTOCOL,
|
|
318
|
+
type: "launch",
|
|
319
|
+
launchId: reservation.launchId,
|
|
320
|
+
ticket: reservation.ticket
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
if (controlResult !== "accepted") {
|
|
324
|
+
broker.revoke(request.launchId);
|
|
325
|
+
launchPromptAlreadySubmitted = controlResult === "active-same-launch";
|
|
326
|
+
if (childLifecycle === "per-turn" && controlResult === "active-other-launch") {
|
|
327
|
+
throw new RuntimeHostContentionError("provider-child-active", `The persistent Agent Host for ${request.owner.roleName} still owns another Provider turn.`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
providerChildLaunched = planned.launch.deferProviderStart !== true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
206
334
|
}
|
|
207
335
|
catch (error) {
|
|
208
|
-
|
|
336
|
+
broker.revoke(request.launchId);
|
|
337
|
+
throw error;
|
|
209
338
|
}
|
|
210
|
-
if (
|
|
339
|
+
if (providerChildLaunched
|
|
211
340
|
&& request.owner.scope === "task"
|
|
212
341
|
&& planned.launch.env.YUI_JOB_CALLER_KEY !== undefined
|
|
213
342
|
&& this.planner.commitTaskCallerKey !== undefined) {
|
|
@@ -218,7 +347,7 @@ export class TmuxSessionHost {
|
|
|
218
347
|
callerKey: planned.launch.env.YUI_JOB_CALLER_KEY
|
|
219
348
|
});
|
|
220
349
|
}
|
|
221
|
-
|
|
350
|
+
const binding = createRuntimeBinding({
|
|
222
351
|
id: bindingId,
|
|
223
352
|
launchId: request.launchId,
|
|
224
353
|
owner: request.owner,
|
|
@@ -230,218 +359,66 @@ export class TmuxSessionHost {
|
|
|
230
359
|
roleName: request.owner.roleName
|
|
231
360
|
}),
|
|
232
361
|
hostCreated,
|
|
233
|
-
...(hostCreated && planned.initialPromptRunId !== undefined
|
|
362
|
+
...(providerChildLaunched && hostCreated && planned.initialPromptRunId !== undefined
|
|
234
363
|
? { initialPromptRunId: planned.initialPromptRunId }
|
|
235
364
|
: {}),
|
|
365
|
+
...((launchPromptAlreadySubmitted || (providerChildLaunched && !hostCreated))
|
|
366
|
+
&& planned.initialPromptRunId !== undefined
|
|
367
|
+
? { launchPromptUncertainRunId: planned.initialPromptRunId }
|
|
368
|
+
: {}),
|
|
236
369
|
...(nativeSessionId === undefined ? {} : { nativeSessionId })
|
|
237
370
|
});
|
|
238
|
-
if (hostCreated) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
catch (error) {
|
|
244
|
-
throw toRuntimeLaunchFailure(error, "host-started", launchContext);
|
|
245
|
-
}
|
|
246
|
-
if (pane === undefined) {
|
|
247
|
-
throw toRuntimeLaunchFailure(new Error("Managed host pane could not be inspected after creation."), "host-started", launchContext);
|
|
248
|
-
}
|
|
249
|
-
this.#onHostCreated?.({ binding, pane });
|
|
250
|
-
if (pane.dead) {
|
|
251
|
-
await deadHostLaunchFailure(this.tmux, hostId, request.owner.roleName, pane, launchContext);
|
|
252
|
-
}
|
|
253
|
-
if (requiresNativeSessionDiscovery(request, planned, this.#waitForNativeSession)) {
|
|
254
|
-
const discoveredNativeSessionId = await this.waitForNativeSessionDiscovery(request, hostId, launchContext, pane);
|
|
255
|
-
binding = createRuntimeBinding({
|
|
256
|
-
...binding,
|
|
257
|
-
nativeSessionId: discoveredNativeSessionId
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
this.#onHostCreated?.({ binding, pane });
|
|
371
|
+
if (hostCreated && this.#onHostCreated !== undefined) {
|
|
372
|
+
const pane = await inspectRolePane(this.tmux, hostId, request.owner.roleName);
|
|
373
|
+
if (pane !== undefined)
|
|
374
|
+
this.#onHostCreated({ binding, pane });
|
|
261
375
|
}
|
|
262
376
|
return binding;
|
|
263
377
|
}
|
|
264
|
-
async waitForNativeSessionDiscovery(request, hostId, context
|
|
265
|
-
if (this.#waitForNativeSession === undefined) {
|
|
266
|
-
throw new Error("Native session discovery is not configured.");
|
|
267
|
-
}
|
|
378
|
+
async waitForNativeSessionDiscovery(request, hostId, context) {
|
|
268
379
|
const controller = new AbortController();
|
|
269
380
|
const discovery = this.#waitForNativeSession(request, controller.signal);
|
|
270
|
-
// The losing branch of the race rejects when the controller aborts; keep
|
|
271
|
-
// that rejection handled so a settled launch does not surface it later.
|
|
272
381
|
discovery.catch(() => undefined);
|
|
382
|
+
let stopped = false;
|
|
383
|
+
let lastContent = "";
|
|
384
|
+
let lastActivityAt = Date.now();
|
|
385
|
+
const monitor = (async () => {
|
|
386
|
+
while (!stopped) {
|
|
387
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(1_000, this.#inactivityTimeoutMs)));
|
|
388
|
+
const pane = await inspectRolePane(this.tmux, hostId, request.owner.roleName);
|
|
389
|
+
if (pane?.dead === true) {
|
|
390
|
+
await deadHostLaunchFailure(this.tmux, hostId, request.owner.roleName, pane, context);
|
|
391
|
+
}
|
|
392
|
+
const content = this.tmux.captureRolePane?.(hostId, request.owner.roleName, 80) ?? "";
|
|
393
|
+
if (content !== lastContent) {
|
|
394
|
+
lastContent = content;
|
|
395
|
+
lastActivityAt = Date.now();
|
|
396
|
+
}
|
|
397
|
+
if (Date.now() - lastActivityAt >= this.#inactivityTimeoutMs) {
|
|
398
|
+
throw new Error(`Agent produced no signal for ${this.#inactivityTimeoutMs}ms. `
|
|
399
|
+
+ "The process is alive but emitted no lifecycle hook, output, or exit.");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
throw new Error("Native session discovery monitor stopped.");
|
|
403
|
+
})();
|
|
404
|
+
monitor.catch(() => undefined);
|
|
273
405
|
try {
|
|
274
|
-
return await Promise.race([
|
|
275
|
-
discovery,
|
|
276
|
-
this.monitorPaneSignals(request, hostId, context, controller.signal)
|
|
277
|
-
]);
|
|
406
|
+
return await Promise.race([discovery, monitor]);
|
|
278
407
|
}
|
|
279
408
|
catch (error) {
|
|
280
409
|
try {
|
|
281
410
|
await stopExactRole(this.tmux, hostId, request.owner.roleName);
|
|
282
411
|
}
|
|
283
412
|
catch {
|
|
284
|
-
//
|
|
285
|
-
// remains responsible for a Provider that rejected immediate stop.
|
|
413
|
+
// Preserve the discovery failure; durable cleanup owns later retries.
|
|
286
414
|
}
|
|
287
|
-
throw toRuntimeLaunchFailure(error, "native-session-discovery",
|
|
288
|
-
...context,
|
|
289
|
-
pane
|
|
290
|
-
});
|
|
415
|
+
throw toRuntimeLaunchFailure(error, "native-session-discovery", context);
|
|
291
416
|
}
|
|
292
417
|
finally {
|
|
418
|
+
stopped = true;
|
|
293
419
|
controller.abort();
|
|
294
420
|
}
|
|
295
421
|
}
|
|
296
|
-
/**
|
|
297
|
-
* Monitors the managed pane for agent-emitted signals while native session
|
|
298
|
-
* discovery is pending. The agent's own behavior drives the outcome:
|
|
299
|
-
*
|
|
300
|
-
* - Pane death → failure with exit status and stderr (the agent exited).
|
|
301
|
-
* - Fatal output (auth, config, executable errors) → failure with the
|
|
302
|
-
* agent's own error text.
|
|
303
|
-
* - No signal at all (no hook, no output change, no exit) for the
|
|
304
|
-
* inactivity window → backstop failure.
|
|
305
|
-
*
|
|
306
|
-
* There is no fixed wall-clock timeout: a slow-but-active agent is never
|
|
307
|
-
* killed merely for taking too long to start.
|
|
308
|
-
*/
|
|
309
|
-
async monitorPaneSignals(request, hostId, context, signal) {
|
|
310
|
-
let lastContent = "";
|
|
311
|
-
let lastActivityAt = Date.now();
|
|
312
|
-
while (!signal.aborted) {
|
|
313
|
-
await abortableDelay(PANE_SIGNAL_POLL_MS, signal);
|
|
314
|
-
let pane;
|
|
315
|
-
try {
|
|
316
|
-
pane = await inspectRolePane(this.tmux, hostId, request.owner.roleName);
|
|
317
|
-
}
|
|
318
|
-
catch {
|
|
319
|
-
// A tmux inspection hiccup must not fabricate a Provider death; the
|
|
320
|
-
// inactivity backstop remains the safety net.
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
if (pane !== undefined && pane.dead) {
|
|
324
|
-
await deadHostLaunchFailure(this.tmux, hostId, request.owner.roleName, pane, context);
|
|
325
|
-
}
|
|
326
|
-
let content = "";
|
|
327
|
-
try {
|
|
328
|
-
content = this.tmux.captureRolePane?.(hostId, request.owner.roleName, 80) ?? "";
|
|
329
|
-
}
|
|
330
|
-
catch {
|
|
331
|
-
// Capture is best-effort; pane death and hooks remain authoritative.
|
|
332
|
-
}
|
|
333
|
-
if (content !== lastContent) {
|
|
334
|
-
lastContent = content;
|
|
335
|
-
lastActivityAt = Date.now();
|
|
336
|
-
if (hasFatalLaunchOutput(content)) {
|
|
337
|
-
throw toRuntimeLaunchFailure(new Error("Agent emitted a fatal error before native session discovery completed."), "native-session-discovery", {
|
|
338
|
-
...context,
|
|
339
|
-
...(pane !== undefined ? { pane } : {}),
|
|
340
|
-
stderrTail: content
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
if (Date.now() - lastActivityAt >= this.#inactivityTimeoutMs) {
|
|
345
|
-
throw new Error(`Agent produced no signal for ${this.#inactivityTimeoutMs}ms. `
|
|
346
|
-
+ "The process is alive but emitted no lifecycle hook, output, or exit.");
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
throw new Error("Native session discovery was aborted.");
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
function requiresNativeSessionDiscovery(request, planned, wait) {
|
|
353
|
-
return request.mode === "new"
|
|
354
|
-
&& request.owner.scope === "task"
|
|
355
|
-
&& request.runId !== undefined
|
|
356
|
-
&& planned.initialPromptRunId === request.runId
|
|
357
|
-
&& wait !== undefined
|
|
358
|
-
&& builtinAgentDriverRegistry()
|
|
359
|
-
.requireByAdapterId(request.adapterId)
|
|
360
|
-
.capabilities.observation.sessionBootstrap === "discovered";
|
|
361
|
-
}
|
|
362
|
-
function positiveInteger(value, label) {
|
|
363
|
-
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
364
|
-
throw new Error(`${label} must be a positive integer.`);
|
|
365
|
-
}
|
|
366
|
-
return value;
|
|
367
|
-
}
|
|
368
|
-
const PANE_SIGNAL_POLL_MS = 1_000;
|
|
369
|
-
function abortableDelay(milliseconds, signal) {
|
|
370
|
-
return new Promise((resolve, reject) => {
|
|
371
|
-
if (signal.aborted) {
|
|
372
|
-
reject(new Error("Native session discovery was aborted."));
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
const onAbort = () => {
|
|
376
|
-
clearTimeout(timer);
|
|
377
|
-
reject(new Error("Native session discovery was aborted."));
|
|
378
|
-
};
|
|
379
|
-
const timer = setTimeout(() => {
|
|
380
|
-
signal.removeEventListener("abort", onAbort);
|
|
381
|
-
resolve();
|
|
382
|
-
}, milliseconds);
|
|
383
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
function planManagedLaunch(planner, request, beforeHostStart) {
|
|
387
|
-
try {
|
|
388
|
-
const input = {
|
|
389
|
-
roleName: request.owner.roleName,
|
|
390
|
-
agentId: request.agentId,
|
|
391
|
-
adapterId: request.adapterId,
|
|
392
|
-
effective: request.effective,
|
|
393
|
-
launchId: request.launchId,
|
|
394
|
-
mode: request.mode,
|
|
395
|
-
...(request.runId === undefined ? {} : { runId: request.runId }),
|
|
396
|
-
...(request.runtimeIsolation === undefined
|
|
397
|
-
? {}
|
|
398
|
-
: { runtimeIsolation: request.runtimeIsolation }),
|
|
399
|
-
...(request.environment === undefined
|
|
400
|
-
? {}
|
|
401
|
-
: { environment: request.environment }),
|
|
402
|
-
...(request.mode === "resume" ? { nativeSessionId: request.nativeSessionId } : {})
|
|
403
|
-
};
|
|
404
|
-
const planned = request.owner.scope === "task"
|
|
405
|
-
? planner.plan({ taskId: request.owner.taskId, ...input })
|
|
406
|
-
: planner.planGlobalRole(input);
|
|
407
|
-
if (planned.role.name !== request.owner.roleName) {
|
|
408
|
-
throw new Error("Planned Role does not match the runtime owner.");
|
|
409
|
-
}
|
|
410
|
-
if (planned.role.workspace !== request.workspace) {
|
|
411
|
-
throw new Error("Planned Role workspace does not match the runtime request.");
|
|
412
|
-
}
|
|
413
|
-
const plannedNativeSessionId = planned.session?.nativeSessionId;
|
|
414
|
-
if (request.mode === "resume"
|
|
415
|
-
&& plannedNativeSessionId !== undefined
|
|
416
|
-
&& plannedNativeSessionId !== request.nativeSessionId) {
|
|
417
|
-
throw new Error("Planned native session does not match the resume request.");
|
|
418
|
-
}
|
|
419
|
-
const nativeSessionId = plannedNativeSessionId
|
|
420
|
-
?? (request.mode === "resume" ? request.nativeSessionId : undefined);
|
|
421
|
-
beforeHostStart?.({
|
|
422
|
-
owner: request.owner,
|
|
423
|
-
launchId: request.launchId,
|
|
424
|
-
...(request.runId === undefined ? {} : { runId: request.runId }),
|
|
425
|
-
agentId: request.agentId,
|
|
426
|
-
adapterId: request.adapterId,
|
|
427
|
-
effective: request.effective,
|
|
428
|
-
...(nativeSessionId === undefined ? {} : { nativeSessionId }),
|
|
429
|
-
...(planned.initialPromptRunId === undefined
|
|
430
|
-
? {}
|
|
431
|
-
: { initialPromptRunId: planned.initialPromptRunId })
|
|
432
|
-
});
|
|
433
|
-
return { planned, nativeSessionId };
|
|
434
|
-
}
|
|
435
|
-
catch (error) {
|
|
436
|
-
if (error instanceof RuntimeLaunchError
|
|
437
|
-
|| error instanceof RuntimeHostContentionError) {
|
|
438
|
-
throw error;
|
|
439
|
-
}
|
|
440
|
-
throw toRuntimeLaunchFailure(error, "validation", {
|
|
441
|
-
agentId: request.agentId,
|
|
442
|
-
cwd: request.workspace
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
422
|
}
|
|
446
423
|
function diagnosticContext(request, planned) {
|
|
447
424
|
return {
|
|
@@ -490,6 +467,12 @@ async function ensureRoleWindow(tmux, hostId, role, launch) {
|
|
|
490
467
|
? tmux.ensureRoleWindow(hostId, role, launch)
|
|
491
468
|
: tmux.ensureRoleWindowAsync(hostId, role, launch);
|
|
492
469
|
}
|
|
470
|
+
function positiveInteger(value, label) {
|
|
471
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
472
|
+
throw new Error(`${label} must be a positive integer.`);
|
|
473
|
+
}
|
|
474
|
+
return value;
|
|
475
|
+
}
|
|
493
476
|
async function probeRoleStatus(tmux, hostId, roleName) {
|
|
494
477
|
return tmux.probeRoleStatusAsync === undefined
|
|
495
478
|
? tmux.probeRoleStatus(hostId, roleName)
|