@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
@@ -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)
@@ -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
  }
@@ -57,6 +57,11 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
57
57
  // event-loop turn. Give already-written requests a poll boundary before the
58
58
  // next durable phase; later phases retain their existing CAS fences.
59
59
  await controlEventLoopTurn();
60
+ // A dormant Session may still name a Host that disappeared after its last
61
+ // Provider Turn. Detach that disposable Host before claiming the next Run,
62
+ // so retained Agent intent restores the same Session instead of first
63
+ // connecting to a known-dead control socket.
64
+ await reconcileDormantRuntimeOwners(store, delivery, lifecycleHost, scope, now, blockedTaskIds);
60
65
  const failedCleanupRoles = await processSelectedRoleRuntimeCleanups(store, delivery, lifecycleHost, scope, now, runtimeCleanupOutcomes, blockedTaskIds);
61
66
  const roleSelection = selectionWithoutFailedCleanupRoles(store, selection, failedCleanupRoles);
62
67
  const availableWakeupSelection = () => (leaderWakeFence?.() === true
@@ -79,7 +84,6 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
79
84
  // Issue 04: reopen due in-place Provider retries on their original
80
85
  // Sessions before delivery, so the existing delivery path re-pushes the
81
86
  // exact same input in this same pass.
82
- resolveDueProviderRetries(store, roleSelection, now);
83
87
  const activeRunDeliveries = await processActiveRoleRunDeliveries(store, delivery, now, roleSelection, inputDeliveryRecoveryCutoff);
84
88
  await controlEventLoopTurn();
85
89
  const unsettledRunRefs = new Set(activeRunDeliveries.flatMap((result) => (result.reason === "delivery-uncertain" || result.terminalFailure !== undefined
@@ -253,10 +257,10 @@ function queueSelectedCompletedTaskRuntimeCleanups(store, selection, now) {
253
257
  }
254
258
  }
255
259
  function schedulerSessionRequiresRuntimeCleanup(session) {
256
- if (session === null || session.status === "stopped" || session.status === "broken") {
260
+ if (session === null || session.status === "ended") {
257
261
  return false;
258
262
  }
259
- return session.status === "running" || session.launchId !== undefined;
263
+ return session.launchId !== undefined;
260
264
  }
261
265
  async function processSelectedRoleRuntimeCleanups(store, delivery, lifecycleHost, scope, now, outcomes, blockedTaskIds = new Set()) {
262
266
  const targets = selectedRuntimeLifecycleTargets(store, scope, blockedTaskIds);
@@ -351,14 +355,18 @@ async function processSelectedRoleRuntimeCleanups(store, delivery, lifecycleHost
351
355
  return failedRoles;
352
356
  }
353
357
  async function reconcileDormantRuntimeOwners(store, delivery, lifecycleHost, scope, now, blockedTaskIds = new Set()) {
354
- if (scope.kind !== "full"
355
- || lifecycleHost === undefined
356
- || store.listDormantRuntimeOwners === undefined
357
- || store.markRuntimeOwnerStopped === undefined) {
358
+ if (lifecycleHost === undefined
359
+ || store.listDormantRuntimeOwners === undefined) {
358
360
  return;
359
361
  }
360
- const candidates = store.listDormantRuntimeOwners().filter((candidate) => (candidate.owner.scope !== "task"
361
- || !blockedTaskIds.has(candidate.owner.taskId)));
362
+ const selectedOwners = scope.kind === "full"
363
+ ? undefined
364
+ : new Set(selectedRuntimeLifecycleTargets(store, scope, blockedTaskIds)
365
+ .map((target) => runtimeOwnerIdentity(runtimeOwner(target))));
366
+ const candidates = store.listDormantRuntimeOwners().filter((candidate) => ((candidate.owner.scope !== "task"
367
+ || !blockedTaskIds.has(candidate.owner.taskId))
368
+ && (selectedOwners === undefined
369
+ || selectedOwners.has(runtimeOwnerIdentity(candidate.owner)))));
362
370
  if (candidates.length === 0)
363
371
  return;
364
372
  const owners = candidates.map((candidate) => candidate.owner);
@@ -391,17 +399,12 @@ async function reconcileDormantRuntimeOwners(store, delivery, lifecycleHost, sco
391
399
  for (const candidate of candidates) {
392
400
  if (byOwner.get(runtimeOwnerIdentity(candidate.owner))?.state
393
401
  === "stopped") {
394
- if (candidate.owner.scope === "task"
395
- && candidate.launchId !== undefined) {
402
+ if (candidate.launchId !== undefined) {
396
403
  // The exact launch-owned resources must settle through the durable
397
- // cleanup lane before its Session fact becomes stopped. The candidate
398
- // is passed back as the CAS fence so a concurrent Hook/launch cannot
399
- // redirect cleanup to a newer generation.
400
- store.enqueueRuntimeCleanup?.(candidate.owner, now, candidate);
401
- continue;
402
- }
403
- if (store.markRuntimeOwnerStopped(candidate, now)) {
404
- forgetPreparedRuntimeOwner(delivery, candidate.owner);
404
+ // cleanup lane before its Host fact is detached. The resumable Session
405
+ // remains active; the candidate is the CAS fence against a concurrent
406
+ // Hook or replacement launch.
407
+ store.enqueueRuntimeHostDetach?.(candidate.owner, now, candidate);
405
408
  }
406
409
  }
407
410
  }
@@ -419,20 +422,6 @@ function runtimeOwnerIdentity(owner) {
419
422
  ? `task\0${owner.taskId}\0${owner.roleName}`
420
423
  : `global\0${owner.roleName}`;
421
424
  }
422
- /**
423
- * Issue 04: reopens due in-place Provider retries on their original Native
424
- * Sessions before the active-run delivery pass, so the existing delivery
425
- * path re-pushes the exact same input in the same pass. A Run whose Session
426
- * is proven dead terminalizes with a replacement blocker instead.
427
- */
428
- function resolveDueProviderRetries(store, selection, now) {
429
- if (typeof store.resolveDueProviderRetries !== "function")
430
- return;
431
- const selectedTaskIds = selectedTaskIdsForBoundedPass(store, selection);
432
- if (selectedTaskIds?.size === 0)
433
- return;
434
- store.resolveDueProviderRetries(now, selectedTaskIds);
435
- }
436
425
  function selectedTaskIdsForBoundedPass(store, selection) {
437
426
  if (!selection.full) {
438
427
  if ((selection.blockedTaskIds?.size ?? 0) === 0)
@@ -1445,25 +1434,13 @@ export class FileTaskController {
1445
1434
  }
1446
1435
  if (this.#stopped)
1447
1436
  return;
1448
- const deadlines = [
1449
- ...this.store.listOpenInputRequests()
1450
- .flatMap((request) => request.policy.kind === "recommended"
1451
- ? [{
1452
- key: `task:${encodeURIComponent(request.taskId)}`,
1453
- at: Date.parse(request.policy.timeoutAt)
1454
- }]
1455
- : []),
1456
- // Issue 04 durable in-place retry timer: arm the Controller wake at the
1457
- // earliest dispatch/deadline wake. Scheduled retries re-push on their
1458
- // original Session; in-flight retries only wake at the episode deadline.
1459
- // The projection is durable, so a restart resumes the lineage.
1460
- ...(typeof this.store.listPendingProviderRetries === "function"
1461
- ? this.store.listPendingProviderRetries()
1462
- : []).map((retry) => ({
1463
- key: `role:${encodeURIComponent(retry.taskId)}/${encodeURIComponent(retry.roleName)}`,
1464
- at: Date.parse(retry.dueAt)
1465
- }))
1466
- ];
1437
+ const deadlines = this.store.listOpenInputRequests()
1438
+ .flatMap((request) => request.policy.kind === "recommended"
1439
+ ? [{
1440
+ key: `task:${encodeURIComponent(request.taskId)}`,
1441
+ at: Date.parse(request.policy.timeoutAt)
1442
+ }]
1443
+ : []);
1467
1444
  const nearest = nearestDeadlineBatch(deadlines);
1468
1445
  if (nearest === null)
1469
1446
  return;