@zq-silk/yui 0.13.8 → 0.13.9

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 (67) hide show
  1. package/README.md +9 -8
  2. package/dist/cli/commandCatalog.js +21 -2
  3. package/dist/cli.js +42 -13
  4. package/dist/commands/agentCommands.js +1 -1
  5. package/dist/commands/configCommands.js +1 -86
  6. package/dist/commands/executionAuditCommands.js +17 -16
  7. package/dist/commands/globalRoleCommands.js +4 -4
  8. package/dist/commands/sessionCommands.js +2 -6
  9. package/dist/commands/taskActor.js +1 -2
  10. package/dist/commands/taskCommands.js +92 -9
  11. package/dist/commands/taskRoleRuntimeStatus.js +3 -3
  12. package/dist/config/configCatalog.js +1 -6
  13. package/dist/config/yuiConfig.js +0 -79
  14. package/dist/controller/clientRuntime.js +40 -3
  15. package/dist/controller/controller.js +29 -52
  16. package/dist/controller/fileSchedulerStoreAdapter.js +305 -1056
  17. package/dist/controller/runtime.js +12 -5
  18. package/dist/controller/runtimeHookRunFence.js +3 -9
  19. package/dist/controller/runtimeLaunchCoordinator.js +44 -67
  20. package/dist/controller/structuredProviderObservation.js +18 -5
  21. package/dist/coordination/workMailbox.js +4 -4
  22. package/dist/execution/executionHealth.js +1 -1
  23. package/dist/executor/agentExecutor.js +92 -68
  24. package/dist/executor/executorRegistry.js +8 -20
  25. package/dist/executor/fileRoleLaunchPlanner.js +24 -90
  26. package/dist/executor/turnCompletion.js +5 -5
  27. package/dist/lifecycle/exactRunTerminalization.js +1 -1
  28. package/dist/observability/executionAudit.js +40 -94
  29. package/dist/operator/operatorSessionHistory.js +7 -5
  30. package/dist/role/role.js +1 -1
  31. package/dist/run/agentRun.js +4 -54
  32. package/dist/runtime/agentDriver.js +2 -0
  33. package/dist/runtime/agentError.js +114 -0
  34. package/dist/runtime/agentHost.js +55 -82
  35. package/dist/runtime/builtinAgentDrivers.js +21 -9
  36. package/dist/runtime/builtinAgentErrorMappers.js +150 -0
  37. package/dist/runtime/exactControlPlane.js +6 -12
  38. package/dist/runtime/index.js +0 -1
  39. package/dist/runtime/launchBroker.js +5 -19
  40. package/dist/runtime/lifecycleReservation.js +20 -4
  41. package/dist/runtime/providerRuntimeIdentity.js +3 -2
  42. package/dist/runtime/runtimeBinding.js +0 -27
  43. package/dist/runtime/runtimeObservation.js +7 -16
  44. package/dist/runtime/runtimeSessionCandidate.js +3 -10
  45. package/dist/runtime/sessionLaunchRequest.js +1 -2
  46. package/dist/runtime/sessionReconciliation.js +2 -2
  47. package/dist/runtime/structuredProviderHost.js +44 -79
  48. package/dist/runtime/taskRuntimeIsolation.js +0 -7
  49. package/dist/runtime/tmuxAdapters.js +6 -49
  50. package/dist/scheduler/activeRoleRunDelivery.js +218 -168
  51. package/dist/scheduler/leaderWakeupProcessor.js +126 -86
  52. package/dist/scheduler/roleRunLiveness.js +4 -1
  53. package/dist/scheduler/roleRunStall.js +7 -11
  54. package/dist/scheduler/wakeReason.js +4 -0
  55. package/dist/storage/migration/productionRegistry.js +332 -0
  56. package/dist/storage/sqliteSchema.js +54 -2
  57. package/dist/storage/sqliteStore.js +11 -44
  58. package/dist/storage/taskStore.js +7 -35
  59. package/package.json +1 -1
  60. package/skills/yui-leader/SKILL.md +30 -8
  61. package/skills/yui-operator/SKILL.md +14 -6
  62. package/skills/yui-runtime/SKILL.md +6 -4
  63. package/dist/lifecycle/providerErrorClass.js +0 -152
  64. package/dist/run/providerRetry.js +0 -226
  65. package/dist/run/providerRetryConfig.js +0 -27
  66. package/dist/runtime/providerErrorCodes.js +0 -278
  67. package/dist/runtime/providerRecoveryDecision.js +0 -55
package/README.md CHANGED
@@ -48,7 +48,7 @@ behavior, then apply only changes the user confirms.
48
48
 
49
49
  Durable settings are grouped by responsibility: `config system` for Home
50
50
  defaults and presentation, `config runtime` for Controller health, concurrency,
51
- launch, delivery, and Provider retry, `config workflow` for Leader/context/review
51
+ launch, and delivery mechanics, `config workflow` for Leader/context/review
52
52
  policy, `config resources` for quarantine and GC, and `config tools` for tmux
53
53
  and diagnostic telemetry. Configured Agents, global Roles, Profiles, and shell
54
54
  completion remain the sibling `config agent|role|profile|completion` domains.
@@ -892,8 +892,9 @@ creates a fresh proxy attachment.
892
892
  If the proxy disconnects, the Host may attach a bounded replacement client and
893
893
  reconcile the exact owned Turn from native history. A failed fresh attachment
894
894
  is released instead of becoming a cleanup prerequisite for later Runs.
895
- Claude Code keeps its independent stream-json process with exact
896
- user-message replay acknowledgement. A timeout or uncertain write becomes
895
+ Claude Code keeps its independent stream-json process. Agent Host is the sole
896
+ writer to that process, so a completed stream write accepts the Turn; the
897
+ later provider `result` event settles it. An uncertain write becomes
897
898
  `delivery-unknown` and is never automatically retried.
898
899
 
899
900
  Task Role observation and takeover are explicit:
