@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
@@ -6,7 +6,7 @@ import { validateConfiguredAgent } from "../agent/agent.js";
6
6
  import { validateCapabilityGrant } from "../grant/capabilityGrant.js";
7
7
  import { validateReleaseWorkflow } from "../release/releaseWorkflow.js";
8
8
  import { publicationExternalKey, validatePublicationReference } from "../task/publicationReference.js";
9
- import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveContextBudget, resolveDeliveryTimeoutSeconds, resolveLeaderNextActionMode, resolveLeaderSemanticBudgetTurns, resolveProviderRetryAdapters, resolveProviderRetryDelaysSeconds, resolveProviderRetryMaxWindowSeconds, resolveProviderRetryMode, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode, resolveResourcesQuarantineTtlHours, resolveRuntimeHealth, resolveTelemetryEnabled, resolveTelemetryRunCap, resolveTelemetryTerminalKeep, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
9
+ import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveContextBudget, resolveDeliveryTimeoutSeconds, resolveLeaderNextActionMode, resolveLeaderSemanticBudgetTurns, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode, resolveResourcesQuarantineTtlHours, resolveRuntimeHealth, resolveTelemetryEnabled, resolveTelemetryRunCap, resolveTelemetryTerminalKeep, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
10
10
  import { resolveTimeZone } from "../output/timePresentation.js";
11
11
  import { mailboxBatches, consumePendingBatch, mailboxHasWork, mailboxTargetKey, pendingLane, validateWorkMailbox } from "../coordination/workMailbox.js";
12
12
  import { validateContextSnapshot } from "../context/contextSnapshot.js";
@@ -15,7 +15,6 @@ import { validateInputRequest } from "../input/inputRequest.js";
15
15
  import { validateRoleSessionSet } from "../executor/agentExecutor.js";
16
16
  import { validateTaskMessage } from "../message/message.js";
17
17
  import { agentRunDeliveryReceiptId, validateAgentRun } from "../run/agentRun.js";
18
- import { providerRetryWakeAt } from "../run/providerRetry.js";
19
18
  import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
20
19
  import { FileSessionOwnerRegistry } from "../runtime/sessionOwnerRegistry.js";
21
20
  import { validateReviewConfig } from "../review/reviewConfig.js";
