@zq-silk/yui 0.13.7 → 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 (78) hide show
  1. package/ARCHITECTURE.md +40 -4
  2. package/README.md +23 -4
  3. package/dist/cli/commandCatalog.js +21 -2
  4. package/dist/cli/updatePorts.js +4 -4
  5. package/dist/cli.js +43 -14
  6. package/dist/commands/agentCommands.js +1 -1
  7. package/dist/commands/configCommands.js +1 -86
  8. package/dist/commands/executionAuditCommands.js +17 -16
  9. package/dist/commands/globalRoleCommands.js +4 -4
  10. package/dist/commands/sessionCommands.js +2 -6
  11. package/dist/commands/taskActor.js +1 -2
  12. package/dist/commands/taskCommands.js +93 -21
  13. package/dist/commands/taskContextCommand.js +1 -1
  14. package/dist/commands/taskExecutionCommands.js +0 -5
  15. package/dist/commands/taskOverviewCommand.js +5 -1
  16. package/dist/commands/taskRoleRuntimeStatus.js +3 -16
  17. package/dist/config/configCatalog.js +1 -6
  18. package/dist/config/yuiConfig.js +0 -79
  19. package/dist/context/sessionBootstrapManifest.js +26 -23
  20. package/dist/controller/clientRuntime.js +40 -3
  21. package/dist/controller/controller.js +29 -54
  22. package/dist/controller/fileSchedulerStoreAdapter.js +372 -1117
  23. package/dist/controller/runtime.js +12 -5
  24. package/dist/controller/runtimeHookRunFence.js +3 -9
  25. package/dist/controller/runtimeLaunchCoordinator.js +44 -67
  26. package/dist/controller/structuredProviderObservation.js +18 -5
  27. package/dist/coordination/workMailbox.js +4 -4
  28. package/dist/execution/executionHealth.js +1 -1
  29. package/dist/executor/agentExecutor.js +103 -93
  30. package/dist/executor/executorRegistry.js +8 -20
  31. package/dist/executor/fileRoleLaunchPlanner.js +26 -93
  32. package/dist/executor/turnCompletion.js +5 -5
  33. package/dist/lifecycle/exactRunTerminalization.js +2 -4
  34. package/dist/observability/executionAudit.js +40 -94
  35. package/dist/operator/operatorSessionHistory.js +7 -5
  36. package/dist/output/rolePresentation.js +0 -1
  37. package/dist/repository/taskWorkspacePreparer.js +0 -1
  38. package/dist/role/role.js +13 -21
  39. package/dist/run/agentRun.js +4 -54
  40. package/dist/runtime/agentDriver.js +2 -0
  41. package/dist/runtime/agentError.js +114 -0
  42. package/dist/runtime/agentHost.js +55 -82
  43. package/dist/runtime/builtinAgentDrivers.js +21 -9
  44. package/dist/runtime/builtinAgentErrorMappers.js +150 -0
  45. package/dist/runtime/exactControlPlane.js +6 -12
  46. package/dist/runtime/index.js +1 -2
  47. package/dist/runtime/launchBroker.js +5 -19
  48. package/dist/runtime/lifecycleReservation.js +20 -4
  49. package/dist/runtime/providerRuntimeIdentity.js +11 -19
  50. package/dist/runtime/runtimeBinding.js +0 -27
  51. package/dist/runtime/runtimeObservation.js +7 -16
  52. package/dist/runtime/runtimeSessionCandidate.js +3 -10
  53. package/dist/runtime/sessionLaunchRequest.js +1 -2
  54. package/dist/runtime/sessionReconciliation.js +2 -2
  55. package/dist/runtime/structuredProviderHost.js +44 -79
  56. package/dist/runtime/taskRuntimeIsolation.js +0 -7
  57. package/dist/runtime/tmuxAdapters.js +6 -49
  58. package/dist/scheduler/activeRoleRunDelivery.js +220 -178
  59. package/dist/scheduler/activeTaskProgress.js +1 -4
  60. package/dist/scheduler/leaderWakeupProcessor.js +123 -88
  61. package/dist/scheduler/roleRunLiveness.js +4 -1
  62. package/dist/scheduler/roleRunStall.js +10 -14
  63. package/dist/scheduler/wakeReason.js +4 -0
  64. package/dist/storage/migration/productionRegistry.js +475 -0
  65. package/dist/storage/sqliteSchema.js +54 -2
  66. package/dist/storage/sqliteStore.js +11 -44
  67. package/dist/storage/taskStore.js +8 -36
  68. package/dist/web/webSnapshot.js +3 -0
  69. package/i18n/README.zh-CN.md +11 -3
  70. package/package.json +1 -1
  71. package/skills/yui-leader/SKILL.md +30 -8
  72. package/skills/yui-operator/SKILL.md +14 -6
  73. package/skills/yui-runtime/SKILL.md +11 -8
  74. package/dist/lifecycle/providerErrorClass.js +0 -152
  75. package/dist/run/providerRetry.js +0 -226
  76. package/dist/run/providerRetryConfig.js +0 -27
  77. package/dist/runtime/providerErrorCodes.js +0 -278
  78. package/dist/runtime/providerRecoveryDecision.js +0 -55