@@ -912,11 +913,11 @@ backpressure for Yui rather than a failed Run.
912
913
  AgentRun is the only durable Role scheduling state. Conversation state does not
913
914
  carry a second current-Run pointer; each Provider Turn records a Run id only to
914
915
  correlate its receipt and terminal event. If an Agent finishes a Yui Run before
915
- the native Turn terminal arrives, the next mailbox batch may already become a
916
- new AgentRun, but Agent Host keeps its input pending and submits it only after
917
- the old Turn settles. TaskRole itself stores identity and desired launch
918
- configuration, not runtime status; Role status shown by CLI/Web is derived from
919
- the active AgentRun plus Session/Driver lifecycle facts.
916
+ the native Turn terminal arrives, the next mailbox intent remains pending until
917
+ that Turn settles. Yui then claims the new AgentRun and submits it through the
918
+ same Session. TaskRole itself stores identity and desired launch configuration,
919
+ not runtime status; Role status shown by CLI/Web is derived from the active
920
+ AgentRun plus Session/Driver lifecycle facts.
920
921
 
921
922
  Global Operator and global Role sessions remain native interactive CLIs:
922
923
 
@@ -48,7 +48,7 @@ const CONFIG_KEY_VALUES = CONFIG_DEFINITIONS.map((definition) => ({
48
48
  }));
