@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.
Files changed (90) hide show
  1. package/README.md +34 -5
  2. package/dist/cli/commandCatalog.js +173 -57
  3. package/dist/cli/helpRenderer.js +3 -1
  4. package/dist/cli.js +99 -11
  5. package/dist/commands/configCommands.js +521 -171
  6. package/dist/commands/deliveryGuardPreflight.js +2 -2
  7. package/dist/commands/executionAuditCommands.js +56 -3
  8. package/dist/commands/projectCommands.js +504 -2
  9. package/dist/commands/releaseCommands.js +0 -1
  10. package/dist/commands/taskActor.js +17 -0
  11. package/dist/commands/taskBaseCommands.js +29 -0
  12. package/dist/commands/taskCommands.js +577 -126
  13. package/dist/commands/taskContextCommand.js +36 -2
  14. package/dist/commands/taskNextActionCommand.js +48 -3
  15. package/dist/commands/taskPublicationCommands.js +319 -0
  16. package/dist/commands/taskRoleRuntimeStatus.js +83 -59
  17. package/dist/commands/telemetryCommands.js +14 -13
  18. package/dist/config/yuiConfig.js +161 -8
  19. package/dist/context/sessionContextBudget.js +68 -0
  20. package/dist/context/wakeNotification.js +65 -0
  21. package/dist/controller/clientRuntime.js +2 -1
  22. package/dist/controller/ephemeralResourceReaper.js +2 -1
  23. package/dist/controller/fileSchedulerStoreAdapter.js +183 -16
  24. package/dist/controller/jobSupervisor.js +5 -4
  25. package/dist/controller/resourceCleanupLinux.js +6 -6
  26. package/dist/controller/resourceInventoryLinux.js +3 -3
  27. package/dist/controller/runtime.js +88 -10
  28. package/dist/controller/updateReconciliation.js +4 -3
  29. package/dist/doctor/doctor.js +26 -7
  30. package/dist/executor/agentConfigurationCatalog.js +18 -0
  31. package/dist/executor/fileRoleLaunchPlanner.js +3 -3
  32. package/dist/lifecycle/contextBudgetRollover.js +81 -0
  33. package/dist/lifecycle/exactRunTerminalization.js +11 -1
  34. package/dist/lifecycle/providerErrorClass.js +33 -12
  35. package/dist/observability/executionAudit.js +214 -6
  36. package/dist/output/table.js +18 -0
  37. package/dist/repository/gitWorkspace.js +92 -0
  38. package/dist/repository/project.js +218 -4
  39. package/dist/repository/taskBaseFreshness.js +318 -0
  40. package/dist/repository/taskWorkspacePreparer.js +16 -2
  41. package/dist/review/deltaRecheck.js +232 -0
  42. package/dist/review/reviewConfig.js +31 -0
  43. package/dist/review/reviewFindingLedger.js +5 -1
  44. package/dist/review/reviewRound.js +156 -1
  45. package/dist/run/providerRetry.js +21 -3
  46. package/dist/run/providerRetryConfig.js +13 -60
  47. package/dist/run/recoveryProjection.js +199 -0
  48. package/dist/runtime/builtinAgentDrivers.js +3 -0
  49. package/dist/runtime/builtinTranscriptUsage.js +76 -32
  50. package/dist/runtime/continuationManager.js +17 -0
  51. package/dist/runtime/index.js +2 -0
  52. package/dist/runtime/launchDiagnostics.js +154 -0
  53. package/dist/runtime/lifecycleReservation.js +13 -0
  54. package/dist/runtime/providerContinuation.js +38 -0
  55. package/dist/runtime/providerContinuationReconciliationService.js +1 -0
  56. package/dist/runtime/providerErrorCodes.js +278 -0
  57. package/dist/runtime/runtimeHealthPolicy.js +20 -0
  58. package/dist/runtime/runtimeObservation.js +1 -0
  59. package/dist/runtime/runtimeProjection.js +115 -23
  60. package/dist/runtime/tmuxAdapters.js +242 -48
  61. package/dist/scheduler/activeRoleRunDelivery.js +139 -3
  62. package/dist/scheduler/activeTaskProgress.js +4 -3
  63. package/dist/scheduler/leaderWakeupProcessor.js +105 -15
  64. package/dist/scheduler/roleRunLiveness.js +2 -1
  65. package/dist/scheduler/roleRunStall.js +3 -2
  66. package/dist/scheduler/taskWake.js +72 -0
  67. package/dist/scheduler/wakeReason.js +64 -0
  68. package/dist/scheduler/wakeupQueue.js +2 -1
  69. package/dist/setup/setupCommand.js +1 -1
  70. package/dist/storage/migration/productionRegistry.js +325 -1
  71. package/dist/storage/sqliteSchema.js +61 -2
  72. package/dist/storage/sqliteStore.js +129 -2
  73. package/dist/storage/storeRpc.js +1 -0
  74. package/dist/storage/taskStore.js +262 -5
  75. package/dist/storage/upgrade/recordVersions.js +6 -1
  76. package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
  77. package/dist/task/completionReadiness.js +282 -0
  78. package/dist/task/publicationReference.js +123 -0
  79. package/dist/task/taskRecordReference.js +3 -1
  80. package/dist/telemetry/telemetryConfig.js +23 -18
  81. package/dist/telemetry/telemetryWiring.js +8 -8
  82. package/dist/tmux/tmuxManager.js +50 -9
  83. package/dist/web/assets/client/i18n.js +4 -0
  84. package/dist/web/assets/client/view.js +18 -0
  85. package/dist/web/webSnapshot.js +100 -10
  86. package/i18n/README.zh-CN.md +5 -5
  87. package/package.json +1 -1
  88. package/skills/yui-leader/SKILL.md +49 -10
  89. package/skills/yui-operator/SKILL.md +13 -3
  90. 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
- ? this.planner.plan({ taskId: request.owner.taskId, ...input })
201
- : this.planner.planGlobalRole(input);
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
- // Process creation is last: every local invariant has already passed.
229
- const hostCreated = await ensureRoleWindow(this.tmux, hostId, planned.role, planned.launch);
230
- if (hostCreated
231
- && request.owner.scope === "task"
232
- && planned.launch.env.YUI_JOB_CALLER_KEY !== undefined
233
- && this.planner.commitTaskCallerKey !== undefined) {
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
- const binding = createRuntimeBinding({
242
- id: bindingId,
243
- launchId: request.launchId,
244
- owner: request.owner,
440
+ throw toRuntimeLaunchFailure(error, "validation", {
245
441
  agentId: request.agentId,
246
- adapterId: request.adapterId,
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
- try {
298
- return tmux.inspectRolePaneAsync === undefined
299
- ? tmux.inspectRolePane?.(hostId, roleName)
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;