@dynamicagents/plugins 0.11.1 → 0.12.0

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 (94) hide show
  1. package/README.md +12 -13
  2. package/dist/arc-agi/analysis.d.ts +16 -30
  3. package/dist/arc-agi/analysis.js +20 -47
  4. package/dist/arc-agi/game-tools.d.ts +5 -6
  5. package/dist/arc-agi/index.d.ts +6 -8
  6. package/dist/arc-agi/index.js +3 -3
  7. package/dist/arc-agi/main-agent.d.ts +5 -8
  8. package/dist/arc-agi/main-agent.js +5 -8
  9. package/dist/arc-agi/recipe.d.ts +17 -20
  10. package/dist/arc-agi/recipe.js +17 -20
  11. package/dist/arc-agi/soul.d.ts +16 -27
  12. package/dist/arc-agi/soul.js +16 -27
  13. package/dist/arc-agi/store.d.ts +4 -5
  14. package/dist/arc-agi/store.js +4 -5
  15. package/dist/arc-agi/tools.d.ts +1 -1
  16. package/dist/arc-agi/tools.js +10 -12
  17. package/dist/arc-agi/types.d.ts +6 -7
  18. package/dist/claude-code/config.d.ts +88 -11
  19. package/dist/claude-code/config.js +11 -2
  20. package/dist/claude-code/credentials.d.ts +21 -9
  21. package/dist/claude-code/credentials.js +43 -9
  22. package/dist/claude-code/egress.d.ts +7 -13
  23. package/dist/claude-code/egress.js +9 -10
  24. package/dist/claude-code/events.d.ts +47 -0
  25. package/dist/claude-code/events.js +96 -10
  26. package/dist/claude-code/index.d.ts +31 -13
  27. package/dist/claude-code/index.js +62 -15
  28. package/dist/claude-code/run.d.ts +135 -41
  29. package/dist/claude-code/run.js +366 -61
  30. package/dist/computer/advisory.d.ts +31 -18
  31. package/dist/computer/advisory.js +30 -5
  32. package/dist/computer/context.d.ts +23 -0
  33. package/dist/computer/context.js +225 -0
  34. package/dist/computer/file-lock.d.ts +5 -0
  35. package/dist/computer/file-lock.js +39 -0
  36. package/dist/computer/gate.d.ts +8 -8
  37. package/dist/computer/gate.js +7 -7
  38. package/dist/computer/host/ca-trust.d.ts +88 -0
  39. package/dist/computer/host/ca-trust.js +303 -0
  40. package/dist/computer/host/container-deps.d.ts +54 -0
  41. package/dist/computer/host/container-deps.js +224 -0
  42. package/dist/computer/host/container-fault.d.ts +38 -0
  43. package/dist/computer/host/container-fault.js +58 -0
  44. package/dist/computer/host/git-host.d.ts +62 -0
  45. package/dist/computer/host/git-host.js +156 -0
  46. package/dist/computer/host/git-identity.d.ts +57 -0
  47. package/dist/computer/host/git-identity.js +147 -0
  48. package/dist/computer/host/install-job.d.ts +121 -0
  49. package/dist/computer/host/install-job.js +766 -0
  50. package/dist/computer/host/sync.d.ts +111 -0
  51. package/dist/computer/host/sync.js +124 -0
  52. package/dist/computer/host/wake.d.ts +19 -0
  53. package/dist/computer/host/wake.js +10 -0
  54. package/dist/computer/host/workspace.d.ts +424 -0
  55. package/dist/computer/host/workspace.js +1258 -0
  56. package/dist/computer/index.d.ts +70 -50
  57. package/dist/computer/index.js +79 -767
  58. package/dist/computer/install.d.ts +8 -12
  59. package/dist/computer/install.js +6 -9
  60. package/dist/computer/paths.d.ts +21 -16
  61. package/dist/computer/paths.js +40 -38
  62. package/dist/computer/read.d.ts +18 -22
  63. package/dist/computer/read.js +24 -27
  64. package/dist/computer/render.d.ts +28 -0
  65. package/dist/computer/render.js +34 -1
  66. package/dist/computer/shell.d.ts +45 -0
  67. package/dist/computer/shell.js +80 -0
  68. package/dist/computer/tools-exec.d.ts +3 -0
  69. package/dist/computer/tools-exec.js +269 -0
  70. package/dist/computer/tools-file.d.ts +4 -0
  71. package/dist/computer/tools-file.js +124 -0
  72. package/dist/computer/tools-find.d.ts +3 -0
  73. package/dist/computer/tools-find.js +216 -0
  74. package/dist/recall/index.d.ts +9 -3
  75. package/dist/recall/index.js +22 -12
  76. package/dist/repo/checkout.d.ts +34 -4
  77. package/dist/repo/checkout.js +36 -1
  78. package/dist/repo/context.d.ts +109 -0
  79. package/dist/repo/context.js +530 -0
  80. package/dist/repo/index.d.ts +6 -29
  81. package/dist/repo/index.js +17 -1027
  82. package/dist/repo/tools-clone.d.ts +4 -0
  83. package/dist/repo/tools-clone.js +192 -0
  84. package/dist/repo/tools-forge.d.ts +4 -0
  85. package/dist/repo/tools-forge.js +179 -0
  86. package/dist/repo/tools-review.d.ts +3 -0
  87. package/dist/repo/tools-review.js +373 -0
  88. package/dist/repo/tools-worktree.d.ts +4 -0
  89. package/dist/repo/tools-worktree.js +221 -0
  90. package/dist/triage/index.d.ts +6 -1
  91. package/dist/triage/index.js +32 -23
  92. package/dist/workspace/index.d.ts +3 -3
  93. package/dist/workspace/index.js +3 -3
  94. package/package.json +21 -15
