@zq-silk/yui 0.6.13 → 0.6.14
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/README.md +34 -5
- package/dist/cli/commandCatalog.js +173 -57
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli.js +99 -11
- package/dist/commands/configCommands.js +521 -171
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/executionAuditCommands.js +56 -3
- package/dist/commands/projectCommands.js +504 -2
- package/dist/commands/releaseCommands.js +0 -1
- package/dist/commands/taskActor.js +17 -0
- package/dist/commands/taskBaseCommands.js +29 -0
- package/dist/commands/taskCommands.js +577 -126
- package/dist/commands/taskContextCommand.js +36 -2
- package/dist/commands/taskNextActionCommand.js +48 -3
- package/dist/commands/taskPublicationCommands.js +319 -0
- package/dist/commands/taskRoleRuntimeStatus.js +83 -59
- package/dist/commands/telemetryCommands.js +14 -13
- package/dist/config/yuiConfig.js +161 -8
- package/dist/context/sessionContextBudget.js +68 -0
- package/dist/context/wakeNotification.js +65 -0
- package/dist/controller/clientRuntime.js +2 -1
- package/dist/controller/ephemeralResourceReaper.js +2 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +183 -16
- package/dist/controller/jobSupervisor.js +5 -4
- package/dist/controller/resourceCleanupLinux.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +3 -3
- package/dist/controller/runtime.js +88 -10
- package/dist/controller/updateReconciliation.js +4 -3
- package/dist/doctor/doctor.js +26 -7
- package/dist/executor/agentConfigurationCatalog.js +18 -0
- package/dist/executor/fileRoleLaunchPlanner.js +3 -3
- package/dist/lifecycle/contextBudgetRollover.js +81 -0
- package/dist/lifecycle/exactRunTerminalization.js +11 -1
- package/dist/lifecycle/providerErrorClass.js +33 -12
- package/dist/observability/executionAudit.js +214 -6
- package/dist/output/table.js +18 -0
- package/dist/repository/gitWorkspace.js +92 -0
- package/dist/repository/project.js +218 -4
- package/dist/repository/taskBaseFreshness.js +318 -0
- package/dist/repository/taskWorkspacePreparer.js +16 -2
- package/dist/review/deltaRecheck.js +232 -0
- package/dist/review/reviewConfig.js +31 -0
- package/dist/review/reviewFindingLedger.js +5 -1
- package/dist/review/reviewRound.js +156 -1
- package/dist/run/providerRetry.js +21 -3
- package/dist/run/providerRetryConfig.js +13 -60
- package/dist/run/recoveryProjection.js +199 -0
- package/dist/runtime/builtinAgentDrivers.js +3 -0
- package/dist/runtime/builtinTranscriptUsage.js +76 -32
- package/dist/runtime/continuationManager.js +17 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/launchDiagnostics.js +154 -0
- package/dist/runtime/lifecycleReservation.js +13 -0
- package/dist/runtime/providerContinuation.js +38 -0
- package/dist/runtime/providerContinuationReconciliationService.js +1 -0
- package/dist/runtime/providerErrorCodes.js +278 -0
- package/dist/runtime/runtimeHealthPolicy.js +20 -0
- package/dist/runtime/runtimeObservation.js +1 -0
- package/dist/runtime/runtimeProjection.js +115 -23
- package/dist/runtime/tmuxAdapters.js +242 -48
- package/dist/scheduler/activeRoleRunDelivery.js +139 -3
- package/dist/scheduler/activeTaskProgress.js +4 -3
- package/dist/scheduler/leaderWakeupProcessor.js +105 -15
- package/dist/scheduler/roleRunLiveness.js +2 -1
- package/dist/scheduler/roleRunStall.js +3 -2
- package/dist/scheduler/taskWake.js +72 -0
- package/dist/scheduler/wakeReason.js +64 -0
- package/dist/scheduler/wakeupQueue.js +2 -1
- package/dist/setup/setupCommand.js +1 -1
- package/dist/storage/migration/productionRegistry.js +325 -1
- package/dist/storage/sqliteSchema.js +61 -2
- package/dist/storage/sqliteStore.js +129 -2
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/taskStore.js +262 -5
- package/dist/storage/upgrade/recordVersions.js +6 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
- package/dist/task/completionReadiness.js +282 -0
- package/dist/task/publicationReference.js +123 -0
- package/dist/task/taskRecordReference.js +3 -1
- package/dist/telemetry/telemetryConfig.js +23 -18
- package/dist/telemetry/telemetryWiring.js +8 -8
- package/dist/tmux/tmuxManager.js +50 -9
- package/dist/web/assets/client/i18n.js +4 -0
- package/dist/web/assets/client/view.js +18 -0
- package/dist/web/webSnapshot.js +100 -10
- package/i18n/README.zh-CN.md +5 -5
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +49 -10
- package/skills/yui-operator/SKILL.md +13 -3
- package/skills/yui-worker/SKILL.md +8 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { createRuntimeBinding } from "./runtimeBinding.js";
|
|
3
3
|
import { normalizeRuntimeOwner } from "./runtimeOwner.js";
|
|
4
|
-
import { RuntimeHostContentionError } from "./ports.js";
|
|
4
|
+
import { RuntimeHostContentionError, RuntimeLaunchError } from "./ports.js";
|
|
5
|
+
import { toRuntimeLaunchFailure, hasFatalLaunchOutput } from "./launchDiagnostics.js";
|
|
6
|
+
import { builtinAgentDriverRegistry } from "./builtinAgentDrivers.js";
|
|
5
7
|
import { requireSafeIdentity } from "./validation.js";
|
|
8
|
+
const DEFAULT_INACTIVITY_TIMEOUT_MS = 300_000;
|
|
6
9
|
/**
|
|
7
10
|
* Runtime lifecycle adapter for the current tmux host. The returned hostRef is
|
|
8
11
|
* opaque to the domain and self-contained for a later inspect/stop operation.
|
|
@@ -13,6 +16,9 @@ export class TmuxSessionHost {
|
|
|
13
16
|
#globalHostId;
|
|
14
17
|
#createBindingId;
|
|
15
18
|
#onHostCreated;
|
|
19
|
+
#validateLaunch;
|
|
20
|
+
#waitForNativeSession;
|
|
21
|
+
#inactivityTimeoutMs;
|
|
16
22
|
#launchTails = new Map();
|
|
17
23
|
constructor(planner, tmux, options = {}) {
|
|
18
24
|
this.planner = planner;
|
|
@@ -20,6 +26,9 @@ export class TmuxSessionHost {
|
|
|
20
26
|
this.#globalHostId = requireSafeIdentity(options.globalHostId ?? "operator", "Global tmux host id");
|
|
21
27
|
this.#createBindingId = options.createBindingId ?? randomUUID;
|
|
22
28
|
this.#onHostCreated = options.onHostCreated;
|
|
29
|
+
this.#validateLaunch = options.validateLaunch;
|
|
30
|
+
this.#waitForNativeSession = options.waitForNativeSession;
|
|
31
|
+
this.#inactivityTimeoutMs = positiveInteger(options.inactivityTimeoutMs ?? DEFAULT_INACTIVITY_TIMEOUT_MS, "Launch inactivity timeout");
|
|
23
32
|
}
|
|
24
33
|
async start(request, beforeHostStart) {
|
|
25
34
|
return this.#launch(request, beforeHostStart);
|
|
@@ -180,6 +189,202 @@ export class TmuxSessionHost {
|
|
|
180
189
|
: {})
|
|
181
190
|
});
|
|
182
191
|
}
|
|
192
|
+
const { planned, nativeSessionId } = planManagedLaunch(this.planner, request, beforeHostStart);
|
|
193
|
+
const launchContext = diagnosticContext(request, planned);
|
|
194
|
+
if (this.#validateLaunch !== undefined) {
|
|
195
|
+
try {
|
|
196
|
+
await this.#validateLaunch(request);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
throw toRuntimeLaunchFailure(error, "validation", launchContext);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Process creation is last: every local invariant has already passed.
|
|
203
|
+
let hostCreated;
|
|
204
|
+
try {
|
|
205
|
+
hostCreated = await ensureRoleWindow(this.tmux, hostId, planned.role, planned.launch);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
throw toRuntimeLaunchFailure(error, "host-start", launchContext);
|
|
209
|
+
}
|
|
210
|
+
if (hostCreated
|
|
211
|
+
&& request.owner.scope === "task"
|
|
212
|
+
&& planned.launch.env.YUI_JOB_CALLER_KEY !== undefined
|
|
213
|
+
&& this.planner.commitTaskCallerKey !== undefined) {
|
|
214
|
+
this.planner.commitTaskCallerKey({
|
|
215
|
+
taskId: request.owner.taskId,
|
|
216
|
+
roleName: request.owner.roleName,
|
|
217
|
+
agentId: request.agentId,
|
|
218
|
+
callerKey: planned.launch.env.YUI_JOB_CALLER_KEY
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
let binding = createRuntimeBinding({
|
|
222
|
+
id: bindingId,
|
|
223
|
+
launchId: request.launchId,
|
|
224
|
+
owner: request.owner,
|
|
225
|
+
agentId: request.agentId,
|
|
226
|
+
adapterId: request.adapterId,
|
|
227
|
+
hostRef: encodeHostRef({
|
|
228
|
+
scope: request.owner.scope,
|
|
229
|
+
hostId,
|
|
230
|
+
roleName: request.owner.roleName
|
|
231
|
+
}),
|
|
232
|
+
hostCreated,
|
|
233
|
+
...(hostCreated && planned.initialPromptRunId !== undefined
|
|
234
|
+
? { initialPromptRunId: planned.initialPromptRunId }
|
|
235
|
+
: {}),
|
|
236
|
+
...(nativeSessionId === undefined ? {} : { nativeSessionId })
|
|
237
|
+
});
|
|
238
|
+
if (hostCreated) {
|
|
239
|
+
let pane;
|
|
240
|
+
try {
|
|
241
|
+
pane = await inspectRolePane(this.tmux, hostId, request.owner.roleName);
|
|
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 });
|
|
261
|
+
}
|
|
262
|
+
return binding;
|
|
263
|
+
}
|
|
264
|
+
async waitForNativeSessionDiscovery(request, hostId, context, pane) {
|
|
265
|
+
if (this.#waitForNativeSession === undefined) {
|
|
266
|
+
throw new Error("Native session discovery is not configured.");
|
|
267
|
+
}
|
|
268
|
+
const controller = new AbortController();
|
|
269
|
+
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
|
+
discovery.catch(() => undefined);
|
|
273
|
+
try {
|
|
274
|
+
return await Promise.race([
|
|
275
|
+
discovery,
|
|
276
|
+
this.monitorPaneSignals(request, hostId, context, controller.signal)
|
|
277
|
+
]);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
try {
|
|
281
|
+
await stopExactRole(this.tmux, hostId, request.owner.roleName);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// The launch failure below is authoritative; durable owner cleanup
|
|
285
|
+
// remains responsible for a Provider that rejected immediate stop.
|
|
286
|
+
}
|
|
287
|
+
throw toRuntimeLaunchFailure(error, "native-session-discovery", {
|
|
288
|
+
...context,
|
|
289
|
+
pane
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
controller.abort();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
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 {
|
|
183
388
|
const input = {
|
|
184
389
|
roleName: request.owner.roleName,
|
|
185
390
|
agentId: request.agentId,
|
|
@@ -197,8 +402,8 @@ export class TmuxSessionHost {
|
|
|
197
402
|
...(request.mode === "resume" ? { nativeSessionId: request.nativeSessionId } : {})
|
|
198
403
|
};
|
|
199
404
|
const planned = request.owner.scope === "task"
|
|
200
|
-
?
|
|
201
|
-
:
|
|
405
|
+
? planner.plan({ taskId: request.owner.taskId, ...input })
|
|
406
|
+
: planner.planGlobalRole(input);
|
|
202
407
|
if (planned.role.name !== request.owner.roleName) {
|
|
203
408
|
throw new Error("Planned Role does not match the runtime owner.");
|
|
204
409
|
}
|
|
@@ -225,45 +430,42 @@ export class TmuxSessionHost {
|
|
|
225
430
|
? {}
|
|
226
431
|
: { initialPromptRunId: planned.initialPromptRunId })
|
|
227
432
|
});
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
this.planner.commitTaskCallerKey({
|
|
235
|
-
taskId: request.owner.taskId,
|
|
236
|
-
roleName: request.owner.roleName,
|
|
237
|
-
agentId: request.agentId,
|
|
238
|
-
callerKey: planned.launch.env.YUI_JOB_CALLER_KEY
|
|
239
|
-
});
|
|
433
|
+
return { planned, nativeSessionId };
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
if (error instanceof RuntimeLaunchError
|
|
437
|
+
|| error instanceof RuntimeHostContentionError) {
|
|
438
|
+
throw error;
|
|
240
439
|
}
|
|
241
|
-
|
|
242
|
-
id: bindingId,
|
|
243
|
-
launchId: request.launchId,
|
|
244
|
-
owner: request.owner,
|
|
440
|
+
throw toRuntimeLaunchFailure(error, "validation", {
|
|
245
441
|
agentId: request.agentId,
|
|
246
|
-
|
|
247
|
-
hostRef: encodeHostRef({
|
|
248
|
-
scope: request.owner.scope,
|
|
249
|
-
hostId,
|
|
250
|
-
roleName: request.owner.roleName
|
|
251
|
-
}),
|
|
252
|
-
hostCreated,
|
|
253
|
-
...(hostCreated && planned.initialPromptRunId !== undefined
|
|
254
|
-
? { initialPromptRunId: planned.initialPromptRunId }
|
|
255
|
-
: {}),
|
|
256
|
-
...(nativeSessionId === undefined ? {} : { nativeSessionId })
|
|
442
|
+
cwd: request.workspace
|
|
257
443
|
});
|
|
258
|
-
if (hostCreated && this.#onHostCreated !== undefined) {
|
|
259
|
-
const pane = await inspectRolePane(this.tmux, hostId, request.owner.roleName);
|
|
260
|
-
if (pane !== undefined) {
|
|
261
|
-
this.#onHostCreated({ binding, pane });
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
return binding;
|
|
265
444
|
}
|
|
266
445
|
}
|
|
446
|
+
function diagnosticContext(request, planned) {
|
|
447
|
+
return {
|
|
448
|
+
command: planned.launch.command,
|
|
449
|
+
argv: [planned.launch.command, ...planned.launch.args],
|
|
450
|
+
cwd: planned.role.cwd ?? planned.role.workspace,
|
|
451
|
+
agentId: request.agentId
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
async function deadHostLaunchFailure(tmux, hostId, roleName, pane, context) {
|
|
455
|
+
let stderrTail;
|
|
456
|
+
try {
|
|
457
|
+
stderrTail = tmux.captureRolePane?.(hostId, roleName, 80);
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
// The pane state is the required evidence; capture is best-effort.
|
|
461
|
+
}
|
|
462
|
+
throw toRuntimeLaunchFailure(new Error("Provider exited immediately after managed host start."), "host-started", {
|
|
463
|
+
...context,
|
|
464
|
+
pane,
|
|
465
|
+
...(pane.exitStatus === undefined ? {} : { exitStatus: pane.exitStatus }),
|
|
466
|
+
...(stderrTail === undefined || stderrTail.length === 0 ? {} : { stderrTail })
|
|
467
|
+
});
|
|
468
|
+
}
|
|
267
469
|
/** Non-blocking receipt-backed tmux push; it never interprets provider output. */
|
|
268
470
|
export class TmuxPromptPushAdapter {
|
|
269
471
|
tmux;
|
|
@@ -294,17 +496,9 @@ async function probeRoleStatus(tmux, hostId, roleName) {
|
|
|
294
496
|
: tmux.probeRoleStatusAsync(hostId, roleName);
|
|
295
497
|
}
|
|
296
498
|
async function inspectRolePane(tmux, hostId, roleName) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
: await tmux.inspectRolePaneAsync(hostId, roleName);
|
|
301
|
-
}
|
|
302
|
-
catch {
|
|
303
|
-
// The pane may have exited between creation and inspection; the owner
|
|
304
|
-
// recorder treats a missing pane as a verification gap, not a launch
|
|
305
|
-
// failure.
|
|
306
|
-
return undefined;
|
|
307
|
-
}
|
|
499
|
+
return tmux.inspectRolePaneAsync === undefined
|
|
500
|
+
? tmux.inspectRolePane?.(hostId, roleName)
|
|
501
|
+
: await tmux.inspectRolePaneAsync(hostId, roleName);
|
|
308
502
|
}
|
|
309
503
|
async function killRole(tmux, hostId, roleName) {
|
|
310
504
|
if (tmux.killRoleAsync === undefined) {
|
|
@@ -5,8 +5,12 @@ import { markYuiRunInput } from "../run/runIdentity.js";
|
|
|
5
5
|
import { taskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
6
6
|
import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain } from "../executor/effectiveLaunch.js";
|
|
7
7
|
import { RuntimeLaunchError } from "../runtime/ports.js";
|
|
8
|
+
import { RuntimeLaunchFailure } from "../runtime/launchDiagnostics.js";
|
|
8
9
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
9
10
|
import { mailboxHasWork, nextPendingBatch } from "../coordination/workMailbox.js";
|
|
11
|
+
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
12
|
+
import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
13
|
+
import { hasRuntimeLifecycleWork, RuntimeLifecycleBusyError, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
10
14
|
/**
|
|
11
15
|
* Delivers durable Work AgentRuns before liveness reconciliation. Task command
|
|
12
16
|
* handlers only record intent; this Controller path is the sole automated
|
|
@@ -41,6 +45,24 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
|
|
|
41
45
|
});
|
|
42
46
|
continue;
|
|
43
47
|
}
|
|
48
|
+
// Single-flight: a Role runtime lifecycle lane that already holds a
|
|
49
|
+
// launch reservation or cleanup obligation must not be entered by a
|
|
50
|
+
// second delivery. The Run stays active-unpushed and is retried after
|
|
51
|
+
// the lane settles; the contention is never terminalized as a failure.
|
|
52
|
+
if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget({
|
|
53
|
+
scope: "task",
|
|
54
|
+
taskId: task.id,
|
|
55
|
+
roleName: role.name
|
|
56
|
+
})))) {
|
|
57
|
+
results.push({
|
|
58
|
+
taskId: task.id,
|
|
59
|
+
roleName: role.name,
|
|
60
|
+
runId: run.id,
|
|
61
|
+
status: "skipped",
|
|
62
|
+
reason: "runtime-unavailable"
|
|
63
|
+
});
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
44
66
|
const existingSession = store.getRoleSession(task.id, role.name, run.effective.agentId);
|
|
45
67
|
const receiptId = formatAgentRunReceiptId(task.id, run.id);
|
|
46
68
|
const target = { kind: "role", taskId: task.id, roleName: role.name };
|
|
@@ -235,6 +257,59 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
|
|
|
235
257
|
}
|
|
236
258
|
catch (error) {
|
|
237
259
|
const message = error instanceof Error ? error.message : String(error);
|
|
260
|
+
// Scheduler single-flight backpressure: the Role runtime lifecycle
|
|
261
|
+
// lane was busy when the launch was reserved. The Run stays
|
|
262
|
+
// active-unpushed and its mailbox claim is retained; the next pass
|
|
263
|
+
// retries after the lane settles. This is contention before any
|
|
264
|
+
// semantic launch, never a Run failure.
|
|
265
|
+
if (error instanceof RuntimeLifecycleBusyError) {
|
|
266
|
+
delivery.forgetPrepared?.({
|
|
267
|
+
taskId: task.id,
|
|
268
|
+
roleName: role.name,
|
|
269
|
+
runId: run.id
|
|
270
|
+
});
|
|
271
|
+
results.push({
|
|
272
|
+
taskId: task.id,
|
|
273
|
+
roleName: role.name,
|
|
274
|
+
runId: run.id,
|
|
275
|
+
status: "skipped",
|
|
276
|
+
reason: "runtime-unavailable",
|
|
277
|
+
error: message
|
|
278
|
+
});
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (error instanceof RuntimeLaunchFailure) {
|
|
282
|
+
if (!deliveryAttempted) {
|
|
283
|
+
delivery.forgetPrepared?.({
|
|
284
|
+
taskId: task.id,
|
|
285
|
+
roleName: role.name,
|
|
286
|
+
runId: run.id,
|
|
287
|
+
...(prepared?.launchId === undefined
|
|
288
|
+
? {}
|
|
289
|
+
: { launchId: prepared.launchId })
|
|
290
|
+
});
|
|
291
|
+
const persisted = store.saveExitedRoleRun({
|
|
292
|
+
task,
|
|
293
|
+
role,
|
|
294
|
+
run,
|
|
295
|
+
session: existingSession,
|
|
296
|
+
summary: error.message,
|
|
297
|
+
now
|
|
298
|
+
});
|
|
299
|
+
results.push({
|
|
300
|
+
taskId: task.id,
|
|
301
|
+
roleName: role.name,
|
|
302
|
+
runId: run.id,
|
|
303
|
+
status: "failed",
|
|
304
|
+
reason: "launch-failed",
|
|
305
|
+
error: persisted === "state-changed"
|
|
306
|
+
? "Run state changed during launch failure."
|
|
307
|
+
: message,
|
|
308
|
+
terminalized: persisted === "failed"
|
|
309
|
+
});
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
238
313
|
if (error instanceof RuntimeLaunchError) {
|
|
239
314
|
const terminalFailure = roleRunDeliveryFailure(run, processing.batchId, existingSession, error.launchId);
|
|
240
315
|
if (error.retryable) {
|
|
@@ -472,7 +547,7 @@ async function processActiveRunContinuation(store, delivery, task, role, run, no
|
|
|
472
547
|
}
|
|
473
548
|
inputDelivery = claim.delivery;
|
|
474
549
|
deliveryAttempted = true;
|
|
475
|
-
const text = continuationInput(task, role, run, inputDelivery.attemptId, inputDelivery.batch);
|
|
550
|
+
const text = continuationInput(task, role, run, inputDelivery.attemptId, inputDelivery.batch, boundedContinuationSummaries(store, task.id, inputDelivery.batch.refs));
|
|
476
551
|
if (mode !== "followup") {
|
|
477
552
|
const routed = await delivery.routeProviderInput({
|
|
478
553
|
delivery: ready,
|
|
@@ -607,7 +682,7 @@ async function reconcileStrandedInputDelivery(store, delivery, task, role, run,
|
|
|
607
682
|
return uncertain(`Provider input readback failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
608
683
|
}
|
|
609
684
|
}
|
|
610
|
-
function continuationInput(task, role, run, attemptId, batch) {
|
|
685
|
+
function continuationInput(task, role, run, attemptId, batch, resultSummaries) {
|
|
611
686
|
const references = batch.refs.map((ref) => ("taskId" in ref
|
|
612
687
|
? `${ref.type}:${ref.taskId}/${ref.id}`
|
|
613
688
|
: `${ref.type}:${ref.id}`));
|
|
@@ -618,9 +693,70 @@ function continuationInput(task, role, run, attemptId, batch) {
|
|
|
618
693
|
"Do not create a new Yui Run merely because this is a new Provider Turn.",
|
|
619
694
|
`Reasons: ${batch.reasons.join(", ")}.`,
|
|
620
695
|
...(batch.sources.length === 0 ? [] : [`Sources: ${batch.sources.join(", ")}.`]),
|
|
621
|
-
...(references.length === 0 ? [] : [`References: ${references.join(", ")}.`])
|
|
696
|
+
...(references.length === 0 ? [] : [`References: ${references.join(", ")}.`]),
|
|
697
|
+
...(resultSummaries.length === 0
|
|
698
|
+
? []
|
|
699
|
+
: [
|
|
700
|
+
"Native child results (bounded excerpts; read the referenced event for the full content):",
|
|
701
|
+
...resultSummaries
|
|
702
|
+
])
|
|
622
703
|
].join("\n"), run.id, taskRoleSessionTitle(task, role.name));
|
|
623
704
|
}
|
|
705
|
+
/**
|
|
706
|
+
* Issue 13: the parent prompt only ever sees a bounded excerpt of a native
|
|
707
|
+
* child result plus its durable event reference. The full content stays in
|
|
708
|
+
* the Task event log and is read on demand through `yui task event show`.
|
|
709
|
+
* This prevents an unbounded provider result from being re-injected into the
|
|
710
|
+
* parent Session on every continuation wake.
|
|
711
|
+
*/
|
|
712
|
+
const MAX_RESULT_SUMMARY_CHARS = 512;
|
|
713
|
+
const MAX_RESULT_SUMMARY_LINES = 8;
|
|
714
|
+
function boundedContinuationSummaries(store, taskId, refs) {
|
|
715
|
+
if (store.listEvents === undefined)
|
|
716
|
+
return [];
|
|
717
|
+
const events = store.listEvents(taskId);
|
|
718
|
+
const continuations = projectProviderContinuations(events);
|
|
719
|
+
const byKey = new Map(continuations.map((entry) => [
|
|
720
|
+
`${entry.identity.continuationId}\u0000${entry.identity.generation}`,
|
|
721
|
+
entry
|
|
722
|
+
]));
|
|
723
|
+
const summaries = [];
|
|
724
|
+
for (const ref of refs) {
|
|
725
|
+
if (!("taskId" in ref) || ref.type !== "event")
|
|
726
|
+
continue;
|
|
727
|
+
const event = events.find((entry) => entry.id === ref.id);
|
|
728
|
+
if (event === undefined)
|
|
729
|
+
continue;
|
|
730
|
+
const observation = runtimeObservationFromTaskEvent(event);
|
|
731
|
+
if (observation === null || observation.kind !== "continuation.reported")
|
|
732
|
+
continue;
|
|
733
|
+
const summary = observation.payload?.summary;
|
|
734
|
+
if (summary === undefined || summary.trim().length === 0)
|
|
735
|
+
continue;
|
|
736
|
+
const continuation = byKey.get(`${observation.fence.continuationId}\u0000${observation.fence.continuationGeneration}`);
|
|
737
|
+
const digest = continuation?.reports
|
|
738
|
+
.find((report) => report.reportId === observation.payload?.reportId)
|
|
739
|
+
?.resultDigest;
|
|
740
|
+
summaries.push([
|
|
741
|
+
`- child ${observation.fence.continuationId}`,
|
|
742
|
+
...(continuation?.durability === "durable-result"
|
|
743
|
+
? [" (durable-result; full content: "
|
|
744
|
+
+ `yui task event show ${taskId} ${ref.id}`
|
|
745
|
+
+ (digest === undefined ? "" : `; digest ${digest.slice(0, 12)}`)
|
|
746
|
+
+ ")"]
|
|
747
|
+
: [" (best-effort; rerun if the parent Session was lost)"]),
|
|
748
|
+
` ${boundedExcerpt(summary)}`
|
|
749
|
+
].join("\n"));
|
|
750
|
+
}
|
|
751
|
+
return summaries;
|
|
752
|
+
}
|
|
753
|
+
function boundedExcerpt(text) {
|
|
754
|
+
const lines = text.split("\n").slice(0, MAX_RESULT_SUMMARY_LINES);
|
|
755
|
+
const joined = lines.join("\n");
|
|
756
|
+
if (joined.length <= MAX_RESULT_SUMMARY_CHARS && text.length === joined.length)
|
|
757
|
+
return joined;
|
|
758
|
+
return `${joined.slice(0, MAX_RESULT_SUMMARY_CHARS)}… [truncated]`;
|
|
759
|
+
}
|
|
624
760
|
function roleRunDeliveryFailure(run, mailboxBatchId, session, launchId) {
|
|
625
761
|
return {
|
|
626
762
|
taskId: run.taskId,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { selectedActiveSchedulerTasks, isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
2
2
|
import { queueLeaderWakeup } from "./wakeupQueue.js";
|
|
3
|
+
import { wakeReason } from "./wakeReason.js";
|
|
3
4
|
import { projectTaskExecution } from "./taskExecutionProjection.js";
|
|
4
5
|
import { collectTaskActionability, computeActionabilityDigest, decideOrphanWake } from "./actionability.js";
|
|
5
6
|
/**
|
|
@@ -57,14 +58,14 @@ export function repairOrphanedActiveTasks(store, now, selection) {
|
|
|
57
58
|
if (leaderMailbox?.processing !== null && leaderMailbox?.processing !== undefined) {
|
|
58
59
|
const recovered = store.releaseLeaderWakeupAndEnqueue === undefined
|
|
59
60
|
? store.releaseWorkMailbox(leaderTarget, leaderMailbox.processing.batchId)
|
|
60
|
-
&& (queueLeaderWakeup(store, task.id, "task-orphaned", now), true)
|
|
61
|
-
: store.releaseLeaderWakeupAndEnqueue(task.id, leaderMailbox.processing.batchId, "task-orphaned", now);
|
|
61
|
+
&& (queueLeaderWakeup(store, task.id, wakeReason("task-orphaned"), now), true)
|
|
62
|
+
: store.releaseLeaderWakeupAndEnqueue(task.id, leaderMailbox.processing.batchId, wakeReason("task-orphaned"), now);
|
|
62
63
|
if (recovered) {
|
|
63
64
|
repaired.push(task.id);
|
|
64
65
|
}
|
|
65
66
|
continue;
|
|
66
67
|
}
|
|
67
|
-
queueLeaderWakeup(store, task.id, "task-orphaned", now);
|
|
68
|
+
queueLeaderWakeup(store, task.id, wakeReason("task-orphaned"), now);
|
|
68
69
|
repaired.push(task.id);
|
|
69
70
|
}
|
|
70
71
|
return repaired;
|