49
49
  const CONFIG_DOMAIN_SUMMARIES = {
50
50
  system: "Configure Home-wide defaults and human-facing presentation.",
51
- runtime: "Configure Controller recovery, concurrency, health, launch, delivery, and Provider retry policy.",
51
+ runtime: "Configure Controller recovery, concurrency, health, launch, and delivery mechanics.",
52
52
  workflow: "Configure Leader convergence, context, and optional review policy.",
53
53
  resources: "Configure resource garbage collection and quarantine policy.",
54
54
  tools: "Configure tmux and optional diagnostic telemetry."
@@ -559,7 +559,7 @@ const taskChildren = [
559
559
  summary: "Manage Roles within a Task.",
560
560
  sections: [{ id: "manage", title: "Commands", entries: [
561
561
  "add", "list", "status", "show", "update", "remove", "bind", "unbind",
562
- "view", "takeover", "release"
562
+ "session", "view", "takeover", "release"
563
563
  ] }],
564
564
  children: [
565
565
  {
@@ -587,6 +587,25 @@ const taskChildren = [
587
587
  { name: "remove", summary: "Remove a Task Role.", usage: "yui task role remove <task> <role>" },
588
588
  { name: "bind", summary: "Bind and activate an Agent for a Task Role.", usage: "yui task role bind <task> <role> <agent-id>" },
589
589
  { name: "unbind", summary: "Unbind a dormant Agent from a Task Role.", usage: "yui task role unbind <task> <role> <agent-id>" },
590
+ {
591
+ name: "session",
592
+ summary: "Inspect or stop one Task Role's native Session.",
593
+ executable: true,
594
+ sections: [{ id: "manage", title: "Commands", entries: ["inspect", "stop"] }],
595
+ children: [
596
+ {
597
+ name: "inspect",
598
+ summary: "Read the current Session, Host activation, and Turn facts.",
599
+ usage: "yui task role session inspect <task> <role>"
600
+ },
601
+ {
602
+ name: "stop",
603
+ summary: "Stop one idle Session and its exact Host activation.",
604
+ usage: "yui task role session stop <task> <role> --reason <text>",
605
+ options: ["--reason"]
606
+ }
607
+ ]
608
+ },
590
609
  {
591
610
  name: "view",
592
611
  summary: "Attach read-only to an independent Provider presentation surface.",
package/dist/cli.js CHANGED
@@ -366,7 +366,16 @@ export async function main() {
366
366
  return;
367
367
  }
368
368
  if (args[1] === "runtime-hook" && args.length === 2) {
369
- await runRuntimeObservationHookCommand(readFileSync(0, "utf8"), process.env);
369
+ // Provider lifecycle Hooks are observation channels, never execution
370
+ // gates. A late/stale Hook must not make Claude reject an otherwise
371
+ // valid Session or Turn; its exact fence is revalidated before any
372
+ // inbox fact is written, so dropping an invalid observation is safe.
373
+ try {
374
+ await runRuntimeObservationHookCommand(readFileSync(0, "utf8"), process.env);
375
+ }
376
+ catch {
377
+ return;
378
+ }
370
379
  return;
371
380
  }
372
381
  throw usageError("Internal lifecycle callback usage is invalid.");
@@ -1402,6 +1411,30 @@ export async function main() {
1402
1411
  if (jsonOutput) {
1403
1412
  throw usageError("Task Role view/takeover requires an interactive terminal.");
1404
1413
  }
1414
+ if (result.kind === "session-stop") {
1415
+ await ensureFileTaskController(home, { environment: process.env });
1416
+ try {
1417
+ await runtime.stopExactTaskRoleSession({
1418
+ taskId: result.taskId,
1419
+ roleName: result.roleName,
1420
+ agentId: result.agentId,
1421
+ adapterId: result.adapterId,
1422
+ nativeSessionId: result.nativeSessionId,
1423
+ ...(result.launchId === undefined ? {} : { launchId: result.launchId }),
1424
+ sessionUpdatedAt: result.sessionUpdatedAt
1425
+ });
1426
+ }
1427
+ catch (error) {
1428
+ throw runtimeError(`Session stop was requested but physical Host cleanup did not complete: ${error instanceof Error ? error.message : String(error)}`);
1429
+ }
1430
+ emit(result.output, false, {
1431
+ taskId: result.taskId,
1432
+ roleName: result.roleName,
1433
+ stopped: true,
1434
+ reason: result.reason
1435
+ });
1436
+ return;
1437
+ }
1405
1438
  if (result.kind === "view") {
1406
1439
  if (result.output !== undefined)
1407
1440
  emit(result.output);
@@ -1604,17 +1637,14 @@ async function preflightManagedTaskControlPlane() {
1604
1637
  && process.env.YUI_DRIVER_ID !== undefined
1605
1638
  ? builtinAgentDriverRegistry().require(process.env.YUI_DRIVER_ID)
1606
1639
  : undefined;
1607
- const preallocatedDriverCallback = runtimeDriverCallback
1608
- ?.capabilities.observation.sessionBootstrap === "preallocated";
1609
1640
  const verifiedStore = openCompatibleFileTaskStore(control.yuiHome);
1610
- assertExactTaskRuntimeState(runtime, verifiedStore, preallocatedDriverCallback
1611
- ? {
1612
- preallocatedDriverSessionReservation: {
1613
- yuiHome: control.yuiHome,
1614
- adapterId: runtimeDriverCallback.adapterId
1615
- }
1616
- }
1617
- : {});
1641
+ // Runtime Hooks have their own event-aware fence, including the valid case
1642
+ // where a Provider terminal arrives after its Yui Run has yielded and a
1643
+ // later wake is pending. Ordinary managed commands still require the exact
1644
+ // current mutable runtime before routing.
1645
+ if (runtimeDriverCallback === undefined) {
1646
+ assertExactTaskRuntimeState(runtime, verifiedStore);
1647
+ }
1618
1648
  const request = taskFinalReviewInvocation.request;
1619
1649
  if (request === undefined) {
1620
1650
  return {
@@ -2334,8 +2364,7 @@ async function executeOperatorSessionControl(control, home, store, runtime, tmux
2334
2364
  await ensureFileTaskController(home, { environment: process.env });
2335
2365
  if (paneRunning
2336
2366
  || (active !== undefined
2337
- && active.status !== "stopped"
2338
- && active.status !== "broken")) {
2367
+ && active.status === "active")) {
2339
2368
  await runtime.stopGlobalRoleSession(role.name);
2340
2369
  }
2341
2370
  applyOperatorSessionControl(control, store);
@@ -244,7 +244,7 @@ function findNonStoppedSessionReference(store, agentId) {
244
244
  }
245
245
  function sessionReference(set, agentId, reference) {
246
246
  const session = set.sessions[agentId];
247
- if (session === undefined || session.status === "stopped")
247
+ if (session === undefined || session.status === "ended")
248
248
  return null;
249
249
  return { ...reference, status: session.status };
250
250
  }
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, realpathSync } from "node:fs";
2
2
  import { isAbsolute, relative, resolve } from "node:path";
3
3
  import { usageError } from "../errors/cliError.js";
4
- import { DEFAULT_AGENT_LAUNCH_INACTIVITY_TIMEOUT_SECONDS, DEFAULT_CONTROLLER_TASK_CONCURRENCY, DEFAULT_DELIVERY_TIMEOUT_SECONDS, DEFAULT_LEADER_NEXT_ACTION_MODE, DEFAULT_LEADER_SEMANTIC_BUDGET_TURNS, DEFAULT_PROVIDER_RETRY_DELAYS_SECONDS, DEFAULT_PROVIDER_RETRY_MAX_WINDOW_SECONDS, DEFAULT_PROVIDER_RETRY_MODE, DEFAULT_RECONCILIATION_INTERVAL_SECONDS, DEFAULT_RESOURCES_GC_MODE, DEFAULT_RESOURCES_QUARANTINE_TTL_HOURS, DEFAULT_TMUX_HISTORY_LIMIT, LEADER_NEXT_ACTION_MODES, PROVIDER_RETRY_MODES, reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveContextBudget, resolveDeliveryTimeoutSeconds, resolveLeaderNextActionMode, resolveLeaderSemanticBudgetTurns, resolveProviderRetryAdapters, resolveProviderRetryDelaysSeconds, resolveProviderRetryMaxWindowSeconds, resolveProviderRetryMode, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode, resolveResourcesQuarantineTtlHours, resolveRuntimeHealth, resolveTelemetryEnabled, resolveTelemetryRunCap, resolveTelemetryTerminalKeep, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
4
+ import { DEFAULT_AGENT_LAUNCH_INACTIVITY_TIMEOUT_SECONDS, DEFAULT_CONTROLLER_TASK_CONCURRENCY, DEFAULT_DELIVERY_TIMEOUT_SECONDS, DEFAULT_LEADER_NEXT_ACTION_MODE, DEFAULT_LEADER_SEMANTIC_BUDGET_TURNS, DEFAULT_RECONCILIATION_INTERVAL_SECONDS, DEFAULT_RESOURCES_GC_MODE, DEFAULT_RESOURCES_QUARANTINE_TTL_HOURS, DEFAULT_TMUX_HISTORY_LIMIT, LEADER_NEXT_ACTION_MODES, reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveContextBudget, resolveDeliveryTimeoutSeconds, resolveLeaderNextActionMode, resolveLeaderSemanticBudgetTurns, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode, resolveResourcesQuarantineTtlHours, resolveRuntimeHealth, resolveTelemetryEnabled, resolveTelemetryRunCap, resolveTelemetryTerminalKeep, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
5
5
  import { CONFIG_DEFINITIONS, CONFIG_KEYS, configDefinition, configDefinitionsForDomain } from "../config/configCatalog.js";
6
6
  import { resolveTimeZone } from "../output/timePresentation.js";
7
7
  import { defaultTableWidth, renderTable } from "../output/table.js";
@@ -73,10 +73,6 @@ export function effectiveConfigData(config, domain) {
73
73
  resourcesGcMode: resolveResourcesGcMode(config.resourcesGcMode),
74
74
  resourcesGcAutoQuarantine: resolveResourcesGcAutoQuarantine(config.resourcesGcAutoQuarantine),
75
75
  resourcesQuarantineTtlHours: resolveResourcesQuarantineTtlHours(config.resourcesQuarantineTtlHours),
76
- providerRetryMode: resolveProviderRetryMode(config.providerRetryMode),
77
- providerRetryAdapters: resolveProviderRetryAdapters(config.providerRetryAdapters),
78
- providerRetryDelaysSeconds: resolveProviderRetryDelaysSeconds(config.providerRetryDelaysSeconds),
79
- providerRetryMaxWindowSeconds: resolveProviderRetryMaxWindowSeconds(config.providerRetryMaxWindowSeconds),
80
76
  runtimeHealth: {
81
77
  quietAfterSeconds: health.quietAfterMs / 1_000,
82
78
  diagnosticAfterSeconds: health.diagnosticAfterMs / 1_000,
@@ -498,87 +494,6 @@ const CONFIG_KEY_HANDLERS = [
498
494
  return `Delivery timeout reset to ${DEFAULT_DELIVERY_TIMEOUT_SECONDS} seconds\n`;
499
495
  }
500
496
  },
501
- {
502
- key: "provider-retry-mode",
503
- showLabel: "Provider retry mode",
504
- showValue: (config) => resolveProviderRetryMode(config.providerRetryMode),
505
- set(args, store) {
506
- if (args.length !== 1)
507
- throw usageError(`Runtime config set usage: yui config runtime set provider-retry-mode <${PROVIDER_RETRY_MODES.join("|")}>.`);
508
- const mode = validatedConfigValue(() => resolveProviderRetryMode(args[0]), `Runtime config set usage: yui config runtime set provider-retry-mode <${PROVIDER_RETRY_MODES.join("|")}>.`);
509
- saveConfigKey(store, (config) => ({ ...config, providerRetryMode: mode }));
510
- return `Provider retry mode set to ${mode}\n`;
511
- },
512
- clear(store) {
513
- saveConfigKey(store, (config) => {
514
- const { providerRetryMode: _removed, ...rest } = config;
515
- return rest;
516
- });
517
- return `Provider retry mode reset to ${DEFAULT_PROVIDER_RETRY_MODE}\n`;
518
- }
519
- },
520
- {
521
- key: "provider-retry-adapters",
522
- showLabel: "Provider retry adapters",
523
- showValue: (config) => resolveProviderRetryAdapters(config.providerRetryAdapters).join(", ") || "none",
524
- set(args, store) {
525
- if (args.length !== 1)
526
- throw usageError("Runtime config set usage: yui config runtime set provider-retry-adapters <all|claude,codex|off>.");
527
- const raw = args[0].trim().toLowerCase();
528
- const adapters = raw === "off" || raw === "" || raw === "0"
529
- ? []
530
- : validatedConfigValue(() => resolveProviderRetryAdapters(raw.split(",")), "Runtime config set usage: yui config runtime set provider-retry-adapters <all|claude,codex|off>.");
531
- saveConfigKey(store, (config) => ({ ...config, providerRetryAdapters: adapters }));
532
- return `Provider retry adapters set to ${adapters.join(", ") || "none"}\n`;
533
- },
534
- clear(store) {
535
- saveConfigKey(store, (config) => {
536
- const { providerRetryAdapters: _removed, ...rest } = config;
537
- return rest;
538
- });
539
- return "Provider retry adapters reset to all supported\n";
540
- }
541
- },
542
- {
543
- key: "provider-retry-delays-seconds",
544
- showLabel: "Provider retry delays",
545
- showValue: (config) => `${resolveProviderRetryDelaysSeconds(config.providerRetryDelaysSeconds).join(", ")} seconds`,
546
- set(args, store) {
547
- const usage = "Runtime config set usage: yui config runtime set provider-retry-delays-seconds <comma-separated-seconds>.";
548
- if (args.length !== 1)
549
- throw usageError(usage);
550
- const providerRetryDelaysSeconds = validatedConfigValue(() => resolveProviderRetryDelaysSeconds(args[0].split(",").map(Number)), usage);
551
- saveConfigKey(store, (config) => ({ ...config, providerRetryDelaysSeconds }));
552
- return `Provider retry delays set to ${providerRetryDelaysSeconds.join(", ")} seconds\n`;
553
- },
554
- clear(store) {
555
- saveConfigKey(store, (config) => {
556
- const { providerRetryDelaysSeconds: _removed, ...rest } = config;
557
- return rest;
558
- });
559
- return `Provider retry delays reset to ${DEFAULT_PROVIDER_RETRY_DELAYS_SECONDS.join(", ")} seconds\n`;
560
- }
561
- },
562
- {
563
- key: "provider-retry-max-window-seconds",
564
- showLabel: "Provider retry max window",
565
- showValue: (config) => `${resolveProviderRetryMaxWindowSeconds(config.providerRetryMaxWindowSeconds)} seconds`,
566
- set(args, store) {
567
- const usage = "Runtime config set usage: yui config runtime set provider-retry-max-window-seconds <positive-seconds>.";
568
- if (args.length !== 1)
569
- throw usageError(usage);
570
- const providerRetryMaxWindowSeconds = validatedConfigValue(() => resolveProviderRetryMaxWindowSeconds(Number(args[0])), usage);
571
- saveConfigKey(store, (config) => ({ ...config, providerRetryMaxWindowSeconds }));
572
- return `Provider retry max window set to ${providerRetryMaxWindowSeconds} seconds\n`;
573
- },
574
- clear(store) {
575
- saveConfigKey(store, (config) => {
576
- const { providerRetryMaxWindowSeconds: _removed, ...rest } = config;
577
- return rest;
578
- });
579
- return `Provider retry max window reset to ${DEFAULT_PROVIDER_RETRY_MAX_WINDOW_SECONDS} seconds\n`;
580
- }
581
- },
582
497
  {
583
498
  key: "tmux-bin",
584
499
  showLabel: "Tmux bin",
@@ -183,29 +183,30 @@ export function renderExecutionAudit(report, width = defaultTableWidth()) {
183
183
  else {
184
184
  lines.push("", ...sectionError("events", report));
185
185
  }
186
- if (report.providerRetries.status === "ok" && report.providerRetries.data !== undefined) {
187
- const retries = report.providerRetries.data;
188
- if (retries.total > 0) {
189
- lines.push("", `Provider retries: ${retries.total} run(s) retried in place · ${retries.terminal} terminal`);
190
- lines.push(renderTable("Provider retry lineages", [
186
+ if (report.agentErrors.status === "ok" && report.agentErrors.data !== undefined) {
187
+ const errors = report.agentErrors.data;
188
+ if (errors.total > 0) {
189
+ lines.push("", `Agent errors: ${errors.total} · ${Object.entries(errors.byCategory)
190
+ .map(([category, count]) => `${category}:${count}`).join(", ")}`);
191
+ lines.push(renderTable("Agent errors", [
191
192
  { header: "Task", minWidth: 8, maxWidth: 14 },
192
193
  { header: "Run", minWidth: 14, maxWidth: 24 },
193
194
  { header: "Role", minWidth: 8, maxWidth: 12 },
194
- { header: "Attempts", minWidth: 8, maxWidth: 10 },
195
- { header: "Error class", minWidth: 14, maxWidth: 24 },
196
- { header: "Decision", minWidth: 14, maxWidth: 26 }
197
- ], retries.entries.map((entry) => [
195
+ { header: "Category", minWidth: 12, maxWidth: 20 },
196
+ { header: "Code", minWidth: 16, maxWidth: 32 },
197
+ { header: "Session", minWidth: 12, maxWidth: 16 }
198
+ ], errors.entries.map((entry) => [
198
199
  entry.taskId,
199
200
  entry.runId,
200
201
  entry.roleName,
201
- String(entry.attempts),
202
- entry.errorClass,
203
- entry.decision
202
+ entry.category,
203
+ entry.code,
204
+ entry.sessionDisposition
204
205
  ]), width));
205
206
  }
206
207
  }
207
208
  else {
208
- lines.push("", ...sectionError("providerRetries", report));
209
+ lines.push("", ...sectionError("agentErrors", report));
209
210
  }
210
211
  if (report.workItems.status === "ok" && report.workItems.data !== undefined) {
211
212
  const items = report.workItems.data;
@@ -257,8 +258,8 @@ export function renderExecutionAudit(report, width = defaultTableWidth()) {
257
258
  const versions = Object.entries(runtime.contextProtocolVersions)
258
259
  .map(([version, count]) => `${version}:${count}`)
259
260
  .join(", ") || "none";
260
- const retryStates = Object.entries(runtime.activeRetryStates)
261
- .map(([state, count]) => `${state}:${count}`)
261
+ const errorCategories = Object.entries(runtime.agentErrorCategories)
262
+ .map(([category, count]) => `${category}:${count}`)
262
263
  .join(", ") || "none";
263
264
  const exits = Object.entries(runtime.processExitClassifications)
264
265
  .map(([classification, count]) => `${classification}:${count}`)
@@ -266,7 +267,7 @@ export function renderExecutionAudit(report, width = defaultTableWidth()) {
266
267
  const usage = Object.entries(runtime.usageSemantics)
267
268
  .map(([semantics, count]) => `${semantics}:${count}`)
268
269
  .join(", ") || "none";
269
- lines.push("", `Runtime protocol: context versions ${versions} · manifest compatibility identities ${runtime.manifestCompatibilityDigests}`, `Active retry: ${runtime.activeRetryEpisodes} episode(s) [${retryStates}] · failures ${runtime.activeConsecutiveFailures} · dispatched ${runtime.activeDispatchedRetries}`, `Retry audit: ${runtime.retryClassifiedEvents} classified · ${runtime.retryDispatchedEvents} dispatched · ${runtime.retryRecoveredEvents} recovered · ${runtime.retryExhaustedEvents} exhausted`, `Process exits: ${runtime.processExitObservations} [${exits}] · capacity failures ${runtime.contextCapacityFailures}`, `Context telemetry: usage [${usage}] · native compaction events ${runtime.compactionEvents}`);
270
+ lines.push("", `Runtime protocol: context versions ${versions} · manifest compatibility identities ${runtime.manifestCompatibilityDigests}`, `Agent errors: ${runtime.agentErrors} [${errorCategories}]`, `Process exits: ${runtime.processExitObservations} [${exits}] · capacity failures ${runtime.contextCapacityFailures}`, `Context telemetry: usage [${usage}] · native compaction events ${runtime.compactionEvents}`);
270
271
  }
271
272
  else {
272
273
  lines.push("", ...sectionError("runtimeProtocol", report));
@@ -247,7 +247,7 @@ function bindRole(args, store) {
247
247
  const switched = switchActiveRoleAgent(withBinding, existingSet ?? createRoleSessionSet({ scope: "global", roleName: name }, role.activeAgentId, now), agentId, {
248
248
  activeRun: false,
249
249
  nativeProcessRunning: activeSession !== undefined
250
- && activeSession.status !== "stopped"
250
+ && activeSession.status === "active"
251
251
  }, now);
252
252
  tx.saveGlobalRoleWithSessionSet(switched.role, switched.sessions);
253
253
  return { message: `Bound role ${name} to ${agentId}`, role: switched.role };
@@ -271,7 +271,7 @@ function removeRole(args, store) {
271
271
  roleName: role.name
272
272
  }, "removal");
273
273
  const sessions = tx.getGlobalRoleSessionSet(name);
274
- if (Object.values(sessions?.sessions ?? {}).some(({ status }) => status !== "stopped")) {
274
+ if (Object.values(sessions?.sessions ?? {}).some(({ status }) => status === "active")) {
275
275
  throw usageError(`GlobalRole is active and cannot be removed: ${name}.`);
276
276
  }
277
277
  if (!tx.removeGlobalRole(name))
@@ -336,7 +336,7 @@ function roleSession(args, store, options) {
336
336
  adapterId: binding.adapterId,
337
337
  nativeSessionId,
338
338
  policy: "fixed",
339
- status: environment.YUI_ROLE === name ? "running" : "ready",
339
+ status: "active",
340
340
  effective: resolveEffectiveLaunch({ role, purpose: "execution" })
341
341
  };
342
342
  if (command === "record") {
@@ -349,7 +349,7 @@ function roleSession(args, store, options) {
349
349
  if (existing === null) {
350
350
  throw usageError("Native session replacement requires an existing native session.");
351
351
  }
352
- if (existing.status !== "stopped") {
352
+ if (existing.status !== "ended") {
353
353
  throw usageError("Native session replacement is blocked while the native Agent process is running.");
354
354
  }
355
355
  if (existing.nativeSessionId === nativeSessionId) {
@@ -75,16 +75,12 @@ function blockedSessions(input) {
75
75
  return [];
76
76
  return [{
77
77
  session,
78
- reason: session.status === "running"
79
- ? "running"
80
- : "runtime-work-pending"
78
+ reason: "runtime-work-pending"
81
79
  }];
82
80
  });
83
81
  }
84
82
  function blockedStopResult(blocked) {
85
- const details = blocked.map(({ session, reason }) => (`- ${renderSessionOwner(session)} (${reason === "running"
86
- ? "a Turn or Run is still running"
87
- : "an active Run or lifecycle operation is still pending"})`));
83
+ const details = blocked.map(({ session }) => (`- ${renderSessionOwner(session)} (an active Run or lifecycle operation is still pending)`));
88
84
  return {
89
85
  output: [
90
86
  `Cannot stop managed Sessions: ${blocked.length} Session(s) are still busy.`,
@@ -207,8 +207,7 @@ export function taskLeaderActionRunId(store, taskId, environment, yuiHome) {
207
207
  if (session === undefined
208
208
  || identity(session.nativeSessionId) === undefined
209
209
  || identity(session.launchId) === undefined
210
- || session.status === "stopped"
211
- || session.status === "broken"
210
+ || session.status === "ended"
212
211
  || session.adapterId !== adapterId
213
212
  || (explicitAssertion === undefined && session.launchId !== launchId))
214
213
  return undefined;
@@ -1276,6 +1276,8 @@ function taskRoleCommand(args, store, options) {
1276
1276
  return output(bindTaskRole(rest, store, options));
1277
1277
  if (command === "unbind")
1278
1278
  return output(unbindTaskRole(rest, store, options));
1279
+ if (command === "session")
1280
+ return taskRoleSessionCommand(rest, store, options);
1279
1281
  if (command === "view")
1280
1282
  return viewTaskRole(rest, store);
1281
1283
  if (command === "takeover")
@@ -1286,6 +1288,87 @@ function taskRoleCommand(args, store, options) {
1286
1288
  ? "Task role command is required."
1287
1289
  : `Unknown command: task role ${command}`);
1288
1290
  }
1291
+ function taskRoleSessionCommand(args, store, options) {
1292
+ const [command, ...rest] = args;
1293
+ if (command === "inspect") {
1294
+ exactPositionals(rest, 2, "Task Role Session inspect usage: yui task role session inspect <task> <role>.");
1295
+ const task = requireTask(store, rest[0]);
1296
+ const role = requireRole(store, task.id, rest[1]);
1297
+ taskActor(store, options, task.id);
1298
+ const sessions = store.getTaskRoleSessionSet(task.id, role.name);
1299
+ const active = sessions?.sessions[sessions.activeAgentId] ?? null;
1300
+ const binding = sessions?.providerBinding ?? null;
1301
+ return output(active === null
1302
+ ? `No Session exists for ${task.id}/${role.name}.\n`
1303
+ : [
1304
+ `Session ${task.id}/${role.name}`,
1305
+ `Agent: ${active.agentId}/${active.adapterId}`,
1306
+ `Native id: ${active.nativeSessionId}`,
1307
+ `Host activation: ${active.launchId ?? "none"}`,
1308
+ `Session: ${active.status}${active.endReason === undefined ? "" : `/${active.endReason}`}`,
1309
+ `Turn: ${binding?.turn?.status ?? "none"}`
1310
+ ].join("\n") + "\n", { task, role, session: active, providerBinding: binding });
1311
+ }
1312
+ if (command === "stop") {
1313
+ const usage = "Task Role Session stop usage: yui task role session stop <task> <role> --reason <text>.";
1314
+ const parsed = parseTail(rest, new Set(["--reason"]), usage);
1315
+ exactPositionals(parsed.positionals, 2, usage);
1316
+ const reason = requiredOption(parsed.options, "--reason");
1317
+ const now = clock(options);
1318
+ const request = store.transaction((tx) => {
1319
+ const task = requireTask(tx, parsed.positionals[0]);
1320
+ assertTaskOpen(task);
1321
+ const actor = taskActor(tx, options, task.id);
1322
+ const role = requireRole(tx, task.id, parsed.positionals[1]);
1323
+ if (actor === "leader" && role.name === LEADER_ROLE) {
1324
+ throw usageError("A Leader cannot stop the Session executing its own current command.", usage);
1325
+ }
1326
+ if (tx.getActiveAgentRun(task.id, role.name) !== null) {
1327
+ throw usageError(`Task Role has an active Run; settle or retire it before stopping the Session: ${task.id}/${role.name}.`, usage);
1328
+ }
1329
+ const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
1330
+ const session = sessions?.sessions[sessions.activeAgentId];
1331
+ if (session === undefined || session.status === "ended") {
1332
+ throw usageError(`Task Role has no active Session: ${task.id}/${role.name}.`, usage);
1333
+ }
1334
+ const lifecycle = tx.getWorkMailbox(runtimeLifecycleTarget({
1335
+ scope: "task",
1336
+ taskId: task.id,
1337
+ roleName: role.name
1338
+ }));
1339
+ if (lifecycle !== null && workMailboxHasWork(lifecycle)) {
1340
+ throw usageError(`Task Role Session lifecycle is busy: ${task.id}/${role.name}.`, usage);
1341
+ }
1342
+ recordTaskEvent(tx, task.id, "runtime.session-stop-requested", {
1343
+ roleName: role.name,
1344
+ agentId: session.agentId,
1345
+ adapterId: session.adapterId,
1346
+ nativeSessionId: session.nativeSessionId,
1347
+ launchId: session.launchId ?? "",
1348
+ reason,
1349
+ requestedBy: actor
1350
+ }, now);
1351
+ return {
1352
+ taskId: task.id,
1353
+ roleName: role.name,
1354
+ agentId: session.agentId,
1355
+ adapterId: session.adapterId,
1356
+ nativeSessionId: session.nativeSessionId,
1357
+ ...(session.launchId === undefined ? {} : { launchId: session.launchId }),
1358
+ sessionUpdatedAt: session.updatedAt
1359
+ };
1360
+ });
1361
+ return {
1362
+ kind: "session-stop",
1363
+ ...request,
1364
+ reason,
1365
+ output: `Stopped Session ${request.taskId}/${request.roleName}: ${reason}\n`
1366
+ };
1367
+ }
1368
+ throw usageError(command === undefined
1369
+ ? "Task Role Session command is required."
1370
+ : `Unknown command: task role session ${command}`);
1371
+ }
1289
1372
  function addTaskRole(args, store, options) {
1290
1373
  const usage = "Task role add usage: yui task role add <task> <name> [Role and Agent settings].";
1291
1374
  const [taskId, roleName, ...tail] = args;
@@ -1483,7 +1566,7 @@ function removeTaskRole(args, store, options) {
1483
1566
  throw usageError(`Task Role has an active Run and cannot be removed: ${task.id}/${role.name}.`);
1484
1567
  }
1485
1568
  const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
1486
- if (Object.values(sessions?.sessions ?? {}).some(({ status }) => status !== "stopped")) {
1569
+ if (Object.values(sessions?.sessions ?? {}).some(({ status }) => status === "active")) {
1487
1570
  throw usageError(`Task Role has a running native Agent and cannot be removed: ${task.id}/${role.name}.`);
1488
1571
  }
1489
1572
  if (!tx.removeTaskRole(task.id, role.name))
@@ -1531,7 +1614,7 @@ function bindTaskRole(args, store, options) {
1531
1614
  return switchActiveRoleAgent(bound, existing, agent.id, {
1532
1615
  activeRun: tx.getActiveAgentRun(task.id, role.name) !== null,
1533
1616
  nativeProcessRunning: currentSession !== undefined
1534
- && currentSession.status !== "stopped"
1617
+ && currentSession.status === "active"
1535
1618
  }, now);
1536
1619
  }
1537
1620
  catch (error) {
@@ -1585,7 +1668,7 @@ function viewTaskRole(args, store) {
1585
1668
  }
1586
1669
  const role = requireRole(store, task.id, args[1]);
1587
1670
  const session = store.getRoleSession(task.id, role.name);
1588
- if (session === null || session.status === "stopped" || session.status === "broken") {
1671
+ if (session === null || session.status === "ended") {
1589
1672
  throw usageError(`Task Role has no live Provider view: ${task.id}/${role.name}.`);
1590
1673
  }
1591
1674
  return {
@@ -1614,7 +1697,7 @@ function transferTaskRoleAuthority(args, store, options, action) {
1614
1697
  if (sessions === null || sessions === undefined || session === undefined
1615
1698
  || binding === null || binding === undefined
1616
1699
  || session.launchId === undefined
1617
- || session.status === "stopped" || session.status === "broken") {
1700
+ || session.status === "ended") {
1618
1701
  throw new Error(`Task Role has no live managed Provider: ${task.id}/${role.name}.`);
1619
1702
  }
1620
1703
  const activation = currentProviderActivation(binding);
@@ -3059,7 +3142,7 @@ function rebindTaskFinalReviewContract(args, store, options) {
3059
3142
  if (leaderSessions.inFlight !== null) {
3060
3143
  throw usageError("Task final-review contract cannot rebind while the Leader runtime has unsettled Run state.");
3061
3144
  }
3062
- const liveLeaderSession = Object.values(leaderSessions.sessions).find(({ status }) => status !== "stopped" && status !== "broken");
3145
+ const liveLeaderSession = Object.values(leaderSessions.sessions).find(({ status }) => status === "active");
3063
3146
  if (liveLeaderSession !== undefined) {
3064
3147
  throw usageError(`Task final-review contract cannot rebind while Leader Session ${liveLeaderSession.agentId} is ${liveLeaderSession.status}.`);
3065
3148
  }
@@ -7162,8 +7245,8 @@ function settleTaskExecutionForCompletion(store, taskId, roles, summary, now) {
7162
7245
  sessions = terminalizeTaskRoleRunSession(sessions, sessions.inFlight, now);
7163
7246
  }
7164
7247
  const current = sessions.sessions[sessions.activeAgentId];
7165
- if (current !== undefined && current.status !== "stopped" && current.status !== "broken") {
7166
- sessions = updateRoleAgentSessionStatus(sessions, sessions.activeAgentId, "stopped", now);
7248
+ if (current !== undefined && current.status === "active") {
7249
+ sessions = updateRoleAgentSessionStatus(sessions, sessions.activeAgentId, "ended", now, "stopped");
7167
7250
  }
7168
7251
  store.saveTaskRoleSessionSet(sessions);
7169
7252
  }
@@ -7180,14 +7263,14 @@ function settleTaskExecutionForCompletion(store, taskId, roles, summary, now) {
7180
7263
  return targets;
7181
7264
  }
7182
7265
  function ownedRuntimeSessionRequiresCleanup(session) {
7183
- if (session === undefined || session.status === "stopped" || session.status === "broken") {
7266
+ if (session === undefined || session.status === "ended") {
7184
7267
  return false;
7185
7268
  }
7186
7269
  // Older synthetic session fixtures can represent a ready native session
7187
7270
  // without a lifecycle generation. Do not manufacture a cleanup obligation
7188
7271
  // for those records; coordinated launches always carry launchId, while a
7189
7272
  // running session remains actionable even when an older record lacks it.
7190
- return session.status === "running" || session.launchId !== undefined;
7273
+ return session.launchId !== undefined;
7191
7274
  }
7192
7275
  function roleMailbox(taskId, roleName) {
7193
7276
  return { kind: "role", taskId, roleName };
@@ -253,7 +253,7 @@ function calculateHealth(role, activeRun, lastRun, nativeSession, runtimeCleanup
253
253
  healthReason: "the native Session is unbound; verified runtime cleanup is pending"
254
254
  };
255
255
  }
256
- if (nativeSession?.status === "broken") {
256
+ if (nativeSession?.status === "ended" && nativeSession.endReason === "failed") {
257
257
  // Issue 09: a broken Session only fails a live Run. When the last Run
258
258
  // already yielded, the Session death is a lifecycle event, not a Run
259
259
  // failure — surface it as attention with the persisted Run outcome.
@@ -329,10 +329,10 @@ function calculateHealth(role, activeRun, lastRun, nativeSession, runtimeCleanup
329
329
  }
330
330
  }
331
331
  }
332
- if (nativeSession?.status === "running" && tmux.state !== "running") {
332
+ if (nativeSession?.status === "active" && tmux.state !== "running") {
333
333
  return { health: "needs-attention", healthReason: "the native session is running without a live tmux pane" };
334
334
  }
335
- if (nativeSession?.status === "stopped" && tmux.state === "running") {
335
+ if (nativeSession?.status === "ended" && tmux.state === "running") {
336
336
  return { health: "needs-attention", healthReason: "a stopped native session has a live tmux pane" };
337
337
  }
338
338
  if (role.name === "leader" && openInputRequestCount > 0) {
@@ -1,6 +1,5 @@
1
- import { supportedAgentAdapterIds } from "../agent/adapterCatalog.js";
2
1
  import { MAX_RUN_CAP } from "../telemetry/telemetryConfig.js";
3
- import { MAX_CONTEXT_BUDGET_TOKENS, MAX_PROVIDER_RETRY_ATTEMPTS, MIN_CONTEXT_BUDGET_TOKENS } from "./yuiConfig.js";
2
+ import { MAX_CONTEXT_BUDGET_TOKENS, MIN_CONTEXT_BUDGET_TOKENS } from "./yuiConfig.js";
4
3
  export const CONFIG_DOMAINS = ["system", "runtime", "workflow", "resources", "tools"];
5
4
  /**
6
5
  * Public durable configuration contract. CLI help, completion, config show,
@@ -16,10 +15,6 @@ export const CONFIG_DEFINITIONS = Object.freeze([
16
15
  { key: "runtime-health", domain: "runtime", property: "runtimeHealth", label: "Runtime health thresholds", summary: "Quiet, checkpoint-overdue, and read-only diagnostic thresholds in seconds; values must be strictly increasing (defaults: 300/900/1800).", takesEffect: "CLI and Web projections immediately; scheduler diagnostics after the Controller restarts." },
17
16
  { key: "agent-launch-inactivity-timeout-seconds", domain: "runtime", property: "agentLaunchInactivityTimeoutSeconds", label: "Agent launch inactivity timeout", summary: "Maximum launch silence before startup fails, 15-3600 seconds (default: 300).", takesEffect: "After the Controller restarts." },
18
17
  { key: "delivery-timeout-seconds", domain: "runtime", property: "deliveryTimeoutSeconds", label: "Delivery timeout", summary: "Total control-plane delivery retry budget, 5-600 seconds (default: 120).", takesEffect: "After the Controller restarts; internal retry cadence remains automatic." },
19
- { key: "provider-retry-mode", domain: "runtime", property: "providerRetryMode", label: "Provider retry mode", summary: "Provider retry mode: off, shadow, or enforce (default: enforce).", takesEffect: "The next eligible provider-failure decision." },
20
- { key: "provider-retry-adapters", domain: "runtime", property: "providerRetryAdapters", label: "Provider retry adapters", summary: `Adapters with in-place retry: all, off, or a comma-separated subset of ${supportedAgentAdapterIds().join(", ")} (default: all).`, takesEffect: "The next eligible provider-failure decision." },
21
- { key: "provider-retry-delays-seconds", domain: "runtime", property: "providerRetryDelaysSeconds", label: "Provider retry delays", summary: `Ordered comma-separated retry delays of 1-600 seconds, with 1-${MAX_PROVIDER_RETRY_ATTEMPTS} attempts (default: 2,5,15).`, takesEffect: "The next eligible provider-failure decision; attempt count is the list length." },
22
- { key: "provider-retry-max-window-seconds", domain: "runtime", property: "providerRetryMaxWindowSeconds", label: "Provider retry max window", summary: "Positive-integer total retry budget per Run lineage in seconds (default: 600).", takesEffect: "The next retry episode; an active episode keeps its existing deadline." },
23
18
  { key: "leader-next-action", domain: "workflow", property: "leaderNextActionMode", label: "Leader next-action mode", summary: "Leader next-action mode: display, warn, or enforce (default: display).", takesEffect: "The next Leader next-action projection or gate." },
24
19
  { key: "leader-semantic-budget-turns", domain: "workflow", property: "leaderSemanticBudgetTurns", label: "Leader semantic budget", summary: "Consecutive yielded Leader turns without durable delivery progress before warning, 1-20 (default: 3).", takesEffect: "The next Leader delivery guard evaluation." },
25
20
  { key: "context-budget", domain: "workflow", property: "contextBudget", label: "Legacy context budget (inactive)", summary: `Compatibility-only soft and hard values in ${MIN_CONTEXT_BUDGET_TOKENS}-${MAX_CONTEXT_BUDGET_TOKENS}; they are parsed but never control Session or workflow behavior.`, takesEffect: "No runtime effect; Session Token metrics are read-only observations." },