@@ -16,11 +16,10 @@ import { z } from "zod";
16
16
  *
17
17
  * A plugin must never run drizzle's durable-sqlite **migrator**: it keeps one
18
18
  * flat integer journal and one global `__drizzle_migrations` table, which two
19
- * independently-versioned packages cannot share — the two predecessor agents,
20
- * both consuming the same `notify_tasks` module, had already forked that journal
21
- * at index 1. And `drizzle-kit generate` diffs against a snapshot in a single
22
- * output directory, so a plugin shipping from its own repo cannot produce a
23
- * correct diff at all.
19
+ * independently-versioned packages cannot share: two agents consuming the same
20
+ * module fork that journal at the first index they disagree on. And
21
+ * `drizzle-kit generate` diffs against a snapshot in a single output directory,
22
+ * so a plugin shipping from its own repo cannot produce a correct diff at all.
24
23
  *
25
24
  * None of that touches the **query builder**, which is a typed wrapper over the
26
25
  * same `DurableObjectStorage` holding no journal and no connection state. So the
@@ -30,7 +30,7 @@ import type { ToolFamilyContext, RecipeToolSet } from "@dynamicagents/core";
30
30
  /**
31
31
  * This family's key in a Recipe's `toolFamilies`. Exported because it is also
32
32
  * what tells the parent a Subtask needs a scorecard leased before it can run —
33
- * see `resolveRuntime` in the ReactiveAgent DO.
33
+ * see `resolveRuntime` in `./index.ts`.
34
34
  */
35
35
  export declare const ARC_GAME_FAMILY = "arc-game";
36
36
  export declare function buildArcGameTools(apiKey: string, ctx: ToolFamilyContext): RecipeToolSet;
@@ -35,16 +35,15 @@ import { ARC_SESSION_PATH } from "./types.js";
35
35
  /**
36
36
  * This family's key in a Recipe's `toolFamilies`. Exported because it is also
37
37
  * what tells the parent a Subtask needs a scorecard leased before it can run —
38
- * see `resolveRuntime` in the ReactiveAgent DO.
38
+ * see `resolveRuntime` in `./index.ts`.
39
39
  */
40
40
  export const ARC_GAME_FAMILY = "arc-game";
