@zq-silk/yui 0.6.16 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/coordination/workMailbox.js +25 -22
- 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 +48 -4
- 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
|
@@ -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)
|
|
@@ -3,6 +3,7 @@ import { isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
|
3
3
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
4
4
|
import { markYuiRunInput } from "../run/runIdentity.js";
|
|
5
5
|
import { taskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
6
|
+
import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
|
|
6
7
|
import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain } from "../executor/effectiveLaunch.js";
|
|
7
8
|
import { RuntimeLaunchError } from "../runtime/ports.js";
|
|
8
9
|
import { RuntimeLaunchFailure } from "../runtime/launchDiagnostics.js";
|
|
@@ -11,6 +12,9 @@ import { mailboxHasWork, nextPendingBatch } from "../coordination/workMailbox.js
|
|
|
11
12
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
12
13
|
import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
13
14
|
import { hasRuntimeLifecycleWork, RuntimeLifecycleBusyError, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
15
|
+
import { serializeRunBootstrapEnvelope } from "../context/runContextContract.js";
|
|
16
|
+
import { serializeProviderRetryEnvelope } from "../run/providerRetry.js";
|
|
17
|
+
import { serializeWorkflowOutcomeRequestEnvelope } from "../run/runControlRequest.js";
|
|
14
18
|
/**
|
|
15
19
|
* Delivers durable Work AgentRuns before liveness reconciliation. Task command
|
|
16
20
|
* handlers only record intent; this Controller path is the sole automated
|
|
@@ -32,7 +36,10 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
|
|
|
32
36
|
// re-push guard keys on pushedAt (transport), not deliveredAt (provider
|
|
33
37
|
// acceptance): a pushed-but-unaccepted Run must never be pushed twice —
|
|
34
38
|
// no duplicate Enter while acceptance is still pending.
|
|
35
|
-
if (run === null
|
|
39
|
+
if (run === null
|
|
40
|
+
|| (run.pushedAt !== undefined
|
|
41
|
+
&& run.providerRetry?.state !== "dispatching"
|
|
42
|
+
&& run.controlRequest?.state !== "dispatching"))
|
|
36
43
|
continue;
|
|
37
44
|
const taskWorkspace = store.getTaskWorkspace(task.id);
|
|
38
45
|
if (!isSchedulerTaskWorkspaceReady(task, taskWorkspace)) {
|
|
@@ -64,7 +71,7 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
|
|
|
64
71
|
continue;
|
|
65
72
|
}
|
|
66
73
|
const existingSession = store.getRoleSession(task.id, role.name, run.effective.agentId);
|
|
67
|
-
const receiptId =
|
|
74
|
+
const receiptId = agentRunDeliveryReceiptId(run);
|
|
68
75
|
const target = { kind: "role", taskId: task.id, roleName: role.name };
|
|
69
76
|
const claim = store.claimWorkMailbox({
|
|
70
77
|
target,
|
|
@@ -229,7 +236,21 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
|
|
|
229
236
|
const outcome = await delivery.sendOnce({
|
|
230
237
|
delivery: ready,
|
|
231
238
|
receiptId,
|
|
232
|
-
text: run.
|
|
239
|
+
text: run.controlRequest?.state === "dispatching"
|
|
240
|
+
? serializeWorkflowOutcomeRequestEnvelope({
|
|
241
|
+
taskId: task.id,
|
|
242
|
+
runId: run.id,
|
|
243
|
+
roleName: role.name,
|
|
244
|
+
request: run.controlRequest
|
|
245
|
+
})
|
|
246
|
+
: run.providerRetry?.state === "dispatching"
|
|
247
|
+
? serializeProviderRetryEnvelope({
|
|
248
|
+
taskId: task.id,
|
|
249
|
+
runId: run.id,
|
|
250
|
+
roleName: role.name,
|
|
251
|
+
retry: run.providerRetry
|
|
252
|
+
})
|
|
253
|
+
: serializeRunBootstrapEnvelope(run.bootstrapEnvelope)
|
|
233
254
|
});
|
|
234
255
|
if (outcome === "busy" || outcome === "unavailable") {
|
|
235
256
|
results.push({
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { createAgentRun } from "../run/agentRun.js";
|
|
2
|
-
import {
|
|
3
|
-
import { taskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
2
|
+
import { createRunAssignment, serializeRunBootstrapEnvelope } from "../context/runContextContract.js";
|
|
4
3
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
5
4
|
import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain } from "../executor/effectiveLaunch.js";
|
|
6
5
|
import { hasRuntimeLifecycleWork, RuntimeLifecycleBusyError, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
7
6
|
import { recordLeaderFailure } from "./leaderFailure.js";
|
|
8
7
|
import { createLeaderRecoveryNotification } from "./operatorNotification.js";
|
|
9
|
-
import { evaluateSessionContextBudget } from "../context/sessionContextBudget.js";
|
|
10
8
|
import { isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
11
9
|
import { RuntimeLaunchError } from "../runtime/ports.js";
|
|
12
10
|
export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
@@ -122,52 +120,21 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
122
120
|
&& existingSession.status !== "stopped" && existingSession.status !== "broken") {
|
|
123
121
|
throw new Error(`Leader Session is incompatible with desired effective launch: ${task.id}/${role.name}.`);
|
|
124
122
|
}
|
|
125
|
-
// Issue 04: a native Session generation whose observed per-request
|
|
126
|
-
// input peak crossed the hard context budget is retired before dispatch
|
|
127
|
-
// so the wake starts a fresh generation instead of waiting for
|
|
128
|
-
// provider-side auto-compaction. Durable Task records are the checkpoint;
|
|
129
|
-
// the bounded snapshot below re-establishes working context.
|
|
130
|
-
let contextBudgetAdvisory;
|
|
131
|
-
if (hasNativeSession(existingSession)
|
|
132
|
-
&& compatibleSession
|
|
133
|
-
&& existingSession.status !== "stopped"
|
|
134
|
-
&& existingSession.status !== "broken"
|
|
135
|
-
&& typeof store.listEvents === "function"
|
|
136
|
-
&& typeof store.getContextBudget === "function"
|
|
137
|
-
&& typeof store.rolloverTaskRoleSessionForContextBudget === "function") {
|
|
138
|
-
const budget = evaluateSessionContextBudget(store.listEvents(task.id), {
|
|
139
|
-
taskId: task.id,
|
|
140
|
-
roleName: role.name,
|
|
141
|
-
...(existingSession.nativeSessionId === undefined
|
|
142
|
-
? {}
|
|
143
|
-
: { nativeSessionId: existingSession.nativeSessionId }),
|
|
144
|
-
...(existingSession.launchId === undefined
|
|
145
|
-
? {}
|
|
146
|
-
: { launchId: existingSession.launchId })
|
|
147
|
-
}, store.getContextBudget());
|
|
148
|
-
if (budget.state === "hard") {
|
|
149
|
-
const rollover = store.rolloverTaskRoleSessionForContextBudget({
|
|
150
|
-
taskId: task.id,
|
|
151
|
-
roleName: role.name,
|
|
152
|
-
peakTokens: budget.peakTokens,
|
|
153
|
-
hardTokens: budget.budget.hardTokens,
|
|
154
|
-
now
|
|
155
|
-
});
|
|
156
|
-
if (rollover !== null) {
|
|
157
|
-
existingSession = null;
|
|
158
|
-
effectiveSession = null;
|
|
159
|
-
contextBudgetAdvisory = `Previous Session generation ${rollover.retiredNativeSessionId ?? "unknown"} was retired at the hard context budget (peak ${budget.peakTokens} >= ${budget.budget.hardTokens} tokens). This wake starts a fresh generation; durable Task records preserve all state.`;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
else if (budget.state === "soft") {
|
|
163
|
-
contextBudgetAdvisory = `Session context budget advisory: observed per-request input peak ${budget.peakTokens} tokens crossed the soft threshold (${budget.budget.softTokens}). Durable Yui records are the checkpoint; a fresh generation starts automatically at the hard threshold (${budget.budget.hardTokens}).`;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
123
|
const mode = hasNativeSession(existingSession) && compatibleSession ? "resume" : "new";
|
|
167
124
|
const runId = store.peekNextAgentRunId(task.id);
|
|
168
125
|
const wakeEnvelope = resolveLeaderWakeEnvelope(store, task.id);
|
|
169
|
-
const
|
|
170
|
-
|
|
126
|
+
const contextSnapshot = store.freezeLeaderContextSnapshot?.(task.id, role.name, now);
|
|
127
|
+
const assignment = createRunAssignment({
|
|
128
|
+
runId,
|
|
129
|
+
roleName: role.name,
|
|
130
|
+
purpose: "execution",
|
|
131
|
+
action: "leader-wake",
|
|
132
|
+
subject: { taskId: task.id },
|
|
133
|
+
directive: leaderWakeupInput(task.id, runId, wakeup.reasons),
|
|
134
|
+
...(contextSnapshot === undefined ? {} : { contextSnapshotRef: contextSnapshot.ref }),
|
|
135
|
+
deltaRefIds: contextSnapshot?.deltaRefIds ?? []
|
|
136
|
+
});
|
|
137
|
+
run = createAgentRun(runId, task.id, role.name, mode, assignment, now, {
|
|
171
138
|
...(role.managedWorkspace === undefined
|
|
172
139
|
? {}
|
|
173
140
|
: { workspace: role.managedWorkspace }),
|
|
@@ -300,7 +267,7 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
300
267
|
const outcome = await delivery.sendOnce({
|
|
301
268
|
delivery: ready,
|
|
302
269
|
receiptId: formatAgentRunReceiptId(task.id, run.id),
|
|
303
|
-
text:
|
|
270
|
+
text: serializeRunBootstrapEnvelope(run.bootstrapEnvelope)
|
|
304
271
|
});
|
|
305
272
|
if (outcome === "busy" || outcome === "unavailable") {
|
|
306
273
|
results.push({
|
|
@@ -490,18 +457,9 @@ function resolveLeaderWakeEnvelope(store, taskId) {
|
|
|
490
457
|
return null;
|
|
491
458
|
return store.getTaskWakeEnvelope(taskId);
|
|
492
459
|
}
|
|
493
|
-
function leaderWakeupInput(taskId, runId,
|
|
494
|
-
|
|
495
|
-
"Follow the injected yui-leader Skill for this Yui wakeup.",
|
|
496
|
-
`Current Leader Run: ${runId}.`,
|
|
460
|
+
function leaderWakeupInput(taskId, runId, reasons) {
|
|
461
|
+
return [
|
|
497
462
|
`For every Leader decision, milestone, or Work Item lifecycle command that is meaningful progress, carry this exact current-turn assertion on that command: YUI_LEADER_ACTION_RUN_ID=${runId} YUI_LEADER_ACTION_RECEIPT_ID=${formatAgentRunReceiptId(taskId, runId)}. The native Session environment may retain an older YUI_RUN_ID/launch; never copy those values, and never reuse this assertion after the turn changes.`,
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
? [`Read the authoritative context with yui task context ${taskId}.`]
|
|
501
|
-
: [
|
|
502
|
-
"Wake envelope (Issue 04):",
|
|
503
|
-
wakeEnvelope.text
|
|
504
|
-
]),
|
|
505
|
-
];
|
|
506
|
-
return lines.join("\n");
|
|
463
|
+
`Wake reasons: ${reasons.join(", ")}. Load exact context for ${taskId}/${runId}.`
|
|
464
|
+
].join("\n");
|
|
507
465
|
}
|