@@ -45,7 +44,7 @@ import { CURRENT_AGGREGATE_SCHEMA_VERSION, requireCompatibleStorageSchema, requi
45
44
  export const STORAGE_STATE_FILE = "state.json";
46
45
  /** The root StorageState schema is the persisted aggregate document version. */
47
46
  export const CURRENT_STORAGE_STATE_SCHEMA_VERSION = CURRENT_AGGREGATE_SCHEMA_VERSION;
48
- export const CURRENT_CONFIG_SCHEMA_VERSION = 3;
47
+ export const CURRENT_CONFIG_SCHEMA_VERSION = 4;
49
48
  export const CURRENT_HOME_IDENTITY_SCHEMA_VERSION = 1;
50
49
  export const CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION = 3;
51
50
  /**
@@ -60,7 +59,7 @@ export const CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION = 2;
60
59
  export const CURRENT_PROJECT_SCHEMA_VERSION = 5;
61
60
  export const CURRENT_AGENT_PROFILE_SCHEMA_VERSION = 2;
62
61
  export const CURRENT_GLOBAL_ROLE_SCHEMA_VERSION = 3;
63
- export const CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION = 3;
62
+ export const CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION = 4;
64
63
  export const CURRENT_TASK_SCHEMA_VERSION = 6;
65
64
  export const CURRENT_TASK_BRIEF_SCHEMA_VERSION = 2;
66
65
  export const CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION = 1;
@@ -77,9 +76,9 @@ export const CURRENT_MILESTONE_SCHEMA_VERSION = 2;
77
76
  export const CURRENT_EVENT_SCHEMA_VERSION = 2;
78
77
  export const CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION = 1;
79
78
  export const CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION = 1;
80
- export const CURRENT_WORK_MAILBOX_SCHEMA_VERSION = 2;
79
+ export const CURRENT_WORK_MAILBOX_SCHEMA_VERSION = 3;
81
80
  export const CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION = 1;
82
- export const CURRENT_ROLE_AGENT_SESSION_SCHEMA_VERSION = 3;
81
+ export const CURRENT_ROLE_AGENT_SESSION_SCHEMA_VERSION = 4;
83
82
  export const CURRENT_PENDING_WAKEUP_SCHEMA_VERSION = 1;
84
83
  const STORAGE_LOCK_DIRECTORY = ".state.lock";
85
84
  const LOCK_TIMEOUT_MS = 5_000;
@@ -118,13 +117,13 @@ export const CURRENT_STORED_TASK_SCHEMA_VERSION = 18;
118
117
  * Keep these named at the storage boundary so the upgrade record-axis map can
119
118
  * assert it is classifying the same bytes the store reads and writes.
120
119
  */
121
- export const CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION = 7;
120
+ export const CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION = 8;
122
121
  /**
123
122
  * v7 combines optional Issue 04 retry/receipt fields and Issue 05 Leader
124
123
  * actionability fields. All are optional, so the v6→v7 migration is a
125
124
  * version-only rewrite.
126
125
  */
127
- export const CURRENT_AGENT_RUN_SCHEMA_VERSION = 9;
126
+ export const CURRENT_AGENT_RUN_SCHEMA_VERSION = 10;
128
127
  export const CURRENT_INTEGRATION_QUEUE_SCHEMA_VERSION = 1;
129
128
  export class FileTaskStore {
130
129
  rootDir;
@@ -991,29 +990,6 @@ export class FileTaskStore {
991
990
  }
992
991
  getAgentRun(taskId, id) { return optional(this.#state().tasks[taskId]?.agentRuns[id]); }
993
992
  listAgentRuns(taskId) { return values(this.#requireTask(taskId).agentRuns, "id"); }
994
- listPendingProviderRetries(taskIds) {
995
- // The legacy File store can answer the empty case without a scan fallback.
996
- // If durable retry state exists, the db-only capability must fail closed
997
- // instead of silently losing the Controller's wake deadline.
998
- const tasks = taskIds === undefined
999
- ? this.listTasks()
1000
- : [...new Set(taskIds)].sort(numericCompare).flatMap((taskId) => {
1001
- const task = this.getTask(taskId);
1002
- return task === null ? [] : [task];
1003
- });
1004
- for (const task of tasks) {
1005
- if (task.status !== "active")
1006
- continue;
1007
- for (const run of this.listAgentRuns(task.id)) {
1008
- if (run.status === "active"
1009
- && run.providerRetry !== undefined
1010
- && providerRetryWakeAt(run.providerRetry) !== null) {
1011
- throw new StorageRecordError("Provider retry in place requires the SQLite backend; run `yui update` to migrate this Home.");
1012
- }
1013
- }
1014
- }
1015
- return [];
1016
- }
1017
993
  saveAgentRun(run) {
1018
994
  const stored = identified(run, CURRENT_AGENT_RUN_SCHEMA_VERSION, "id", run.id, "Agent run");
1019
995
  validateAgentRun(stored);
@@ -2498,10 +2474,6 @@ export function validateYuiConfig(config) {
2498
2474
  resolveResourcesGcMode(config.resourcesGcMode);
2499
2475
  resolveResourcesGcAutoQuarantine(config.resourcesGcAutoQuarantine);
2500
2476
  resolveResourcesQuarantineTtlHours(config.resourcesQuarantineTtlHours);
2501
- resolveProviderRetryMode(config.providerRetryMode);
2502
- resolveProviderRetryAdapters(config.providerRetryAdapters);
2503
- resolveProviderRetryDelaysSeconds(config.providerRetryDelaysSeconds);
2504
- resolveProviderRetryMaxWindowSeconds(config.providerRetryMaxWindowSeconds);
2505
2477
  resolveRuntimeHealth(config.runtimeHealth);
2506
2478
  resolveControllerTaskConcurrency(config.controllerTaskConcurrency);
2507
2479
  resolveAgentLaunchInactivityTimeoutSeconds(config.agentLaunchInactivityTimeoutSeconds);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.13.8",
3
+ "version": "0.13.9",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -122,8 +122,8 @@ or yield merely to preserve that native wait.
122
122
  Before the first durable Leader action, Yui observes fresh native generations
123
123
  that produce no WorkItem, Review, Integration, or Leader-attributed durable
124
124
  event. Two such generations create a non-blocking orchestration advisory for
125
- Leader and Operator judgment; they do not fail the Role, reduce the configured
126
- Provider retry policy, or prevent another useful generation. Read the evidence
125
+ Leader and Operator judgment; they do not fail the Role or prevent another
126
+ useful generation. Read the evidence
127
127
  before retrying, then choose whether to continue, change the configured Leader,
128
128
  or perform direct maintenance without manufacturing protocol records merely to
129
129
  silence the advisory.
@@ -703,13 +703,35 @@ newer WorkItem. If the original execution Session cannot be resumed, surface
703
703
  the recovery decision to the user; do not silently discard its context by
704
704
  creating a replacement.
705
705
 
706
- If a native Role Session disappears, run `yui task reconcile <task-id>`,
707
- inspect the Run and partial work, then retry only a confirmed failed Run:
706
+ For a Role runtime failure, inspect the exact error and runtime identities first:
708
707
 
709
708
  ```sh
710
- yui task run retry <run-id>
709
+ yui task event show <task> <agent-error-event>
710
+ yui task role session inspect <task> <role>
711
711
  ```
712
712
 
713
+ When a Provider-accepted Turn fails with availability, `429`, capacity, or a
714
+ recoverable transport error and the Session remains usable, retain the Run and
715
+ Session; a recovery action adds a new Turn on that same native Session. A
716
+ Session preparation failure or Driver rejection before input acceptance fails
717
+ the exact Run once; inspect its error and explicitly retry that failed Run when
718
+ another attempt is useful. Core will not redispatch it on a scheduler tick.
719
+
720
+ If the Driver proves that the Session cannot continue, settle or retire the
721
+ exact active Run, stop only that Role Session, then retry the failed Run so the
722
+ next dispatch starts a new Session:
723
+
724
+ ```sh
725
+ yui task role session stop <task> <role> --reason "<error decision>"
726
+ yui task run retry <task>/<failed-run>
727
+ ```
728
+
729
+ The new Run context contains the prior Agent, adapter, Run, Host activation,
730
+ native Session/Turn identities, and complete raw error through the referenced
731
+ Task event. Inspect recent `runtime.agent-error` events before another fresh
732
+ Session; after repeated fresh-Session failures, report the evidence and bounded
733
+ options to the user instead of inventing another automatic loop.
734
+
713
735
  ## Request a decision
714
736
 
715
737
  When a real user choice, new authority, or unavailable external fact is
@@ -789,8 +811,8 @@ yui task complete <task-id> --summary "<outcome, validation, and remaining risks
789
811
 
790
812
  Retire obsolete WorkItems with `yui task work retire <task>/<work> --summary
791
813
  "..."`, optionally using `--replacement`. If the current Provider Conversation
792
- cannot continue, request a bounded switch with
793
- `yui task role session switch <task> <role> --reason "..."`; the current
794
- Conversation remains authoritative until Yui safely binds the replacement. Archiving is a
814
+ cannot continue, settle its Run and stop the exact idle Session with
815
+ `yui task role session stop`; the next explicit Run dispatch creates the
816
+ replacement. Archiving is a
795
817
  separate global Operator lifecycle action. It performs the final Task-owned
796
818
  runtime and clean-worktree teardown, including this Leader.
@@ -349,12 +349,20 @@ the workflow without claiming that version was delivered.
349
349
  treat it as a permission boundary. The Operator may make code, semantic,
350
350
  requirement, acceptance, recovery, and integration decisions and must leave
351
351
  the real actor and rationale in durable Task state.
352
- - Reconcile a disappeared native Session with `task reconcile`; inspect the Run
353
- before retrying a confirmed failure.
354
- - If the current Provider Conversation cannot continue, use
355
- `yui task role session switch <task> <role> --reason "..."`. The request is
356
- audited, and Yui keeps the old Conversation authoritative until the exact
357
- replacement bind succeeds; never reconstruct identities from terminal text.
352
+ - Inspect `runtime.agent-error` and `yui task role session inspect` before a
353
+ recovery. When a Provider-accepted Turn fails with availability, `429`,
354
+ capacity, or a recoverable transport error and the Session remains usable,
355
+ add a new Turn to that Session. A Session preparation failure or Driver
356
+ rejection before input acceptance fails the exact Run once; explicitly retry
357
+ that failed Run when another attempt is useful. Core does not redispatch it
358
+ on a scheduler tick. If the Driver proves the Session cannot continue, settle
359
+ or retire its exact Run, stop that one idle Session with `yui task role
360
+ session stop <task> <role> --reason "..."`, then retry the failed Run. The
361
+ replacement Run receives the old Agent, adapter, Run, Host, Session, Turn,
362
+ and complete raw-error facts through Task context.
363
+ - Inspect recent errors before creating another fresh Session. After repeated
364
+ fresh-Session failures, summarize the evidence and bounded options to the
365
+ user; do not hide them behind an automatic replacement counter or loop.
358
366
  - Retry only an explicitly failed recovery Job.
359
367
  - When a Leader first-progress advisory is reported, inspect its native
360
368
  generations and absence of durable progress. It is cost evidence rather than
@@ -61,7 +61,9 @@ supported checkpoint/yield command as the final control-plane action, then stop
61
61
  immediately. If that direct command is denied or stale, report the blocker once
62
62
  and stop; do not wrap, retry, broaden permissions, or target another Run.
63
63
 
64
- For a transient Provider retry Envelope, continue the failed Turn in the same
65
- native Session. Do not replay the original Assignment or reload unrelated Task
66
- content. Process/child replacement does not by itself authorize a new Yui
67
- generation or native conversation.
64
+ After a failed Provider Turn, read the referenced `runtime.agent-error` fact.
65
+ The failed Turn is immutable; a recovery is always a new Turn. Continue on the
66
+ same native Session when it remains recoverable, and load only the current Run
67
+ delta instead of replaying its original Assignment. A new Host process does not
68
+ imply a new Session, and a new Session must never be substituted silently for
69
+ the persisted native Session id.
@@ -1,152 +0,0 @@
1
- /**
2
- * Issue 04 — Provider error classification.
3
- *
4
- * Provider failures arrive at the driver boundary as opaque free text (Claude
5
- * StopFailure `error`/`errorDetails`, Codex turn-completion summaries). Each
6
- * driver parses its own Provider's format into a structured
7
- * {@link ProviderErrorCode} at the driver boundary. This module maps those
8
- * codes to provider-neutral error classes by lookup, falling back to text
9
- * matching only when the driver could not produce a structured code.
10
- *
11
- * The retry-in-place coordinator needs a stable, provider-neutral error class
12
- * before it can decide whether the original Session may be retried.
13
- *
14
- * Classes (Issue 04 §2):
15
- * - `transient-provider` — 500/502/504, connection reset, backend capacity;
16
- * the original Session is retried in place.
17
- * - `transport-uncertain` — the request may have been accepted but the
18
- * response was lost; native facts are consulted
19
- * before any resend.
20
- * - `policy-denied` — cyber_policy / permission boundary; never retried
21
- * automatically, never worked around by switching
22
- * Session or widening permission.
23
- * - `session-dead` — the process/tmux/native identity is gone; in-place
24
- * retry stops and a replacement blocker is raised.
25
- * - `invalid-request` — deterministic parameter/protocol error; fail fast,
26
- * never call the Provider again.
27
- * - `unclassified` — no conservative match; behaves like
28
- * `invalid-request` for retry purposes (old
29
- * terminalize-immediately behavior) while remaining
30
- * observable in shadow metrics.
31
- */
32
- import { PROVIDER_ERROR_CODE_CLASS } from "../runtime/providerErrorCodes.js";
33
- /** Classes for which the original Session may be retried in place. */
34
- export const RETRYABLE_PROVIDER_ERROR_CLASSES = [
35
- "transient-provider",
36
- "transport-uncertain"
37
- ];
38
- export function isRetryableProviderErrorClass(errorClass) {
39
- return RETRYABLE_PROVIDER_ERROR_CLASSES.includes(errorClass);
40
- }
41
- /**
42
- * Ordered pattern tables. The first class whose pattern matches wins, so the
43
- * table order is the precedence order. Patterns are matched case-insensitively
44
- * against the concatenation of every available text field.
45
- */
46
- const SESSION_DEAD_PATTERNS = [
47
- { pattern: /session not found/iu, label: "session-not-found" },
48
- { pattern: /no such (session|thread)/iu, label: "no-such-session" },
49
- { pattern: /thread not found/iu, label: "thread-not-found" },
50
- { pattern: /session (has )?expired/iu, label: "session-expired" },
51
- { pattern: /session (has )?ended/iu, label: "session-ended" },
52
- { pattern: /session (is )?dead/iu, label: "session-dead" },
53
- { pattern: /session terminated/iu, label: "session-terminated" },
54
- ];
55
- const CONTEXT_CAPACITY_PATTERNS = [
56
- { pattern: /maximum context length/iu, label: "maximum-context-length" },
57
- { pattern: /context length exceeded/iu, label: "context-length-exceeded" },
58
- { pattern: /context window (is )?(full|exceeded)/iu, label: "context-window-exceeded" },
59
- { pattern: /prompt (is )?too long/iu, label: "prompt-too-long" },
60
- { pattern: /too many tokens/iu, label: "too-many-tokens" }
61
- ];
62
- const POLICY_DENIED_PATTERNS = [
63
- { pattern: /cyber[_-]?policy/iu, label: "cyber-policy" },
64
- { pattern: /policy[_-]?violation/iu, label: "policy-violation" },
65
- { pattern: /usage[_-]?policy/iu, label: "usage-policy" },
66
- { pattern: /content[_-]?policy/iu, label: "content-policy" },
67
- { pattern: /safety[_-]?policy/iu, label: "safety-policy" },
68
- { pattern: /policy denial/iu, label: "policy-denial" }
69
- ];
70
- const INVALID_REQUEST_PATTERNS = [
71
- { pattern: /invalid[_-]?request/iu, label: "invalid-request" },
72
- { pattern: /validation error/iu, label: "validation-error" },
73
- { pattern: /bad request/iu, label: "bad-request" },
74
- { pattern: /\b400\b/u, label: "http-400" },
75
- { pattern: /unknown (flag|tool|argument)/iu, label: "unknown-argument" },
76
- { pattern: /unexpected argument/iu, label: "unexpected-argument" },
77
- { pattern: /invalid schema/iu, label: "invalid-schema" }
78
- ];
79
- const TRANSIENT_PROVIDER_PATTERNS = [
80
- { pattern: /\b50[024]\b/u, label: "http-5xx" },
81
- { pattern: /server[\s_-]?error/iu, label: "server-error" },
82
- { pattern: /internal server error/iu, label: "internal-server-error" },
83
- // HTTP/2 RST_STREAM / gRPC status carried by Claude Code and Codex streams
84
- // (Task-27: "stream error: stream ID …; INTERNAL_ERROR; received from peer").
85
- { pattern: /\binternal[\s_-]?error\b/iu, label: "internal-error" },
86
- { pattern: /connection lost/iu, label: "connection-lost" },
87
- { pattern: /connection reset/iu, label: "connection-reset" },
88
- { pattern: /econnreset/iu, label: "econnreset" },
89
- { pattern: /socket hang up/iu, label: "socket-hang-up" },
90
- { pattern: /kv[_-]?cache[_-]?allocate[_-]?failed/iu, label: "kv-cache-allocate-failed" },
91
- { pattern: /overloaded/iu, label: "overloaded" },
92
- { pattern: /\b429\b/u, label: "http-429" },
93
- { pattern: /rate[_-]?limit/iu, label: "rate-limit" },
94
- { pattern: /upstream/iu, label: "upstream" },
95
- { pattern: /bad gateway/iu, label: "bad-gateway" },
96
- { pattern: /gateway timeout/iu, label: "gateway-timeout" },
97
- { pattern: /service unavailable/iu, label: "service-unavailable" },
98
- { pattern: /temporarily unavailable/iu, label: "temporarily-unavailable" },
99
- { pattern: /try again/iu, label: "try-again" }
100
- ];
101
- const TRANSPORT_UNCERTAIN_PATTERNS = [
102
- { pattern: /timed?[ -]?out/iu, label: "timeout" },
103
- { pattern: /etimedout/iu, label: "etimedout" },
104
- { pattern: /response lost/iu, label: "response-lost" },
105
- { pattern: /lost response/iu, label: "lost-response" },
106
- // A stream-level failure means the response may have been cut mid-turn;
107
- // the retry path consults durable completion facts before any resend.
108
- { pattern: /stream error/iu, label: "stream-error" },
109
- { pattern: /stream interrupted/iu, label: "stream-interrupted" },
110
- { pattern: /interrupted function/iu, label: "interrupted-function" },
111
- { pattern: /controller timeout/iu, label: "controller-timeout" },
112
- { pattern: /delivery (unconfirmed|uncertain|not confirmed)/iu, label: "delivery-unconfirmed" },
113
- { pattern: /unconfirmed delivery/iu, label: "unconfirmed-delivery" },
114
- { pattern: /no response/iu, label: "no-response" }
115
- ];
116
- const CLASS_TABLE = [
117
- { errorClass: "session-dead", patterns: SESSION_DEAD_PATTERNS },
118
- { errorClass: "policy-denied", patterns: POLICY_DENIED_PATTERNS },
119
- { errorClass: "context-capacity", patterns: CONTEXT_CAPACITY_PATTERNS },
120
- { errorClass: "invalid-request", patterns: INVALID_REQUEST_PATTERNS },
121
- { errorClass: "transient-provider", patterns: TRANSIENT_PROVIDER_PATTERNS },
122
- { errorClass: "transport-uncertain", patterns: TRANSPORT_UNCERTAIN_PATTERNS }
123
- ];
124
- /**
125
- * Classifies one provider failure. When the driver produced a structured
126
- * {@link ProviderErrorCode}, the class is looked up directly. Otherwise the
127
- * raw text fields are matched against the fallback pattern tables. Every
128
- * available text field is concatenated so a class can be recognized
129
- * regardless of which field carried it.
130
- */
131
- export function classifyProviderError(input) {
132
- // Structured path: the driver already parsed the Provider's error format.
133
- if (input.errorCode !== undefined) {
134
- const errorClass = PROVIDER_ERROR_CODE_CLASS[input.errorCode];
135
- if (errorClass !== undefined) {
136
- return { errorClass, matched: input.errorCode, basis: "structured" };
137
- }
138
- }
139
- // Text fallback: for drivers that cannot yet produce a structured code.
140
- const text = [input.error, input.errorDetails, input.summary]
141
- .filter((value) => typeof value === "string" && value.length > 0)
142
- .join("\n");
143
- if (text.length === 0)
144
- return { errorClass: "unclassified", matched: "none", basis: "text" };
145
- for (const { errorClass, patterns } of CLASS_TABLE) {
146
- for (const { pattern, label } of patterns) {
147
- if (pattern.test(text))
148
- return { errorClass, matched: label, basis: "text" };
149
- }
150
- }
151
- return { errorClass: "unclassified", matched: "none", basis: "text" };
152
- }
@@ -1,226 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { DEFAULT_PROVIDER_RETRY_DELAYS_SECONDS, DEFAULT_PROVIDER_RETRY_MAX_WINDOW_SECONDS, MAX_PROVIDER_RETRY_ATTEMPTS } from "../config/yuiConfig.js";
3
- import { requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
4
- export const PROVIDER_RETRY_DELAYS_MS = Object.freeze(DEFAULT_PROVIDER_RETRY_DELAYS_SECONDS.map((seconds) => seconds * 1_000));
5
- export const PROVIDER_RETRY_EPISODE_WINDOW_MS = DEFAULT_PROVIDER_RETRY_MAX_WINDOW_SECONDS * 1_000;
6
- export function nextProviderRetryDelayMs(retryIndex, delaysMs = PROVIDER_RETRY_DELAYS_MS) {
7
- if (!Number.isSafeInteger(retryIndex)
8
- || retryIndex < 1
9
- || retryIndex > delaysMs.length) {
10
- throw new Error(`Provider retry index is out of range: ${String(retryIndex)}.`);
11
- }
12
- return delaysMs[retryIndex - 1];
13
- }
14
- /**
15
- * True when the retry lineage has used its total wall-clock budget. The
16
- * budget is measured from the first classified failure, so repeated failures
17
- * never extend it.
18
- */
19
- export function providerRetryBudgetExhausted(value, now, maxWindowMs = PROVIDER_RETRY_EPISODE_WINDOW_MS) {
20
- if (!Number.isSafeInteger(maxWindowMs) || maxWindowMs <= 0) {
21
- throw new Error(`Provider retry max window must be a positive integer: ${String(maxWindowMs)}.`);
22
- }
23
- return Math.min(Date.parse(value.episodeDeadlineAt), Date.parse(value.firstFailureAt) + maxWindowMs) <= now.getTime();
24
- }
25
- export function validateAgentRunProviderRetry(value) {
26
- if (value.schemaVersion !== 2) {
27
- throw new Error("Agent run providerRetry must use schemaVersion 2.");
28
- }
29
- if (!["scheduled", "dispatching", "awaiting-progress", "blocked"].includes(value.state)) {
30
- throw new Error("Agent run providerRetry state is invalid.");
31
- }
32
- requireIdentity(value.episodeId, "Agent run providerRetry episodeId");
33
- requireIdentity(value.failureEventId, "Agent run providerRetry failureEventId");
34
- if (value.policyVersion !== 1)
35
- throw new Error("Provider retry policy version is invalid.");
36
- if (!Number.isSafeInteger(value.consecutiveFailures) || value.consecutiveFailures < 1) {
37
- throw new Error("Agent run providerRetry consecutiveFailures must be positive.");
38
- }
39
- if (!Number.isSafeInteger(value.dispatchedRetries)
40
- || value.dispatchedRetries < 0
41
- || value.dispatchedRetries > value.maxRetries) {
42
- throw new Error("Agent run providerRetry dispatchedRetries is invalid.");
43
- }
44
- if (!Number.isSafeInteger(value.maxRetries)
45
- || value.maxRetries < 1
46
- || value.maxRetries > MAX_PROVIDER_RETRY_ATTEMPTS) {
47
- throw new Error("Agent run providerRetry maxRetries is invalid.");
48
- }
49
- requireTimestamp(value.firstFailureAt, "Agent run providerRetry firstFailureAt");
50
- requireTimestamp(value.lastFailureAt, "Agent run providerRetry lastFailureAt");
51
- requireTimestamp(value.episodeDeadlineAt, "Agent run providerRetry episodeDeadlineAt");
52
- if (Date.parse(value.lastFailureAt) < Date.parse(value.firstFailureAt)) {
53
- throw new Error("Agent run providerRetry lastFailureAt precedes firstFailureAt.");
54
- }
55
- if (Date.parse(value.episodeDeadlineAt) <= Date.parse(value.firstFailureAt)) {
56
- throw new Error("Agent run providerRetry deadline must follow firstFailureAt.");
57
- }
58
- if ((value.state === "scheduled") !== (value.nextAttemptAt !== undefined)) {
59
- throw new Error("Only a scheduled providerRetry may carry nextAttemptAt.");
60
- }
61
- if (value.nextAttemptAt !== undefined) {
62
- requireTimestamp(value.nextAttemptAt, "Agent run providerRetry nextAttemptAt");
63
- if (Date.parse(value.nextAttemptAt) > Date.parse(value.episodeDeadlineAt)) {
64
- throw new Error("Agent run providerRetry next attempt exceeds its episode deadline.");
65
- }
66
- }
67
- if ((value.state === "dispatching" || value.state === "awaiting-progress")
68
- !== (value.lastRetryReceiptId !== undefined)) {
69
- throw new Error("An in-flight providerRetry requires one retry receipt identity.");
70
- }
71
- if (value.lastRetryReceiptId !== undefined) {
72
- requireIdentity(value.lastRetryReceiptId, "Agent run providerRetry receipt id");
73
- }
74
- if (value.launchId !== undefined)
75
- requireIdentity(value.launchId, "Agent run providerRetry launchId");
76
- if (value.nativeSessionId !== undefined) {
77
- requireIdentity(value.nativeSessionId, "Agent run providerRetry nativeSessionId");
78
- }
79
- if (value.failedNativeTurnId !== undefined) {
80
- requireIdentity(value.failedNativeTurnId, "Agent run providerRetry failed native Turn id");
81
- }
82
- requireText(value.lastErrorSummary, "Agent run providerRetry lastErrorSummary");
83
- return value;
84
- }
85
- /** Advance one failure episode without ever changing the native Session. */
86
- export function scheduleProviderRetry(previous, input, now, policy = {
87
- delaysMs: PROVIDER_RETRY_DELAYS_MS,
88
- maxWindowMs: PROVIDER_RETRY_EPISODE_WINDOW_MS
89
- }) {
90
- validateRetrySchedulePolicy(policy);
91
- const at = now.toISOString();
92
- const firstFailureAt = previous?.firstFailureAt ?? at;
93
- const episodeDeadlineAt = previous?.episodeDeadlineAt
94
- ?? new Date(now.getTime() + policy.maxWindowMs).toISOString();
95
- if (now.getTime() >= Date.parse(episodeDeadlineAt)) {
96
- return Object.freeze({ outcome: "exhausted", reason: "window" });
97
- }
98
- const consecutiveFailures = (previous?.consecutiveFailures ?? 0) + 1;
99
- const dispatchedRetries = previous?.dispatchedRetries ?? 0;
100
- const schedule = input.scheduleNextAttempt ?? true;
101
- if (schedule && dispatchedRetries >= policy.delaysMs.length) {
102
- return Object.freeze({ outcome: "exhausted", reason: "attempts" });
103
- }
104
- const state = schedule ? "scheduled" : "blocked";
105
- const retryAfterMs = input.retryAfterMs;
106
- if (retryAfterMs !== undefined && (!Number.isSafeInteger(retryAfterMs) || retryAfterMs <= 0)) {
107
- throw new Error("Provider retry Retry-After must be a positive safe integer.");
108
- }
109
- const delayMs = schedule
110
- ? Math.max(nextProviderRetryDelayMs(dispatchedRetries + 1, policy.delaysMs), retryAfterMs ?? 0)
111
- : undefined;
112
- const nextAttemptAt = delayMs === undefined
113
- ? undefined
114
- : new Date(now.getTime() + delayMs).toISOString();
115
- if (nextAttemptAt !== undefined && Date.parse(nextAttemptAt) > Date.parse(episodeDeadlineAt)) {
116
- return Object.freeze({
117
- outcome: "exhausted",
118
- reason: retryAfterMs === undefined ? "window" : "retry-after-window"
119
- });
120
- }
121
- const retry = validateAgentRunProviderRetry({
122
- schemaVersion: 2,
123
- episodeId: previous?.episodeId ?? createHash("sha256")
124
- .update(`${firstFailureAt}\0${input.nativeSessionId ?? "unknown"}\0${input.launchId ?? "unknown"}`)
125
- .digest("hex"),
126
- failureEventId: requireIdentity(input.failureEventId, "Provider failure event id"),
127
- policyVersion: 1,
128
- state,
129
- errorClass: input.errorClass,
130
- consecutiveFailures,
131
- dispatchedRetries,
132
- maxRetries: policy.delaysMs.length,
133
- firstFailureAt,
134
- lastFailureAt: at,
135
- episodeDeadlineAt,
136
- ...(nextAttemptAt === undefined ? {} : { nextAttemptAt }),
137
- ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
138
- ...(input.nativeSessionId === undefined ? {} : { nativeSessionId: input.nativeSessionId }),
139
- ...(input.failedNativeTurnId === undefined
140
- ? {}
141
- : { failedNativeTurnId: input.failedNativeTurnId }),
142
- lastErrorSummary: input.lastErrorSummary
143
- });
144
- return Object.freeze({ outcome: schedule ? "scheduled" : "blocked", retry });
145
- }
146
- function validateRetrySchedulePolicy(policy) {
147
- if (!Number.isSafeInteger(policy.maxWindowMs) || policy.maxWindowMs < 1) {
148
- throw new Error("Provider retry max window must be a positive safe integer.");
149
- }
150
- if (policy.delaysMs.length < 1 || policy.delaysMs.length > MAX_PROVIDER_RETRY_ATTEMPTS
151
- || policy.delaysMs.some((delay) => !Number.isSafeInteger(delay) || delay < 1)) {
152
- throw new Error(`Provider retry delay schedule must contain 1-${MAX_PROVIDER_RETRY_ATTEMPTS} positive safe integers.`);
153
- }
154
- }
155
- /** Mark that one short continuation request was dispatched and now awaits any correlated progress. */
156
- export function prepareProviderRetryDispatch(value, receiptId, now) {
157
- if (value.state !== "scheduled" || value.nextAttemptAt === undefined)
158
- return value;
159
- if (now.getTime() > Date.parse(value.episodeDeadlineAt)) {
160
- throw new Error("Provider retry episode expired before dispatch.");
161
- }
162
- const { nextAttemptAt: _nextAttemptAt, ...rest } = value;
163
- return validateAgentRunProviderRetry({
164
- ...rest,
165
- state: "dispatching",
166
- lastRetryReceiptId: requireIdentity(receiptId, "Provider retry receipt id")
167
- });
168
- }
169
- export function markProviderRetryDispatched(value) {
170
- if (value.state !== "dispatching")
171
- return value;
172
- return validateAgentRunProviderRetry({
173
- ...value,
174
- state: "awaiting-progress",
175
- dispatchedRetries: value.dispatchedRetries + 1
176
- });
177
- }
178
- export function providerRetryIsDue(value, now) {
179
- return value.state === "scheduled"
180
- && value.nextAttemptAt !== undefined
181
- && Date.parse(value.nextAttemptAt) <= now.getTime();
182
- }
183
- /**
184
- * Next Controller wake for an active automatic retry episode. Scheduled
185
- * retries wake to dispatch; an in-flight retry wakes only at the episode
186
- * deadline so silence cannot strand the Run forever. Blocked states are
187
- * intentionally excluded because they require native/user evidence rather
188
- * than an automatic lifecycle transition.
189
- */
190
- export function providerRetryWakeAt(value) {
191
- if (value.state === "scheduled")
192
- return value.nextAttemptAt ?? null;
193
- if (value.state === "dispatching" || value.state === "awaiting-progress") {
194
- return value.episodeDeadlineAt;
195
- }
196
- return null;
197
- }
198
- /** Delay a control-plane recovery gap without changing either failure counter. */
199
- export function deferProviderRetry(value, now) {
200
- const deadline = Date.parse(value.episodeDeadlineAt);
201
- if (now.getTime() >= deadline)
202
- return null;
203
- const { lastRetryReceiptId: _receipt, nextAttemptAt: _next, ...rest } = value;
204
- return validateAgentRunProviderRetry({
205
- ...rest,
206
- state: "scheduled",
207
- nextAttemptAt: new Date(Math.min(now.getTime() + 15_000, deadline)).toISOString()
208
- });
209
- }
210
- /** Short recovery instruction; it cannot repeat the Task Assignment body. */
211
- export function serializeProviderRetryEnvelope(input) {
212
- validateAgentRunProviderRetry(input.retry);
213
- const retryOrdinal = input.retry.state === "dispatching"
214
- ? input.retry.dispatchedRetries + 1
215
- : input.retry.dispatchedRetries;
216
- return [
217
- "Yui managed in-Session continuation retry.",
218
- `task=${requireIdentity(input.taskId, "Provider retry task id")} run=${requireIdentity(input.runId, "Provider retry run id")} role=${requireIdentity(input.roleName, "Provider retry role")}`,
219
- `episode=${input.retry.episodeId} retry=${retryOrdinal}/${input.retry.maxRetries} receipt=${input.retry.lastRetryReceiptId ?? "pending"}`,
220
- `failureEvent=${input.retry.failureEventId}`,
221
- ...(input.retry.failedNativeTurnId === undefined
222
- ? []
223
- : [`retryOfTurn=${input.retry.failedNativeTurnId}`]),
224
- "Continue the existing native conversation from its latest accepted state. Do not repeat completed work; load exact Run deltas if needed."
225
- ].join("\n");
226
- }
@@ -1,27 +0,0 @@
1
- import { resolveProviderRetryAdapters, resolveProviderRetryDelaysSeconds, resolveProviderRetryMaxWindowSeconds, resolveProviderRetryMode } from "../config/yuiConfig.js";
2
- /**
3
- * Resolves the retry flags from the durable Yui config. Homes without the
4
- * fields get the safe defaults: enforce mode, all supported adapters, receipt
5
- * replay on, 10-minute budget.
6
- */
7
- export function providerRetryConfig(config) {
8
- const mode = resolveProviderRetryMode(config.providerRetryMode);
9
- const adapters = resolveProviderRetryAdapters(config.providerRetryAdapters);
10
- return {
11
- mode: adapters.length === 0 ? "off" : mode,
12
- adapters,
13
- delaysMs: resolveProviderRetryDelaysSeconds(config.providerRetryDelaysSeconds)
14
- .map((seconds) => seconds * 1_000),
15
- maxWindowMs: resolveProviderRetryMaxWindowSeconds(config.providerRetryMaxWindowSeconds)
16
- * 1_000
17
- };
18
- }
19
- /** Whether the adapter has in-place retry enabled in the given mode. */
20
- export function providerRetryEnabledForAdapter(config, adapterId, mode) {
21
- return config.mode === mode
22
- && providerRetryAdapterEnabled(config, adapterId);
23
- }
24
- /** Default admission is capability-driven, not a hard-coded Provider list. */
25
- export function providerRetryAdapterEnabled(config, adapterId) {
26
- return config.adapters === "all-capable" || config.adapters.includes(adapterId);
27
- }