41
41
  export function buildArcGameTools(apiKey, ctx) {
42
42
  const { workspace, emitProgress, params } = ctx;
43
- // Closed over from the plugin's config, never `ctx.env`. The predecessor
44
- // passed the Worker `env` to every tool family, which a published package
45
- // cannot do — `Env` is the ambient interface `wrangler types` generates into a
46
- // *consumer's* app and does not exist here. Config-at-instantiation is also
47
- // the only thing that works on Workers at all, where `env` has no module scope.
43
+ // Closed over from the plugin's config, never `ctx.env`: a published package
44
+ // cannot name `Env`, which is the ambient interface `wrangler types` generates
45
+ // into a *consumer's* app. Config-at-instantiation is also the only thing that
46
+ // works on Workers at all, where `env` has no module scope.
48
47
  const runtime = runtimeAs(ctx.runtime);
49
48
  // All settled before the model runs — it cannot pick a different game, and it
50
49
  // cannot see, choose, or name a card or a play at all. The game is the type's
@@ -400,10 +399,9 @@ export function buildArcGameTools(apiKey, ctx) {
400
399
  }
401
400
  })
402
401
  };
403
- // No `abort` hook: the only external state this family used to hold was the
404
- // scorecard, and nothing closes one any more — the API retires an idle card on
405
- // its own. A play left unfinished is simply a run the card records as
406
- // incomplete.
402
+ // No `abort` hook: this family holds no external state to release. The API
403
+ // retires an idle card itself, so a play left unfinished is simply a run the
404
+ // card records as incomplete.
407
405
  return { tools };
408
406
  }
409
407
  /**
@@ -411,8 +409,8 @@ export function buildArcGameTools(apiKey, ctx) {
411
409
  * supply.
412
410
  *
413
411
  * With `runtime.frame` — this chunk is the one that opened the play — the state
414
- * is complete and indistinguishable from what a local RESET used to produce.
415
- * Without it the play already existed, so everything a frame carries is unknown:
412
+ * is complete. Without it the play already existed, so everything a frame
413
+ * carries is unknown:
416
414
  * empty `availableActions` and a null board, both filled in by the first ACTION.
417
415
  * Only `guid` matters for correctness, and it always comes from the parent.
418
416
  */
@@ -188,13 +188,12 @@ export declare const ARC_SESSION_PATH = "arc/session.json";
188
188
  * This plugin's slice of a Subtask's runtime bag — what the parent resolves for
189
189
  * an execution and no model could supply.
190
190
  *
191
- * Core's `SubtaskRuntime` is an open `Record<string, unknown>`, and these four
192
- * fields are the reason. The predecessor declared them in core's own delegation
193
- * types, which meant core imported a domain's vocabulary and every unrelated
194
- * agent carried `cardId`, `cookies`, `guid` and `frame` in its bundle. The
195
- * plugin that writes this slice is the plugin that reads it, so narrow it with
196
- * `runtimeAs<ArcRuntime>` at the boundary and keep the cast's blast radius to
197
- * this one module.
191
+ * Core's `SubtaskRuntime` is an open `Record<string, unknown>`, and this slice is
192
+ * why: declared in core's own delegation types instead, core would import a
193
+ * domain's vocabulary and every unrelated agent would carry it in its bundle.
194
+ * The plugin that writes this slice is the plugin that reads it, so narrow it
195
+ * with `runtimeAs<ArcRuntime>` at the boundary and keep the cast's blast radius
196
+ * to this one module.
198
197
  */