@@ -19,7 +19,7 @@ import { renderRoleDetails } from "../output/rolePresentation.js";
19
19
  import { createTaskMessage, taskMessageAuthorLabel } from "../message/message.js";
20
20
  import { cancelInputRequest } from "../input/inputRequest.js";
21
21
  import { retireExactActiveAgentRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
22
- import { activeRoleAgentBinding, copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
22
+ import { activeRoleAgentBinding, copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole } from "../role/role.js";
23
23
  import { agentRunDeliveryReceiptId, createAgentRun, failAgentRun, withAgentRunContextSnapshot, yieldAgentRun } from "../run/agentRun.js";
24
24
  import { matchYieldReceipt } from "../run/yieldReceipt.js";
25
25
  import { createRejectedYieldAttempt, rejectedYieldAttemptEventPayload, rejectedYieldAttemptFromTaskEvent, RUN_YIELD_REJECTED_EVENT } from "../run/rejectedYieldAttempt.js";
@@ -987,11 +987,6 @@ function retireTaskCommand(args, store, options) {
987
987
  tx.saveInputRequest(task.id, cancelInputRequest(request, `Task retired: ${summary}`, now));
988
988
  }
989
989
  }
990
- for (const role of tx.listRoles(task.id)) {
991
- if (role.status !== "idle") {
992
- tx.saveRole(task.id, updateRoleStatus(role, "idle", now));
993
- }
994
- }
995
990
  for (const mailbox of tx.listWorkMailboxes()) {
996
991
  if ((mailbox.target.kind === "task"
997
992
  || mailbox.target.kind === "role")
@@ -1281,6 +1276,8 @@ function taskRoleCommand(args, store, options) {
1281
1276
  return output(bindTaskRole(rest, store, options));
1282
1277
  if (command === "unbind")
1283
1278
  return output(unbindTaskRole(rest, store, options));
1279
+ if (command === "session")
1280
+ return taskRoleSessionCommand(rest, store, options);
1284
1281
  if (command === "view")
1285
1282
  return viewTaskRole(rest, store);
1286
1283
  if (command === "takeover")
@@ -1291,6 +1288,87 @@ function taskRoleCommand(args, store, options) {
1291
1288
  ? "Task role command is required."
1292
1289
  : `Unknown command: task role ${command}`);
1293
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
+ }
1294
1372
  function addTaskRole(args, store, options) {
1295
1373
  const usage = "Task role add usage: yui task role add <task> <name> [Role and Agent settings].";
1296
1374
  const [taskId, roleName, ...tail] = args;
@@ -1488,7 +1566,7 @@ function removeTaskRole(args, store, options) {
1488
1566
  throw usageError(`Task Role has an active Run and cannot be removed: ${task.id}/${role.name}.`);
1489
1567
  }
1490
1568
  const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
1491
- if (Object.values(sessions?.sessions ?? {}).some(({ status }) => status !== "stopped")) {
1569
+ if (Object.values(sessions?.sessions ?? {}).some(({ status }) => status === "active")) {
1492
1570
  throw usageError(`Task Role has a running native Agent and cannot be removed: ${task.id}/${role.name}.`);
1493
1571
  }
1494
1572
  if (!tx.removeTaskRole(task.id, role.name))
@@ -1536,7 +1614,7 @@ function bindTaskRole(args, store, options) {
1536
1614
  return switchActiveRoleAgent(bound, existing, agent.id, {
1537
1615
  activeRun: tx.getActiveAgentRun(task.id, role.name) !== null,
1538
1616
  nativeProcessRunning: currentSession !== undefined
1539
- && currentSession.status !== "stopped"
1617
+ && currentSession.status === "active"
1540
1618
  }, now);
1541
1619
  }
1542
1620
  catch (error) {
@@ -1590,7 +1668,7 @@ function viewTaskRole(args, store) {
1590
1668
  }
1591
1669
  const role = requireRole(store, task.id, args[1]);
1592
1670
  const session = store.getRoleSession(task.id, role.name);
1593
- if (session === null || session.status === "stopped" || session.status === "broken") {
1671
+ if (session === null || session.status === "ended") {
1594
1672
  throw usageError(`Task Role has no live Provider view: ${task.id}/${role.name}.`);
1595
1673
  }
1596
1674
  return {
@@ -1619,7 +1697,7 @@ function transferTaskRoleAuthority(args, store, options, action) {
1619
1697
  if (sessions === null || sessions === undefined || session === undefined
1620
1698
  || binding === null || binding === undefined
1621
1699
  || session.launchId === undefined
1622
- || session.status === "stopped" || session.status === "broken") {
1700
+ || session.status === "ended") {
1623
1701
  throw new Error(`Task Role has no live managed Provider: ${task.id}/${role.name}.`);
1624
1702
  }
1625
1703
  const activation = currentProviderActivation(binding);
@@ -2374,7 +2452,6 @@ function dispatchWork(args, store, options) {
2374
2452
  const role = requireRole(tx, task.id, unboundRun.roleName);
2375
2453
  tx.saveAgentRun(runWithLineage);
2376
2454
  tx.saveActiveAgentRun(runWithLineage);
2377
- tx.saveRole(task.id, updateRoleStatus(role, "running", now));
2378
2455
  enqueueWork(tx, roleMailbox(task.id, role.name), "run-dispatched", now, [
2379
2456
  runRef(task.id, runWithLineage.id),
2380
2457
  workItemRef(task.id, item.id)
@@ -3065,7 +3142,7 @@ function rebindTaskFinalReviewContract(args, store, options) {
3065
3142
  if (leaderSessions.inFlight !== null) {
3066
3143
  throw usageError("Task final-review contract cannot rebind while the Leader runtime has unsettled Run state.");
3067
3144
  }
3068
- const liveLeaderSession = Object.values(leaderSessions.sessions).find(({ status }) => status !== "stopped" && status !== "broken");
3145
+ const liveLeaderSession = Object.values(leaderSessions.sessions).find(({ status }) => status === "active");
3069
3146
  if (liveLeaderSession !== undefined) {
3070
3147
  throw usageError(`Task final-review contract cannot rebind while Leader Session ${liveLeaderSession.agentId} is ${liveLeaderSession.status}.`);
3071
3148
  }
@@ -4499,7 +4576,6 @@ function retryRun(args, store, options) {
4499
4576
  });
4500
4577
  tx.saveAgentRun(created);
4501
4578
  tx.saveActiveAgentRun(created);
4502
- tx.saveRole(task.id, updateRoleStatus(role, "running", now));
4503
4579
  if (previous.workItemId !== undefined && retriedItemWithGroup !== null) {
4504
4580
  const item = retriedItemWithGroup;
4505
4581
  const workspace = tx.getWorkItemWorkspace(task.id, item.id);
@@ -6072,7 +6148,6 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
6072
6148
  const laneReviewer = requireRole(tx, taskId, unboundRun.roleName);
6073
6149
  tx.saveAgentRun(created);
6074
6150
  tx.saveActiveAgentRun(created);
6075
- tx.saveRole(taskId, updateRoleStatus(laneReviewer, "running", now));
6076
6151
  enqueueWork(tx, roleMailbox(taskId, laneReviewer.name), "review-requested", now, [
6077
6152
  runRef(taskId, created.id),
6078
6153
  ...(item === undefined ? [] : [workItemRef(taskId, item.id)])
@@ -7170,14 +7245,11 @@ function settleTaskExecutionForCompletion(store, taskId, roles, summary, now) {
7170
7245
  sessions = terminalizeTaskRoleRunSession(sessions, sessions.inFlight, now);
7171
7246
  }
7172
7247
  const current = sessions.sessions[sessions.activeAgentId];
7173
- if (current !== undefined && current.status !== "stopped" && current.status !== "broken") {
7174
- sessions = updateRoleAgentSessionStatus(sessions, sessions.activeAgentId, "stopped", now);
7248
+ if (current !== undefined && current.status === "active") {
7249
+ sessions = updateRoleAgentSessionStatus(sessions, sessions.activeAgentId, "ended", now, "stopped");
7175
7250
  }
7176
7251
  store.saveTaskRoleSessionSet(sessions);
7177
7252
  }
7178
- if (role.status !== "idle") {
7179
- store.saveRole(taskId, updateRoleStatus(role, "idle", now));
7180
- }
7181
7253
  if (!cleanupRequired)
7182
7254
  continue;
7183
7255
  const target = runtimeLifecycleTarget({
@@ -7191,14 +7263,14 @@ function settleTaskExecutionForCompletion(store, taskId, roles, summary, now) {
7191
7263
  return targets;
7192
7264
  }
7193
7265
  function ownedRuntimeSessionRequiresCleanup(session) {
7194
- if (session === undefined || session.status === "stopped" || session.status === "broken") {
7266
+ if (session === undefined || session.status === "ended") {
7195
7267
  return false;
7196
7268
  }
7197
7269
  // Older synthetic session fixtures can represent a ready native session
7198
7270
  // without a lifecycle generation. Do not manufacture a cleanup obligation
7199
7271
  // for those records; coordinated launches always carry launchId, while a
7200
7272
  // running session remains actionable even when an older record lacks it.
7201
- return session.status === "running" || session.launchId !== undefined;
7273
+ return session.launchId !== undefined;
7202
7274
  }
7203
7275
  function roleMailbox(taskId, roleName) {
7204
7276
  return { kind: "role", taskId, roleName };
@@ -249,7 +249,7 @@ export function runTaskContextCommand(args, store, currentTaskReviewCandidate =
249
249
  const effectiveSource = activeRun === undefined ? "Session" : "Run";
250
250
  const creation = [...events].reverse().find((event) => (event.type === "role.added" && event.payload.role === role.name));
251
251
  return [
252
- ` ${role.name} [${role.status}]: ${role.activeAgentId}/${binding.adapterId}`,
252
+ ` ${role.name} [${activeRun === undefined ? "idle" : "running"}]: ${role.activeAgentId}/${binding.adapterId}`,
253
253
  ` Desired: r${role.launchRevision}; Profile intent: ${role.defaultAccess}; Model: ${binding.config.model ?? "default"}; effort: ${binding.config.effort ?? "default"}; permission: ${binding.config.permission.strategy}`,
254
254
  ` Effective: ${effective === undefined
255
255
  ? "not started"
@@ -4,7 +4,6 @@ import { recordExecutionLaneResult } from "../execution/executionGroup.js";
4
4
  import { usageError } from "../errors/cliError.js";
5
5
  import { requestDurableJobCancel } from "../job/durableJob.js";
6
6
  import { finishReviewRound, updateReviewExecutionGroup } from "../review/reviewRound.js";
7
- import { updateRoleStatus } from "../role/role.js";
8
7
  import { failAgentRun } from "../run/agentRun.js";
9
8
  import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
10
9
  import { startTaskExecution, stopTaskExecution } from "../task/task.js";
@@ -83,10 +82,6 @@ export function stopTaskExecutionCommand(request, store, options = {}) {
83
82
  if (sessions !== null && sessions.inFlight !== null) {
84
83
  tx.saveTaskRoleSessionSet(terminalizeCurrentRun(sessions, now));
85
84
  }
86
- const role = tx.getRole(task.id, roleName);
87
- if (role !== null && role.status !== "idle") {
88
- tx.saveRole(task.id, updateRoleStatus(role, "idle", now));
89
- }
90
85
  }
91
86
  // A stop is allowed to discard stale pointer projections even when their
92
87
  // historical Runs are already terminal.
@@ -78,7 +78,11 @@ function buildTaskOverviewEntry(task, store, now, runtimeHealthPolicy) {
78
78
  const legacyNext = deriveNextAction(task, brief, workItems, openInputRequests, blockers, pendingWakeup);
79
79
  const leader = {
80
80
  role: "leader",
81
- roleStatus: leaderRole?.status ?? "missing",
81
+ roleStatus: leaderRole === null
82
+ ? "missing"
83
+ : agentRuns.some((run) => run.roleName === "leader" && run.status === "active")
84
+ ? "running"
85
+ : roleSessions.find((session) => session.roleName === "leader")?.status ?? "idle",
82
86
  summary: brief?.leaderSummary ?? null,
83
87
  currentFocus: brief?.currentFocus ?? null,
84
88
  updatedAt: brief?.updatedAt ?? null,
@@ -85,7 +85,6 @@ export function renderTaskRoleRuntimeStatus(status) {
85
85
  ? "-"
86
86
  : status.launchDrift ? "pending next launch" : "none"}`,
87
87
  ` Run/session ${status.runSessionDrift ? "snapshot mismatch" : "snapshot consistent"}`,
88
- ` Role state ${status.role.status}`,
89
88
  ` Active work ${activeWork}`,
90
89
  ` Active run ${activeRun}`,
91
90
  ...(lastRun === undefined ? [] : [` Last run ${lastRun}`]),
@@ -254,10 +253,7 @@ function calculateHealth(role, activeRun, lastRun, nativeSession, runtimeCleanup
254
253
  healthReason: "the native Session is unbound; verified runtime cleanup is pending"
255
254
  };
256
255
  }
257
- if (role.status === "failed" || role.status === "exited") {
258
- return { health: "failed", healthReason: `persisted Role state is ${role.status}` };
259
- }
260
- if (nativeSession?.status === "broken") {
256
+ if (nativeSession?.status === "ended" && nativeSession.endReason === "failed") {
261
257
  // Issue 09: a broken Session only fails a live Run. When the last Run
262
258
  // already yielded, the Session death is a lifecycle event, not a Run
263
259
  // failure — surface it as attention with the persisted Run outcome.
@@ -287,12 +283,6 @@ function calculateHealth(role, activeRun, lastRun, nativeSession, runtimeCleanup
287
283
  };
288
284
  }
289
285
  if (activeRun !== null) {
290
- if (role.status !== "running") {
291
- return {
292
- health: "needs-attention",
293
- healthReason: `the active Run conflicts with persisted Role state ${role.status}`
294
- };
295
- }
296
286
  if (activeRun.deliveredAt !== undefined && tmux.state !== "running") {
297
287
  return { health: "needs-attention", healthReason: "the delivered active Run has no live tmux pane" };
298
288
  }
@@ -339,13 +329,10 @@ function calculateHealth(role, activeRun, lastRun, nativeSession, runtimeCleanup
339
329
  }
340
330
  }
341
331
  }
342
- if (activeRun === null && role.status === "running") {
343
- return { health: "needs-attention", healthReason: "the Role is running without an active Run" };
344
- }
345
- if (nativeSession?.status === "running" && tmux.state !== "running") {
332
+ if (nativeSession?.status === "active" && tmux.state !== "running") {
346
333
  return { health: "needs-attention", healthReason: "the native session is running without a live tmux pane" };
347
334
  }
348
- if (nativeSession?.status === "stopped" && tmux.state === "running") {
335
+ if (nativeSession?.status === "ended" && tmux.state === "running") {
349
336
  return { health: "needs-attention", healthReason: "a stopped native session has a live tmux pane" };
350
337
  }
351
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." },
@@ -1,4 +1,3 @@
1
- import { supportedAgentAdapterIds } from "../agent/adapterCatalog.js";
2
1
  import { DEFAULT_RUN_CAP, DEFAULT_TERMINAL_KEEP, MAX_RUN_CAP } from "../telemetry/telemetryConfig.js";
3
2
  import { DEFAULT_RUNTIME_HEALTH_POLICY } from "../runtime/runtimeHealthPolicy.js";
4
3
  export const DEFAULT_RECONCILIATION_INTERVAL_SECONDS = 120;
@@ -75,84 +74,6 @@ export function resolveLeaderNextActionMode(value) {
75
74
  }
76
75
  return normalized;
77
76
  }
78
- // ── Issue 01: Provider retry ──────────────────────────────────────────────
79
- export const PROVIDER_RETRY_MODES = ["off", "shadow", "enforce"];
80
- export const DEFAULT_PROVIDER_RETRY_MODE = "enforce";
81
- export const DEFAULT_PROVIDER_RETRY_DELAYS_SECONDS = Object.freeze([2, 5, 15]);
82
- export const DEFAULT_PROVIDER_RETRY_MAX_WINDOW_SECONDS = 600;
83
- export const MAX_PROVIDER_RETRY_ATTEMPTS = 10;
84
- export function resolveProviderRetryMode(value) {
85
- if (value === undefined || value === null)
86
- return DEFAULT_PROVIDER_RETRY_MODE;
87
- if (typeof value !== "string") {
88
- throw new TypeError("providerRetryMode must be off, shadow, or enforce.");
89
- }
90
- const normalized = value.trim().toLowerCase();
91
- if (normalized.length === 0)
92
- return DEFAULT_PROVIDER_RETRY_MODE;
93
- if (!PROVIDER_RETRY_MODES.includes(normalized)) {
94
- throw new TypeError("providerRetryMode must be off, shadow, or enforce.");
95
- }
96
- return normalized;
97
- }
98
- /**
99
- * Resolves the adapter list. `["all"]` or undefined means every supported
100
- * adapter; an empty array disables in-place retry.
101
- */
102
- export function resolveProviderRetryAdapters(value) {
103
- if (value === undefined || value === null) {
104
- return [...supportedAgentAdapterIds()];
105
- }
106
- if (!Array.isArray(value)) {
107
- throw new TypeError("providerRetryAdapters must be an array of adapter ids.");
108
- }
109
- const supported = new Set(supportedAgentAdapterIds());
110
- const adapters = [];
111
- for (const raw of value) {
112
- if (typeof raw !== "string") {
113
- throw new TypeError("providerRetryAdapters entries must be strings.");
114
- }
115
- const token = raw.trim().toLowerCase();
116
- if (token === "all") {
117
- for (const adapter of supportedAgentAdapterIds()) {
118
- if (!adapters.includes(adapter))
119
- adapters.push(adapter);
120
- }
121
- continue;
122
- }
123
- if (!/^[a-z0-9][a-z0-9._-]*$/u.test(token)) {
124
- throw new TypeError(`Invalid Provider retry adapter: ${token}.`);
125
- }
126
- if (!supported.has(token)) {
127
- throw new TypeError(`Unknown Provider retry adapter: ${token}.`);
128
- }
129
- if (!adapters.includes(token))
130
- adapters.push(token);
131
- }
132
- return adapters;
133
- }
134
- export function resolveProviderRetryDelaysSeconds(value) {
135
- if (value === undefined || value === null)
136
- return [...DEFAULT_PROVIDER_RETRY_DELAYS_SECONDS];
137
- if (!Array.isArray(value) || value.length < 1 || value.length > MAX_PROVIDER_RETRY_ATTEMPTS) {
138
- throw new TypeError(`providerRetryDelaysSeconds must contain 1-${MAX_PROVIDER_RETRY_ATTEMPTS} positive integers.`);
139
- }
140
- const delays = value.map((entry) => {
141
- if (typeof entry !== "number" || !Number.isSafeInteger(entry) || entry < 1 || entry > 600) {
142
- throw new TypeError("providerRetryDelaysSeconds entries must be integers from 1 to 600.");
143
- }
144
- return entry;
145
- });
146
- for (let index = 1; index < delays.length; index += 1) {
147
- if (delays[index] < delays[index - 1]) {
148
- throw new TypeError("providerRetryDelaysSeconds must be ordered from shortest to longest.");
149
- }
150
- }
151
- return delays;
152
- }
153
- export function resolveProviderRetryMaxWindowSeconds(value) {
154
- return resolveBoundedPositiveInteger(value, DEFAULT_PROVIDER_RETRY_MAX_WINDOW_SECONDS, 1, Number.MAX_SAFE_INTEGER, "providerRetryMaxWindowSeconds");
155
- }
156
77
  // ── Executable paths ──────────────────────────────────────────────────────
157
78
  export function resolveTmuxBin(value) {
158
79
  if (value === undefined || value === null)
@@ -1,15 +1,18 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { chmodSync, existsSync, readFileSync, readdirSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
- import { exactControlPlaneDigest, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
4
+ import { exactControlPlaneCommandPrefix, exactControlPlaneDigest, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
5
5
  import { writeTextFileAtomically } from "../storage/durableFile.js";
6
6
  import { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
7
7
  export { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
8
- const ORDINARY_SESSION_CLI = [
9
- "#!/bin/sh",
10
- "exec yui \"$@\"",
11
- ""
12
- ].join("\n");
8
+ const ordinarySessionCli = ["#!/bin/sh", "exec yui \"$@\"", ""].join("\n");
9
+ function renderSessionCli(controlPlane) {
10
+ return [
11
+ "#!/bin/sh",
12
+ `exec ${exactControlPlaneCommandPrefix(controlPlane)} \"$@\"`,
13
+ ""
14
+ ].join("\n");
15
+ }
13
16
  /** Read back one immutable Session Manifest and verify its content digest. */
14
17
  export function readSessionBootstrapManifest(path) {
15
18
  const source = resolve(path);
@@ -84,10 +87,11 @@ export function materializeSessionBootstrap(input) {
84
87
  const controlDigest = exactControlPlaneDigest(input.controlPlane);
85
88
  const descriptorPath = resolve(join(home, "runtime", "control-plane", `${controlDigest}.json`));
86
89
  writeImmutableText(descriptorPath, `${serializeExactDescriptor(input.controlPlane)}\n`);
87
- // Session identity is carried by the immutable Manifest and durable Role/
88
- // Run fences. Resolve the ordinary CLI on every invocation so package or
89
- // release upgrades do not invalidate a still-current native Session.
90
- const sessionCliContent = ORDINARY_SESSION_CLI;
90
+ // Provider command runners may rebuild PATH independently of the managed
91
+ // process environment. Keep the Session entry point deterministic; the
92
+ // existing continuity preflight accepts compatible package replacement at
93
+ // this path without treating package version as Session identity.
94
+ const sessionCliContent = renderSessionCli(input.controlPlane);
91
95
  const sessionCliDigest = digest(sessionCliContent);
92
96
  const sessionCliPath = resolve(join(home, "runtime", "session-cli", `yui-${sessionCliDigest}.sh`));
93
97
  writeImmutableText(sessionCliPath, sessionCliContent);
@@ -126,11 +130,11 @@ export function materializeSessionBootstrap(input) {
126
130
  roleProfileRef: { digest: profileDigest, path: roleProfilePath },
127
131
  contextProtocol: input.owner.scope === "global"
128
132
  ? {
129
- loadCommand: "yui session context \"$YUI_ROLE\" --json"
133
+ loadCommand: "\"$YUI_SESSION_CLI\" session context \"$YUI_ROLE\" --json"
130
134
  }
131
135
  : {
132
- loadCommand: "yui task run context \"$YUI_TASK_ID/<run-id>\" --json",
133
- expandCommand: "yui task run context expand \"$YUI_TASK_ID/<run-id>\" <ref-id> --store <store> --mode full --json"
136
+ loadCommand: "\"$YUI_SESSION_CLI\" task run context \"$YUI_TASK_ID/<run-id>\" --json",
137
+ expandCommand: "\"$YUI_SESSION_CLI\" task run context expand \"$YUI_TASK_ID/<run-id>\" <ref-id> --store <store> --mode full --json"
134
138
  }
135
139
  };
136
140
  const manifest = Object.freeze({ ...body, digest: digest(body) });
@@ -145,14 +149,13 @@ export function materializeSessionBootstrap(input) {
145
149
  });
146
150
  }
147
151
  /**
148
- * Converts wrappers produced before the protocol-compatible Session CLI to an
149
- * ordinary `yui` invocation. Only a valid Session Manifest may nominate a
150
- * wrapper, and only the exact legacy two-line wrapper shape is changed. The
151
- * Manifest and its frozen descriptor stay immutable and continue to
152
- * authenticate the Session; repeated refreshes are no-ops.
152
+ * Retargets known managed wrappers to the current resolved control plane after
153
+ * a compatible update. Only a valid Session Manifest may nominate a wrapper,
154
+ * and only Yui's ordinary or exact two-line wrapper shapes are changed.
153
155
  */
154
- export function refreshManagedSessionCliWrappers(homeInput) {
156
+ export function refreshManagedSessionCliWrappers(homeInput, controlPlane) {
155
157
  const home = resolve(homeInput);
158
+ const currentSessionCli = renderSessionCli(controlPlane);
156
159
  const manifestDirectory = resolve(join(home, "runtime", "session-manifests"));
157
160
  const sessionCliDirectory = resolve(join(home, "runtime", "session-cli"));
158
161
  if (!existsSync(manifestDirectory)) {
@@ -189,21 +192,21 @@ export function refreshManagedSessionCliWrappers(homeInput) {
189
192
  continue;
190
193
  }
191
194
  const content = readFileSync(wrapperPath, "utf8");
192
- if (content === ORDINARY_SESSION_CLI) {
195
+ if (content === currentSessionCli) {
193
196
  current += 1;
194
197
  continue;
195
198
  }
196
- if (!isLegacyExactSessionCli(content)) {
199
+ if (content !== ordinarySessionCli && !isExactSessionCli(content)) {
197
200
  skipped += 1;
198
201
  continue;
199
202
  }
200
- writeTextFileAtomically(wrapperPath, ORDINARY_SESSION_CLI);
203
+ writeTextFileAtomically(wrapperPath, currentSessionCli);
201
204
  chmodSync(wrapperPath, 0o700);
202
205
  refreshed += 1;
203
206
  }
204
207
  return Object.freeze({ refreshed, current, skipped });
205
208
  }
206
- function isLegacyExactSessionCli(content) {
209
+ function isExactSessionCli(content) {
207
210
  return /^#!\/bin\/sh\nexec [^\n]+ '--yui-control' '[a-f0-9]{64}' "\$@"\n$/u.test(content);
208
211
  }
209
212
  function writeImmutableText(path, content) {
@@ -466,11 +466,48 @@ export class FileTaskWorkflowRuntime {
466
466
  throw new Error(`Role runtime did not stop: ${target.taskId}/${target.roleName}.`);
467
467
  }
468
468
  const session = this.store.getRoleSession(target.taskId, target.roleName);
469
- if (session !== null && session.status !== "stopped") {
469
+ if (session !== null && session.status !== "ended") {
470
470
  throw new Error(`Role runtime session is still active: ${target.taskId}/${target.roleName}.`);
471
471
  }
472
472
  }
473
473
  }
474
+ /** Stops only the exact idle Session observed by an Agent command. */
475
+ async stopExactTaskRoleSession(input) {
476
+ if (this.store.getActiveAgentRun(input.taskId, input.roleName) !== null) {
477
+ throw new Error(`Role has an active Run: ${input.taskId}/${input.roleName}.`);
478
+ }
479
+ const owner = {
480
+ scope: "task",
481
+ taskId: input.taskId,
482
+ roleName: input.roleName
483
+ };
484
+ const target = this.schedulerStore.enqueueRuntimeCleanup(owner, new Date(), {
485
+ owner,
486
+ agentId: input.agentId,
487
+ adapterId: input.adapterId,
488
+ nativeSessionId: input.nativeSessionId,
489
+ ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
490
+ sessionUpdatedAt: input.sessionUpdatedAt
491
+ });
492
+ if (target === null) {
493
+ throw new Error(`Role Session changed before its exact stop was reserved: ${input.taskId}/${input.roleName}.`);
494
+ }
495
+ await callFileTaskController(this.home, "scheduler.scan", {}, {
496
+ ...this.clientOptions,
497
+ requestTimeoutMs: LIFECYCLE_REQUEST_TIMEOUT_MS
498
+ });
499
+ if (hasRuntimeLifecycleWork(this.store.getWorkMailbox(target))) {
500
+ throw new Error(`Role runtime did not stop: ${input.taskId}/${input.roleName}.`);
501
+ }
502
+ const session = this.store.getRoleSession(input.taskId, input.roleName);
503
+ if (session !== null
504
+ && session.status !== "ended"
505
+ && session.agentId === input.agentId
506
+ && session.adapterId === input.adapterId
507
+ && session.nativeSessionId === input.nativeSessionId) {
508
+ throw new Error(`Role runtime session is still active: ${input.taskId}/${input.roleName}.`);
509
+ }
510
+ }
474
511
  /** Wait until every cancellation requested by Task execution stop is physically settled. */
475
512
  async stopTaskDurableJobs(taskId) {
476
513
  const deadline = Date.now() + LIFECYCLE_REQUEST_TIMEOUT_MS;
@@ -506,7 +543,7 @@ export class FileTaskWorkflowRuntime {
506
543
  + activeJobs.map(({ id }) => id).join(", "));
507
544
  }
508
545
  const liveSessions = this.store.listRoleSessionSets(taskId).flatMap((sessions) => (Object.values(sessions.sessions)
509
- .filter((session) => session.status !== "stopped" && session.status !== "broken")
546
+ .filter((session) => session.status === "active")
510
547
  .map((session) => `${sessions.owner.roleName}/${session.agentId}/${session.status}`)));
511
548
  if (liveSessions.length > 0) {
512
549
  throw new WorkspaceCleanupBlockedError("physical-resource-live", `task:${taskId}`, true, `Task physical resources are not released: current Role Session state is still live: `
@@ -556,7 +593,7 @@ export class FileTaskWorkflowRuntime {
556
593
  }
557
594
  const sessions = this.store.getGlobalRoleSessionSet(roleName);
558
595
  const active = sessions?.sessions[sessions.activeAgentId];
559
- if (active !== undefined && active.status !== "stopped") {
596
+ if (active !== undefined && active.status !== "ended") {
560
597
  throw new Error(`Global Role runtime session is still active: ${roleName}.`);
561
598
  }
562
599
  }