199
198
  export type ArcRuntime = {
200
199
  /** The scorecard leased for this chunk. */
@@ -25,6 +25,21 @@ export type PermissionMode = "default" | "acceptEdits" | "bypassPermissions" | "
25
25
  * {@link ClaudeCodeConfig.permissionMode}.
26
26
  */
27
27
  export declare const DEFAULT_PERMISSION_MODE: PermissionMode;
28
+ /**
29
+ * The effort levels `claude --effort` accepts.
30
+ *
31
+ * Spelled out for a sharper reason than {@link PermissionMode}: an unrecognised
32
+ * permission mode exits 1, but an unrecognised effort is **warned about on
33
+ * stderr and then ignored**, leaving the session at the model's default. A typo
34
+ * here does not fail, it quietly buys nothing — and stderr from a headless
35
+ * session reaches an operator far less reliably than a type error reaches a
36
+ * developer.
37
+ *
38
+ * A model that does not carry a level is the same silence: the CLI clamps down
39
+ * to `high` rather than refusing, so asking for more than a model offers is
40
+ * indistinguishable from asking for `high`.
41
+ */
42
+ export type EffortLevel = "low" | "medium" | "high" | "xhigh" | "max";
28
43
  export interface ClaudeCodeConfig {
29
44
  /**
30
45
  * The credential pool, in priority order — index 0 is tried first.
@@ -61,14 +76,28 @@ export interface ClaudeCodeConfig {
61
76
  /** Which model the session runs. Unset, Claude Code picks its own default. */
62
77
  model?: string;
63
78
  /**
64
- * Ceiling on the *outer* session's turns.
79
+ * How hard the session's model thinks, per turn.
80
+ *
81
+ * Unset, the model's own default applies, which is `high` for the frontier
82
+ * models this plugin exists to reach. Raising it buys depth at a multiple of
83
+ * the spend, per turn — so the level decides how many sessions a credential
84
+ * holds, not just how well one thinks. The README's Costs section carries the
85
+ * multiples and the sizing they imply.
65
86
  *
66
- * Advisory. Claude Code's own subagent tree multiplies whatever this says, and
67
- * the tree is invisible to Dynamic Agents' scheduler. {@link timeoutMs} is
68
- * the ceiling that actually holds, because the container runtime enforces it.
87
+ * Not to be confused with the host round loop's own reasoning effort, which
88
+ * belongs to a different model on the Worker side. This one reaches only the
89
+ * CLI in the container.
90
+ */
91
+ effort?: EffortLevel;
92
+ /**
93
+ * Caps on Claude Code's own subagent tree.
94
+ *
95
+ * Advisory, and the distinction is the reason there is no turn ceiling beside
96
+ * them — see the README on what this package deliberately does not do. That
97
+ * tree is invisible to Dynamic Agents' scheduler, so it multiplies whatever
98
+ * these say; {@link timeoutMs} is the ceiling that actually holds, because the
99
+ * container runtime enforces it.
69
100
  */
70
- maxTurns?: number;
71
- /** Caps on Claude Code's own subagent tree. Advisory, like `maxTurns`. */
72
101
  maxSubagentDepth?: number;
73
102
  maxConcurrentSubagents?: number;
74
103
  /**
@@ -116,10 +145,33 @@ export interface ClaudeCodeConfig {
116
145
  /**
117
146
  * How long one chunk blocks before checkpointing and yielding.
118
147
  *
119
- * Must stay under the Workflow step timeout with room to spare, and must be
120
- * long enough that a session does not exhaust its branch's chunk allowance
121
- * while it is still thinking. Defaults to eight minutes, against a 30-minute
122
- * step timeout and a 15-minute soft chunk budget.
148
+ * **A read deadline on the session's stdout stream, not a halt.** Nothing is
149
+ * stopped and nothing is destroyed when it expires: the host detaches, the
150
+ * session process inside the container never notices, and the next chunk
151
+ * re-attaches to the live session from the cursor. So the window is bounded
152
+ * against `STEP_TIMEOUT_MS` rather than against core's `CHUNK_SOFT_MS` —
153
+ * there is no in-flight turn to overrun, which is what sizes the recipe
154
+ * path's budget.
155
+ *
156
+ * What prices the headroom instead is the post-exit filesystem pull, which is
157
+ * deliberately unbounded because it carries whatever the session wrote,
158
+ * dependency trees included, and can outlast the window. So no value here can
159
+ * guarantee the chunk fits inside the step timeout: what is left over is
160
+ * sized for an *expected* pull, plus the sub-second unwind of yielding and
161
+ * the step's own bookkeeping, and a large enough pull overruns it whatever
162
+ * this is set to. See {@link file://./run.ts DrainOptions.windowMs}.
163
+ *
164
+ * **Overrunning is the cheaper failure, which is why the default is
165
+ * generous.** The step is killed and retried; the filesystem sync resumes
166
+ * from the blocks it has already committed rather than starting over, and the
167
+ * retry re-attaches to the session from the cursor — so an overrun costs a
168
+ * retry and some replayed notes, never a session's edits. An undersized
169
+ * window costs a chunk boundary on *every* session instead — a checkpoint, a
170
+ * fresh step, a re-hydrated subagent and a re-attach that replays the tail —
171
+ * paid whether or not the pull was ever going to be large.
172
+ *
173
+ * It must also be long enough that a session does not exhaust its branch's
174
+ * chunk allowance while it is still thinking.
123
175
  */
124
176
  windowMs?: number;
125
177
  /**
@@ -133,8 +185,33 @@ export interface ClaudeCodeConfig {
133
185
  timeoutMs?: number;
134
186
  /** Extra environment for the session. Never secrets — see the README. */
135
187
  env?: Record<string, string>;
188
+ /**
189
+ * Who the session's own git commits are attributed to.
190
+ *
191
+ * A session has a shell and a checkout, so it commits — and it amends,
192
+ * rebases and cherry-picks, none of which a host can intercept the way it
193
+ * names an identity on a commit it makes itself. Left unset, every one of
194
+ * those falls through to whatever config the checkout happens to carry, which
195
+ * is a value frozen whenever that checkout was created.
196
+ *
197
+ * See {@link file://./run.ts buildLaunch} for why this becomes an environment
198
+ * rather than a `git config`.
199
+ */
200
+ author?: {
201
+ name: string;
202
+ email: string;
203
+ };
136
204
  }
137
- /** Eight minutes: comfortably inside `CHUNK_SOFT_MS`, long enough to be useful. */
205
+ /**
206
+ * Twenty minutes against a 30-minute step timeout, which leaves ~10 minutes for
207
+ * the sub-second unwind of yielding the window and for the post-exit filesystem
208
+ * pull. An expected pull fits there; a pull that carries an install's
209
+ * dependency tree may not, and nothing here promises it will — that pull is
210
+ * unbounded by design. Sized for the expected case deliberately: the pull that
211
+ * overruns is retried and resumes, while a shorter window would buy that back
212
+ * by paying for extra chunk boundaries on every session. See
213
+ * {@link ClaudeCodeConfig.windowMs}.
214
+ */
138
215
  export declare const DEFAULT_WINDOW_MS: number;
139
216
  /** Fifteen minutes, well under the coder workspace's 20-minute idle timer. */
140
217
  export declare const DEFAULT_TIMEOUT_MS: number;
@@ -7,7 +7,16 @@
7
7
  * {@link ClaudeCodeConfig.permissionMode}.
8
8
  */
9
9
  export const DEFAULT_PERMISSION_MODE = "bypassPermissions";
10
- /** Eight minutes: comfortably inside `CHUNK_SOFT_MS`, long enough to be useful. */
11
- export const DEFAULT_WINDOW_MS = 8 * 60_000;
10
+ /**
11
+ * Twenty minutes against a 30-minute step timeout, which leaves ~10 minutes for
12
+ * the sub-second unwind of yielding the window and for the post-exit filesystem
13
+ * pull. An expected pull fits there; a pull that carries an install's
14
+ * dependency tree may not, and nothing here promises it will — that pull is
15
+ * unbounded by design. Sized for the expected case deliberately: the pull that
16
+ * overruns is retried and resumes, while a shorter window would buy that back
17
+ * by paying for extra chunk boundaries on every session. See
18
+ * {@link ClaudeCodeConfig.windowMs}.
19
+ */
20
+ export const DEFAULT_WINDOW_MS = 20 * 60_000;
12
21
  /** Fifteen minutes, well under the coder workspace's 20-minute idle timer. */
13
22
  export const DEFAULT_TIMEOUT_MS = 15 * 60_000;
@@ -5,15 +5,12 @@
5
5
  * ## Why a pool rather than a budget
6
6
  *
7
7
  * A Claude subscription has two limits that matter here: a rolling 5-hour
8
- * session bucket and a weekly one. Neither is readable, and 0.5.0 tried to stay
9
- * under them by *estimating* spend — a counter in dollars, a cap, and a gate
10
- * that refused to start work when the counter got high.
11
- *
12
- * That was the wrong instrument. The estimate is a guess about a bucket nobody
13
- * can see, and it only moved when a run *ended* (usage is learned from the
14
- * terminal `result` event), so it was never a cap on spend at all — only a gate
15
- * on starting. Meanwhile the bucket itself says so, precisely, the moment it is
16
- * empty: Anthropic answers `429`.
8
+ * session bucket and a weekly one. Neither is readable, so staying under them by
9
+ * *estimating* spend is the wrong instrument twice over — the estimate is a
10
+ * guess about a bucket nobody can see, and it only moves when a run *ends*,
11
+ * since usage is learned from the terminal `result` event. It is never a cap on
12
+ * spend, only a gate on starting. The bucket itself says so precisely the moment
13
+ * it is empty: Anthropic answers `429`.
17
14
  *
18
15
  * So this module does not predict. It **detects and routes around**: the
19
16
  * credential becomes an ordered pool, the gateway uses the first usable entry,
@@ -154,6 +151,21 @@ export type Refusal = {
154
151
  kind: "transient";
155
152
  retryAfterMs: number;
156
153
  };
154
+ /**
155
+ * Read the client's own bucket reading as a verdict on the credential in use.
156
+ *
157
+ * `undefined` means "nothing to act on", which today is every reading — see
158
+ * {@link RATE_LIMIT_SPENT}. Otherwise the epoch-milliseconds the bucket refills,
159
+ * ready for {@link CredentialPool.spend}.
160
+ *
161
+ * Here rather than beside the parser because this is the same question
162
+ * {@link readRefusal} answers about a response: is this credential still worth
163
+ * using? The parser's job is to say what the line contained.
164
+ */
165
+ export declare function readRateLimitEvent(info: {
166
+ status: string;
167
+ resetsAt?: number;
168
+ }): number | undefined;
157
169
  /**
158
170
  * Read a response as a verdict on the credential that produced it.
159
171
  *
@@ -5,15 +5,12 @@
5
5
  * ## Why a pool rather than a budget
6
6
  *
7
7
  * A Claude subscription has two limits that matter here: a rolling 5-hour
8
- * session bucket and a weekly one. Neither is readable, and 0.5.0 tried to stay
9
- * under them by *estimating* spend — a counter in dollars, a cap, and a gate
10
- * that refused to start work when the counter got high.
11
- *
12
- * That was the wrong instrument. The estimate is a guess about a bucket nobody
13
- * can see, and it only moved when a run *ended* (usage is learned from the
14
- * terminal `result` event), so it was never a cap on spend at all — only a gate
15
- * on starting. Meanwhile the bucket itself says so, precisely, the moment it is
16
- * empty: Anthropic answers `429`.
8
+ * session bucket and a weekly one. Neither is readable, so staying under them by
9
+ * *estimating* spend is the wrong instrument twice over — the estimate is a
10
+ * guess about a bucket nobody can see, and it only moves when a run *ends*,
11
+ * since usage is learned from the terminal `result` event. It is never a cap on
12
+ * spend, only a gate on starting. The bucket itself says so precisely the moment
13
+ * it is empty: Anthropic answers `429`.
17
14
  *
18
15
  * So this module does not predict. It **detects and routes around**: the
19
16
  * credential becomes an ordered pool, the gateway uses the first usable entry,
@@ -243,6 +240,43 @@ function resetAtFrom(value) {
243
240
  const parsed = Date.parse(trimmed);
244
241
  return Number.isNaN(parsed) ? undefined : parsed;
245
242
  }
243
+ /**
244
+ * The `rate_limit_event` statuses that mean the bucket is empty.
245
+ *
246
+ * **Empty, and that is the finding rather than a placeholder.** The client
247
+ * announces its bucket on every session and the only status ever observed is
248
+ * {@link file://./events.ts RATE_LIMIT_OK} — so there is no known value here to
249
+ * put in, and putting in a guess is the expensive half of the asymmetry
250
+ * {@link readRefusal} is built around: reading a healthy bucket as exhausted
251
+ * retires a working credential for hours, and with a pool of one that is an
252
+ * outage. The same reasoning that leaves a 403 unclassified leaves this set
253
+ * empty.
254
+ *
255
+ * What fills it is the log. `run.ts` reports every change of reading, and a
256
+ * status that is not `allowed` is also surfaced as a progress note, so the first
257
+ * real one arrives named — at which point it goes in here and the pool starts
258
+ * rotating *before* Anthropic refuses a request rather than because it did.
259
+ */
260
+ const RATE_LIMIT_SPENT = new Set();
261
+ /**
262
+ * Read the client's own bucket reading as a verdict on the credential in use.
263
+ *
264
+ * `undefined` means "nothing to act on", which today is every reading — see
265
+ * {@link RATE_LIMIT_SPENT}. Otherwise the epoch-milliseconds the bucket refills,
266
+ * ready for {@link CredentialPool.spend}.
267
+ *
268
+ * Here rather than beside the parser because this is the same question
269
+ * {@link readRefusal} answers about a response: is this credential still worth
270
+ * using? The parser's job is to say what the line contained.
271
+ */
272
+ export function readRateLimitEvent(info) {
273
+ if (!RATE_LIMIT_SPENT.has(info.status))
274
+ return undefined;
275
+ // Seconds on the wire. A value passed through unconverted lands in 1970, and a
276
+ // credential marked spent until then reads as usable — the failure would be
277
+ // this function doing nothing, silently.
278
+ return info.resetsAt === undefined ? undefined : info.resetsAt * 1000;
279
+ }
246
280
  /**
247
281
  * Read a response as a verdict on the credential that produced it.
248
282
  *
@@ -33,10 +33,9 @@ import { type CredentialStore } from "./credentials.js";
33
33
  *
34
34
  * ## What this deliberately does not do
35
35
  *
36
- * It does not meter. 0.5.0 had a budget gate that estimated spend in dollars and
37
- * refused to start work over a cap; 0.6.0 deleted it. An estimate is a guess
38
- * about a bucket nobody can read, it only moved once per run, and the bucket
39
- * itself announces the answer exactly — see the docblock in `credentials.ts`.
36
+ * It does not meter, and a budget gate must not come back: an estimate of spend
37
+ * is a guess about a bucket nobody can read, moves only once per run, and the
38
+ * bucket announces the answer exactly — see the docblock in `credentials.ts`.
40
39
  *
41
40
  * And it does not, by default, **bound exfiltration**. The container holds the
42
41
  * checkout, and with no restriction configured it can send it anywhere. That is
@@ -49,16 +48,11 @@ import { type CredentialStore } from "./credentials.js";
49
48
  export declare const ANTHROPIC_HOST = "api.anthropic.com";
50
49
  export interface EgressConfig {
51
50
  /**
52
- * The credential pool, in priority order — index 0 is tried first.
51
+ * The credential pool — see {@link file://./config.ts ClaudeCodeConfig.credentials}.
53
52
  *
54
- * A thunk so a rotated secret is picked up without rebuilding the plugin list.
55
- * Called per request; it must be cheap. **None of these values enters the
56
- * container**: the session is launched with `CREDENTIAL_PLACEHOLDER` and the
57
- * swap happens here.
58
- *
59
- * An array of one is entirely valid and behaves as 0.5.0's single credential
60
- * did — used until its bucket empties, after which requests are refused with
61
- * the reset time.
53
+ * Called per request, so it must be cheap. This is the one place a real
54
+ * credential is read: the session is launched with `CREDENTIAL_PLACEHOLDER`
55
+ * and the swap happens here.
62
56
  */
63
57
  credentials: () => readonly string[];
64
58
  /**
@@ -33,10 +33,9 @@ import { credentialPool, readRefusal } from "./credentials.js";
33
33
  *
34
34
  * ## What this deliberately does not do
35
35
  *
36
- * It does not meter. 0.5.0 had a budget gate that estimated spend in dollars and
37
- * refused to start work over a cap; 0.6.0 deleted it. An estimate is a guess
38
- * about a bucket nobody can read, it only moved once per run, and the bucket
39
- * itself announces the answer exactly — see the docblock in `credentials.ts`.
36
+ * It does not meter, and a budget gate must not come back: an estimate of spend
37
+ * is a guess about a bucket nobody can read, moves only once per run, and the
38
+ * bucket announces the answer exactly — see the docblock in `credentials.ts`.
40
39
  *
41
40
  * And it does not, by default, **bound exfiltration**. The container holds the
42
41
  * checkout, and with no restriction configured it can send it anywhere. That is
@@ -241,8 +240,8 @@ export function claudeCodeEgress(config) {
241
240
  * The swap, and it is the whole point of the file.
242
241
  *
243
242
  * `authorization` is set and `x-api-key` removed because that is the shape
244
- * the sanctioned client sends — the Phase 0 capture showed `authorization:
245
- * Bearer …` with no `x-api-key` at all. Everything else is forwarded
243
+ * the sanctioned client sends: `authorization: Bearer …` with no
244
+ * `x-api-key` at all. Everything else is forwarded
246
245
  * untouched: the `anthropic-beta` list (which carries
247
246
  * `claude-code-20250219` and `oauth-2025-04-20`, almost certainly part of
248
247
  * what marks the request as coming from the client) and the `user-agent`
@@ -278,10 +277,10 @@ export function claudeCodeEgress(config) {
278
277
  /**
279
278
  * Capture the whole thing, every time.
280
279
  *
281
- * **No genuine subscription-exhaustion `429` has ever been observed through
282
- * this path** — the Phase 0 spike's 429s were the raw-API refusal, which is
283
- * a different response — so the rules in `readRefusal` are inferences from
284
- * the documented API rate limits. This log is the instrument that replaces
280
+ * **No genuine subscription-exhaustion `429` has been observed through this
281
+ * path** — a raw-API refusal is a different response — so the rules in
282
+ * `readRefusal` are inferences from the documented API rate limits. This log
283
+ * is the instrument that replaces
285
284
  * them with a fact, and it costs nothing: these statuses are rare, and the
286
285
  * body of one is a few hundred bytes.
287
286
  */
@@ -32,10 +32,46 @@ export interface ClaudeCodeUsage {
32
32
  cacheRead: number;
33
33
  cacheWrite: number;
34
34
  }
35
+ /**
36
+ * What the client knows about the subscription bucket it is drawing on.
37
+ *
38
+ * The one thing on this stream that is about the *deployment* rather than the
39
+ * run. This plugin's entry point describes the 5-hour and weekly limits as
40
+ * routed around rather than predicted, because the gateway only learns a bucket
41
+ * is empty when Anthropic refuses a request — this is the client saying so in
42
+ * advance, on every session.
43
+ *
44
+ * `resetsAt` is **seconds**, not milliseconds, which is the one field here it is
45
+ * possible to get silently wrong: a value handed to `Date` unconverted lands in
46
+ * January 1970, and a credential marked spent until then reads as usable.
47
+ */
48
+ export interface RateLimitInfo {
49
+ /**
50
+ * {@link RATE_LIMIT_OK} on a healthy bucket. Any other value is
51
+ * **unclassified** — no consumer should read one as exhaustion.
52
+ */
53
+ status: string;
54
+ /** Unix **seconds** at which this bucket refills. */
55
+ resetsAt?: number;
56
+ /** Which bucket — `five_hour`, and the weekly one. */
57
+ rateLimitType?: string;
58
+ /** Whether spend beyond the bucket is permitted, and why not. */
59
+ overageStatus?: string;
60
+ overageDisabledReason?: string;
61
+ }
35
62
  export type ClaudeCodeEvent = {
36
63
  kind: "init";
37
64
  sessionId: string;
38
65
  model?: string;
66
+ }
67
+ /**
68
+ * The subscription bucket, as the client sees it. Carried rather than acted
69
+ * on here — {@link describe} decides what a reader is told, and the caller
70
+ * decides what the credential pool is told.
71
+ */
72
+ | {
73
+ kind: "rateLimit";
74
+ info: RateLimitInfo;
39
75
  } | {
40
76
  kind: "assistant";
41
77
  text: string;
@@ -104,6 +140,17 @@ export interface ParsedStream {
104
140
  * `result` and `init` are never nested and are read unconditionally.
105
141
  */
106
142
  export declare function parseStream(buffer: string): ParsedStream;
143
+ /**
144
+ * The status a healthy bucket reports, and the only one ever observed.
145
+ *
146
+ * **Every other value is unclassified, not a refusal.** Nothing here knows what
147
+ * a non-`allowed` status means, and the one place that decides whether a
148
+ * credential is spent — `readRateLimitEvent` in this package's `credentials.ts`
149
+ * — recognises none of them, deliberately: retiring a working credential on a
150
+ * guess is the expensive half of that trade. What an unrecognised status earns
151
+ * is a note and a log, so somebody can name it.
152
+ */
153
+ export declare const RATE_LIMIT_OK = "allowed";
107
154
  /**
108
155
  * Turn events into progress notes the parent can post.
109
156
  *