@dynamicagents/plugins 0.11.0 → 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 (95) 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/client.d.ts +3 -2
  5. package/dist/arc-agi/game-tools.d.ts +5 -6
  6. package/dist/arc-agi/index.d.ts +6 -8
  7. package/dist/arc-agi/index.js +3 -3
  8. package/dist/arc-agi/main-agent.d.ts +5 -8
  9. package/dist/arc-agi/main-agent.js +5 -8
  10. package/dist/arc-agi/recipe.d.ts +17 -20
  11. package/dist/arc-agi/recipe.js +17 -20
  12. package/dist/arc-agi/soul.d.ts +16 -27
  13. package/dist/arc-agi/soul.js +16 -27
  14. package/dist/arc-agi/store.d.ts +4 -5
  15. package/dist/arc-agi/store.js +4 -5
  16. package/dist/arc-agi/tools.d.ts +1 -1
  17. package/dist/arc-agi/tools.js +26 -19
  18. package/dist/arc-agi/types.d.ts +6 -7
  19. package/dist/claude-code/config.d.ts +88 -11
  20. package/dist/claude-code/config.js +11 -2
  21. package/dist/claude-code/credentials.d.ts +21 -9
  22. package/dist/claude-code/credentials.js +43 -9
  23. package/dist/claude-code/egress.d.ts +7 -13
  24. package/dist/claude-code/egress.js +9 -10
  25. package/dist/claude-code/events.d.ts +47 -0
  26. package/dist/claude-code/events.js +96 -10
  27. package/dist/claude-code/index.d.ts +31 -13
  28. package/dist/claude-code/index.js +62 -15
  29. package/dist/claude-code/run.d.ts +135 -41
  30. package/dist/claude-code/run.js +366 -61
  31. package/dist/computer/advisory.d.ts +31 -18
  32. package/dist/computer/advisory.js +30 -5
  33. package/dist/computer/context.d.ts +23 -0
  34. package/dist/computer/context.js +225 -0
  35. package/dist/computer/file-lock.d.ts +5 -0
  36. package/dist/computer/file-lock.js +39 -0
  37. package/dist/computer/gate.d.ts +8 -8
  38. package/dist/computer/gate.js +7 -7
  39. package/dist/computer/host/ca-trust.d.ts +88 -0
  40. package/dist/computer/host/ca-trust.js +303 -0
  41. package/dist/computer/host/container-deps.d.ts +54 -0
  42. package/dist/computer/host/container-deps.js +224 -0
  43. package/dist/computer/host/container-fault.d.ts +38 -0
  44. package/dist/computer/host/container-fault.js +58 -0
  45. package/dist/computer/host/git-host.d.ts +62 -0
  46. package/dist/computer/host/git-host.js +156 -0
  47. package/dist/computer/host/git-identity.d.ts +57 -0
  48. package/dist/computer/host/git-identity.js +147 -0
  49. package/dist/computer/host/install-job.d.ts +121 -0
  50. package/dist/computer/host/install-job.js +766 -0
  51. package/dist/computer/host/sync.d.ts +111 -0
  52. package/dist/computer/host/sync.js +124 -0
  53. package/dist/computer/host/wake.d.ts +19 -0
  54. package/dist/computer/host/wake.js +10 -0
  55. package/dist/computer/host/workspace.d.ts +424 -0
  56. package/dist/computer/host/workspace.js +1258 -0
  57. package/dist/computer/index.d.ts +70 -50
  58. package/dist/computer/index.js +79 -767
  59. package/dist/computer/install.d.ts +8 -12
  60. package/dist/computer/install.js +6 -9
  61. package/dist/computer/paths.d.ts +21 -16
  62. package/dist/computer/paths.js +40 -38
  63. package/dist/computer/read.d.ts +18 -22
  64. package/dist/computer/read.js +24 -27
  65. package/dist/computer/render.d.ts +28 -0
  66. package/dist/computer/render.js +34 -1
  67. package/dist/computer/shell.d.ts +45 -0
  68. package/dist/computer/shell.js +80 -0
  69. package/dist/computer/tools-exec.d.ts +3 -0
  70. package/dist/computer/tools-exec.js +269 -0
  71. package/dist/computer/tools-file.d.ts +4 -0
  72. package/dist/computer/tools-file.js +124 -0
  73. package/dist/computer/tools-find.d.ts +3 -0
  74. package/dist/computer/tools-find.js +216 -0
  75. package/dist/recall/index.d.ts +9 -3
  76. package/dist/recall/index.js +22 -12
  77. package/dist/repo/checkout.d.ts +34 -4
  78. package/dist/repo/checkout.js +36 -1
  79. package/dist/repo/context.d.ts +109 -0
  80. package/dist/repo/context.js +530 -0
  81. package/dist/repo/index.d.ts +6 -29
  82. package/dist/repo/index.js +17 -1027
  83. package/dist/repo/tools-clone.d.ts +4 -0
  84. package/dist/repo/tools-clone.js +192 -0
  85. package/dist/repo/tools-forge.d.ts +4 -0
  86. package/dist/repo/tools-forge.js +179 -0
  87. package/dist/repo/tools-review.d.ts +3 -0
  88. package/dist/repo/tools-review.js +373 -0
  89. package/dist/repo/tools-worktree.d.ts +4 -0
  90. package/dist/repo/tools-worktree.js +221 -0
  91. package/dist/triage/index.d.ts +6 -1
  92. package/dist/triage/index.js +32 -23
  93. package/dist/workspace/index.d.ts +3 -3
  94. package/dist/workspace/index.js +3 -3
  95. package/package.json +21 -15
@@ -193,12 +193,83 @@ export function parseStream(buffer) {
193
193
  events.push({ kind: "result", result: readResult(event) });
194
194
  break;
195
195
  }
196
+ /**
197
+ * A top-level `type`, not a `system` subtype, so it is handled here and
198
+ * not in the subtype switch above.
199
+ *
200
+ * The placement is load-bearing rather than tidy: everything this switch
201
+ * does not name falls to `default`, which counts it as an unrecognised
202
+ * line and warns. A recognised shape parsed in the wrong place is
203
+ * therefore indistinguishable from a schema that moved — the drain reports
204
+ * a steady `skipped` on every session, and the reading it is dropping is
205
+ * the only free view of the subscription bucket anything here gets.
206
+ */
207
+ case "rate_limit_event": {
208
+ const info = readRateLimit(event);
209
+ if (!info) {
210
+ skip(trimmed);
211
+ break;
212
+ }
213
+ events.push({ kind: "rateLimit", info });
214
+ break;
215
+ }
196
216
  default:
197
217
  skip(trimmed);
198
218
  }
199
219
  }
200
220
  return { events, carry, skipped, nested, ...(sample ? { sample } : {}) };
201
221
  }
222
+ /**
223
+ * Read a `rate_limit_event` line, or nothing when it carries no status.
224
+ *
225
+ * `status` is the only required field: it is what decides whether anything is
226
+ * wrong, and a line without it says nothing this package can use — so it is
227
+ * skipped and sampled like any other unrecognised shape, rather than recorded as
228
+ * a bucket in an unknown state.
229
+ */
230
+ function readRateLimit(event) {
231
+ const info = asRecord(event.rate_limit_info);
232
+ const status = info && str(info.status);
233
+ if (!info || !status)
234
+ return undefined;
235
+ const resetsAt = info.resetsAt;
236
+ return {
237
+ status,
238
+ // Finite is not enough. `1e308` is finite, and seconds past the `Date` range
239
+ // make `toISOString()` throw — inside `describe`, inside the drain's parse
240
+ // loop, which would take a whole session down over one malformed vendor
241
+ // field. The contract everywhere else here is to drop a bad line, not to
242
+ // raise, so the check is "does this name a moment" rather than "is this a
243
+ // number".
244
+ ...(typeof resetsAt === "number" && isRealDate(resetsAt * 1000)
245
+ ? { resetsAt }
246
+ : {}),
247
+ ...(str(info.rateLimitType)
248
+ ? { rateLimitType: str(info.rateLimitType) }
249
+ : {}),
250
+ ...(str(info.overageStatus)
251
+ ? { overageStatus: str(info.overageStatus) }
252
+ : {}),
253
+ ...(str(info.overageDisabledReason)
254
+ ? { overageDisabledReason: str(info.overageDisabledReason) }
255
+ : {})
256
+ };
257
+ }
258
+ /** Whether a millisecond value is one `Date` can actually represent. */
259
+ function isRealDate(ms) {
260
+ return Number.isFinite(ms) && !Number.isNaN(new Date(ms).getTime());
261
+ }
262
+ /**
263
+ * The status a healthy bucket reports, and the only one ever observed.
264
+ *
265
+ * **Every other value is unclassified, not a refusal.** Nothing here knows what
266
+ * a non-`allowed` status means, and the one place that decides whether a
267
+ * credential is spent — `readRateLimitEvent` in this package's `credentials.ts`
268
+ * — recognises none of them, deliberately: retiring a working credential on a
269
+ * guess is the expensive half of that trade. What an unrecognised status earns
270
+ * is a note and a log, so somebody can name it.
271
+ */
272
+ export const RATE_LIMIT_OK = "allowed";
202
273
  /** Read a `result` line. Total by construction — a missing field reads as zero. */
203
274
  function readResult(event) {
204
275
  const apiErrorStatus = event.api_error_status;
@@ -222,15 +293,11 @@ function readResult(event) {
222
293
  permissionDenials: denials
223
294
  };
224
295
  }
225
- /** How much of one progress note the parent is shown. */
226
- const PROGRESS_MAX_CHARS = 240;
227
296
  /**
228
297
  * How much of an unrecognised line is kept for the logs.
229
298
  *
230
- * Shorter than a progress note and for a different reason: a note is read by a
231
- * model deciding what to do next, whereas this is read by a person deciding
232
- * whether a schema moved. The discriminating part of a stream-json line is its
233
- * `type` and `subtype`, both of which are at the front.
299
+ * Read by a person deciding whether a schema moved, and the discriminating part
300
+ * of a stream-json line is its `type` and `subtype`, both at the front.
234
301
  */
235
302
  const SAMPLE_MAX_CHARS = 200;
236
303
  /**
@@ -273,8 +340,10 @@ function describe(event) {
273
340
  // Deliberately narrower than silencing the whole channel: `denied` and
274
341
  // `retry` below stay, because they are the only evidence of a session that
275
342
  // is being refused or throttled.
343
+ //
344
+ // Never clipped. A plan or a report cut mid-sentence reads as complete.
276
345
  case "assistant":
277
- return event.text ? clip(event.text) : undefined;
346
+ return event.text || undefined;
278
347
  case "denied":
279
348
  // The one progress note that is more useful than the session's own
280
349
  // account of itself. A refused tool call is invisible in the transcript —
@@ -282,12 +351,29 @@ function describe(event) {
282
351
  // parent sees a subagent being resourceful and never learns it was fenced
283
352
  // in. Named tool first, because that is what distinguishes a broken
284
353
  // configuration from a deny rule doing its job.
285
- return `permission denied${event.tool ? ` for ${event.tool}` : ""}: ${clip(event.reason)}`;
354
+ return `permission denied${event.tool ? ` for ${event.tool}` : ""}: ${event.reason}`;
286
355
  case "retry":
287
356
  // Surfaced deliberately. This is what a budget refusal from the egress
288
357
  // gateway looks like from inside the container, and a run that ends
289
358
  // shortly afterwards is explained by it.
290
- return `retrying the model call: ${clip(event.detail)}`;
359
+ return `retrying the model call: ${event.detail}`;
360
+ case "rateLimit":
361
+ /**
362
+ * Silent while the bucket is fine, which is every line of a healthy
363
+ * session — one note per `rate_limit_event` would be the noisiest thing on
364
+ * the stream and would say "allowed" each time.
365
+ *
366
+ * The test is inverted deliberately: an unrecognised status produces a
367
+ * note rather than silence. The statuses are a vendor's and only
368
+ * {@link RATE_LIMIT_OK} has been seen, so a new one meaning "nearly out"
369
+ * should surface as prose somebody reads, not be assumed benign.
370
+ */
371
+ return event.info.status === RATE_LIMIT_OK
372
+ ? undefined
373
+ : `the ${event.info.rateLimitType ?? "subscription"} limit reports "${event.info.status}"` +
374
+ (event.info.resetsAt === undefined
375
+ ? ""
376
+ : ` until ${new Date(event.info.resetsAt * 1000).toISOString()}`);
291
377
  // `init` names a session id nobody outside this package can use, and
292
378
  // `result` is the terminal outcome the caller reports itself.
293
379
  case "init":
@@ -295,6 +381,6 @@ function describe(event) {
295
381
  return undefined;
296
382
  }
297
383
  }
298
- function clip(text, max = PROGRESS_MAX_CHARS) {
384
+ function clip(text, max) {
299
385
  return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
300
386
  }
@@ -1,6 +1,6 @@
1
1
  import type { AgentPlugin } from "@dynamicagents/core";
2
2
  import type { CredentialStore, Lead } from "./credentials.js";
3
- import { type DrainCursor, type DrainOutcome, type SessionRuntime } from "./run.js";
3
+ import { type DrainCursor, type DrainOutcome, type DrainOptions, type SessionRuntime } from "./run.js";
4
4
  import { type ClaudeCodeConfig } from "./config.js";
5
5
  /**
6
6
  * `@dynamicagents/plugins/claude-code` — subtasks that run the Claude Code CLI.
@@ -36,10 +36,9 @@ import { type ClaudeCodeConfig } from "./config.js";
36
36
  *
37
37
  * ## The 5-hour and weekly limits are routed around, not predicted
38
38
  *
39
- * 0.5.0 had a budget gate: an estimate of spend in dollars, and a refusal to
40
- * start work over a cap. 0.6.0 deletes it. The estimate was a guess about a
41
- * bucket nobody can read, and it only moved when a run *ended* — so it was never
42
- * a cap on spend, only a gate on starting.
39
+ * **No budget gate, and one must not come back.** An estimate of spend is a
40
+ * guess about a bucket nobody can read, and it only moves when a run *ends* — so
41
+ * it caps nothing, it only refuses to start.
43
42
  *
44
43
  * The gateway sees Anthropic's actual response, which is the one place the
45
44
  * bucket announces itself. So the credential is a **pool**: the first usable
@@ -71,9 +70,28 @@ export declare function claudeCodeSession(config: ClaudeCodeConfig): {
71
70
  * two sessions in one workspace under one id would spawn over each other —
72
71
  * see {@link execIdFor}.
73
72
  */
74
- start(runtime: SessionRuntime, subtaskId: string | number, prompt: string, dir: string): Promise<DrainOutcome>;
75
- /** Re-attach to a running session and drain one more window. */
76
- resume(runtime: SessionRuntime, cursor: DrainCursor): Promise<DrainOutcome>;
73
+ start(runtime: SessionRuntime, subtaskId: string | number, prompt: string, dir: string, sinks?: Pick<DrainOptions, "onProgress" | "onCheckpoint">): Promise<DrainOutcome>;
74
+ /**
75
+ * Re-attach to a running session and drain one more window.
76
+ *
77
+ * **A session whose container was replaced is reported, not thrown.** The
78
+ * attachment is the first thing to notice a replacement, and the caller is a
79
+ * chunk loop with a cursor it will happily keep presenting: a throw here
80
+ * retries the same dead id until the chunk allowance runs out, and the run
81
+ * ends on a stack trace that names neither the session nor the cause. A
82
+ * terminal outcome ends it at the first attempt instead, with the exit code
83
+ * the runtime uses for a killed process and an explanation on `stderr`,
84
+ * which is exactly the channel a session that died without a result line
85
+ * already reports through.
86
+ *
87
+ * **What it must not say is that starting over is safe.** A session is an
88
+ * agent: by the time its container went, it may have written files that
89
+ * reached the workspace, committed, pushed, or called something outside
90
+ * altogether. None of that is visible from here — only that the attachment
91
+ * is gone — so the honest report is what is known plus where to look, and
92
+ * the decision belongs to whoever can read the durable state.
93
+ */
94
+ resume(runtime: SessionRuntime, cursor: DrainCursor, sinks?: Pick<DrainOptions, "onProgress" | "onCheckpoint">): Promise<DrainOutcome>;
77
95
  /** Stop a session — `SIGTERM`, so its own process tree goes with it. */
78
96
  stop(runtime: SessionRuntime, subtaskId: string | number): Promise<void>;
79
97
  /**
@@ -116,12 +134,12 @@ export type ClaudeCodeSession = ReturnType<typeof claudeCodeSession>;
116
134
  export declare function claudeCode(config: ClaudeCodeConfig): AgentPlugin;
117
135
  export { ANTHROPIC_HOST, claudeCodeEgress } from "./egress.js";
118
136
  export type { EgressConfig } from "./egress.js";
119
- export { credentialPool, readRefusal } from "./credentials.js";
137
+ export { credentialPool, readRefusal, readRateLimitEvent } from "./credentials.js";
120
138
  export type { CredentialPool, CredentialPoolConfig, CredentialState, CredentialStore, Lead, Refusal } from "./credentials.js";
121
139
  export { buildLaunch, CLAUDE_EXEC_PREFIX, CREDENTIAL_PLACEHOLDER, execIdFor, freshCursor } from "./run.js";
122
- export type { DrainCursor, DrainOutcome, Launch, LaunchOptions, SessionRuntime } from "./run.js";
123
- export { parseStream, toProgress } from "./events.js";
124
- export type { ClaudeCodeEvent, ClaudeCodeResult, ClaudeCodeUsage } from "./events.js";
140
+ export type { DrainCursor, DrainOptions, DrainOutcome, Launch, LaunchOptions, SessionRuntime } from "./run.js";
141
+ export { parseStream, toProgress, RATE_LIMIT_OK } from "./events.js";
142
+ export type { ClaudeCodeEvent, ClaudeCodeResult, ClaudeCodeUsage, RateLimitInfo } from "./events.js";
125
143
  export { CLAUDE_CODE_CAPABILITY, CLAUDE_CODE_RECIPE, CLAUDE_CODE_SPEC, CLAUDE_CODE_TYPE, WORKSPACE_RUNTIME_KEY } from "./recipe.js";
126
144
  export { DEFAULT_PERMISSION_MODE, DEFAULT_TIMEOUT_MS, DEFAULT_WINDOW_MS } from "./config.js";
127
- export type { ClaudeCodeConfig, PermissionMode } from "./config.js";
145
+ export type { ClaudeCodeConfig, EffortLevel, PermissionMode } from "./config.js";
@@ -53,7 +53,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
53
53
  import { definePlugin, PLUGIN_CONTRACT_VERSION } from "@dynamicagents/core";
54
54
  import { claudeCodeEgress } from "./egress.js";
55
55
  import { credentialPool } from "./credentials.js";
56
- import { attachRun, drainRun, execIdFor, freshCursor, killRun, startRun } from "./run.js";
56
+ import { attachRun, drainRun, execIdFor, freshCursor, isExecLost, killRun, startRun } from "./run.js";
57
57
  import { DEFAULT_TIMEOUT_MS, DEFAULT_WINDOW_MS } from "./config.js";
58
58
  import { CLAUDE_CODE_SPEC, CLAUDE_CODE_TYPE, WORKSPACE_RUNTIME_KEY } from "./recipe.js";
59
59
  /**
@@ -90,10 +90,9 @@ import { CLAUDE_CODE_SPEC, CLAUDE_CODE_TYPE, WORKSPACE_RUNTIME_KEY } from "./rec
90
90
  *
91
91
  * ## The 5-hour and weekly limits are routed around, not predicted
92
92
  *
93
- * 0.5.0 had a budget gate: an estimate of spend in dollars, and a refusal to
94
- * start work over a cap. 0.6.0 deletes it. The estimate was a guess about a
95
- * bucket nobody can read, and it only moved when a run *ended* — so it was never
96
- * a cap on spend, only a gate on starting.
93
+ * **No budget gate, and one must not come back.** An estimate of spend is a
94
+ * guess about a bucket nobody can read, and it only moves when a run *ends* — so
95
+ * it caps nothing, it only refuses to start.
97
96
  *
98
97
  * The gateway sees Anthropic's actual response, which is the one place the
99
98
  * bucket announces itself. So the credential is a **pool**: the first usable
@@ -124,7 +123,7 @@ export function claudeCodeSession(config) {
124
123
  prompt,
125
124
  dir,
126
125
  ...(config.model ? { model: config.model } : {}),
127
- ...(config.maxTurns === undefined ? {} : { maxTurns: config.maxTurns }),
126
+ ...(config.effort ? { effort: config.effort } : {}),
128
127
  ...(config.maxSubagentDepth === undefined
129
128
  ? {}
130
129
  : { maxSubagentDepth: config.maxSubagentDepth }),
@@ -134,7 +133,8 @@ export function claudeCodeSession(config) {
134
133
  // Forwarded only when set, so `buildLaunch` owns the default in one place
135
134
  // rather than this line resolving it and the flag being written twice.
136
135
  ...(config.permissionMode ? { permissionMode: config.permissionMode } : {}),
137
- ...(config.env ? { env: config.env } : {})
136
+ ...(config.env ? { env: config.env } : {}),
137
+ ...(config.author ? { author: config.author } : {})
138
138
  });
139
139
  return {
140
140
  /**
@@ -144,7 +144,7 @@ export function claudeCodeSession(config) {
144
144
  * two sessions in one workspace under one id would spawn over each other —
145
145
  * see {@link execIdFor}.
146
146
  */
147
- async start(runtime, subtaskId, prompt, dir) {
147
+ async start(runtime, subtaskId, prompt, dir, sinks = {}) {
148
148
  const env_1 = { stack: [], error: void 0, hasError: false };
149
149
  try {
150
150
  const execId = execIdFor(subtaskId);
@@ -154,7 +154,10 @@ export function claudeCodeSession(config) {
154
154
  execId,
155
155
  timeoutMs
156
156
  }), false);
157
- return await drainRun(handle, freshCursor(execId), { windowMs });
157
+ return await drainRun(handle, freshCursor(execId), {
158
+ windowMs,
159
+ ...sinks
160
+ });
158
161
  }
159
162
  catch (e_1) {
160
163
  env_1.error = e_1;
@@ -164,12 +167,56 @@ export function claudeCodeSession(config) {
164
167
  __disposeResources(env_1);
165
168
  }
166
169
  },
167
- /** Re-attach to a running session and drain one more window. */
168
- async resume(runtime, cursor) {
170
+ /**
171
+ * Re-attach to a running session and drain one more window.
172
+ *
173
+ * **A session whose container was replaced is reported, not thrown.** The
174
+ * attachment is the first thing to notice a replacement, and the caller is a
175
+ * chunk loop with a cursor it will happily keep presenting: a throw here
176
+ * retries the same dead id until the chunk allowance runs out, and the run
177
+ * ends on a stack trace that names neither the session nor the cause. A
178
+ * terminal outcome ends it at the first attempt instead, with the exit code
179
+ * the runtime uses for a killed process and an explanation on `stderr`,
180
+ * which is exactly the channel a session that died without a result line
181
+ * already reports through.
182
+ *
183
+ * **What it must not say is that starting over is safe.** A session is an
184
+ * agent: by the time its container went, it may have written files that
185
+ * reached the workspace, committed, pushed, or called something outside
186
+ * altogether. None of that is visible from here — only that the attachment
187
+ * is gone — so the honest report is what is known plus where to look, and
188
+ * the decision belongs to whoever can read the durable state.
189
+ */
190
+ async resume(runtime, cursor, sinks = {}) {
169
191
  const env_2 = { stack: [], error: void 0, hasError: false };
170
192
  try {
171
- const handle = __addDisposableResource(env_2, await attachRun(runtime, cursor), false);
172
- return await drainRun(handle, cursor, { windowMs });
193
+ let handle;
194
+ try {
195
+ handle = await attachRun(runtime, cursor);
196
+ }
197
+ catch (err) {
198
+ if (!isExecLost(err))
199
+ throw err;
200
+ console.warn("[claude-code] the session's container was replaced", {
201
+ execId: cursor.execId,
202
+ err: String(err)
203
+ });
204
+ return {
205
+ done: true,
206
+ cursor,
207
+ progress: [],
208
+ exitCode: -1,
209
+ stderr: "the container holding this session was replaced, so the session " +
210
+ "was lost before it finished and cannot be re-attached. What it had " +
211
+ "already written to the workspace is still there, and anything it " +
212
+ "did outside the workspace — a commit, a push, a request — has " +
213
+ "already happened. Check the workspace and the branch before " +
214
+ "starting this subtask again, since a rerun repeats from the " +
215
+ "beginning."
216
+ };
217
+ }
218
+ const session = __addDisposableResource(env_2, handle, false);
219
+ return await drainRun(session, cursor, { windowMs, ...sinks });
173
220
  }
174
221
  catch (e_2) {
175
222
  env_2.error = e_2;
@@ -272,8 +319,8 @@ export function claudeCode(config) {
272
319
  });
273
320
  }
274
321
  export { ANTHROPIC_HOST, claudeCodeEgress } from "./egress.js";
275
- export { credentialPool, readRefusal } from "./credentials.js";
322
+ export { credentialPool, readRefusal, readRateLimitEvent } from "./credentials.js";
276
323
  export { buildLaunch, CLAUDE_EXEC_PREFIX, CREDENTIAL_PLACEHOLDER, execIdFor, freshCursor } from "./run.js";
277
- export { parseStream, toProgress } from "./events.js";
324
+ export { parseStream, toProgress, RATE_LIMIT_OK } from "./events.js";
278
325
  export { CLAUDE_CODE_CAPABILITY, CLAUDE_CODE_RECIPE, CLAUDE_CODE_SPEC, CLAUDE_CODE_TYPE, WORKSPACE_RUNTIME_KEY } from "./recipe.js";
279
326
  export { DEFAULT_PERMISSION_MODE, DEFAULT_TIMEOUT_MS, DEFAULT_WINDOW_MS } from "./config.js";
@@ -1,7 +1,7 @@
1
1
  import type { WorkspaceRuntimeExecHandle, WorkspaceRuntimeExecOptions, WorkspaceRuntimeGetOptions, WorkspaceRuntimeKillOptions } from "@cloudflare/computer";
2
2
  import type { ProgressEvent } from "@dynamicagents/core/subtasks";
3
- import { type ClaudeCodeResult } from "./events.js";
4
- import { type PermissionMode } from "./config.js";
3
+ import { type ClaudeCodeResult, type RateLimitInfo } from "./events.js";
4
+ import { type EffortLevel, type PermissionMode } from "./config.js";
5
5
  /**
6
6
  * Launching Claude Code in the workspace container, and draining it in windows.
7
7
  *
@@ -16,9 +16,8 @@ import { type PermissionMode } from "./config.js";
16
16
  *
17
17
  * The run is spawned under an exec id and left running. Each chunk re-attaches,
18
18
  * drains for a bounded window, and returns. That shape is not a preference: a
19
- * drain owned by an RPC that returns in milliseconds gets disposed mid-command,
20
- * which is exactly how the dependency install used to die halfway through
21
- * `npm ci`.
19
+ * drain owned by an RPC that returns in milliseconds gets disposed mid-command
20
+ * — the same way a dependency install dies halfway through `npm ci`.
22
21
  *
23
22
  * ## The cursor
24
23
  *
@@ -69,11 +68,10 @@ export declare function execIdFor(subtaskId: string | number): string;
69
68
  /**
70
69
  * What the container is given instead of a credential.
71
70
  *
72
- * Claude Code does not validate it locally — proven in the Phase 0c spike, where
73
- * a run with this exact value succeeded and the proxy log confirmed the
74
- * container only ever sent the placeholder. The egress gateway swaps in the real
75
- * credential on the way out, so a `postinstall` script that dumps the
76
- * environment learns this and nothing else.
71
+ * Claude Code does not validate it locally: a run with this exact value
72
+ * succeeds, and the container only ever sends the placeholder. The egress
73
+ * gateway swaps in the real credential on the way out, so a `postinstall` script
74
+ * that dumps the environment learns this and nothing else.
77
75
  *
78
76
  * Shaped like a real token deliberately: something that looks obviously fake
79
77
  * invites a future reader to "fix" it by putting the real one there.
@@ -85,16 +83,16 @@ export interface LaunchOptions {
85
83
  /** Where the checkout is. The session runs with this as its cwd. */
86
84
  dir: string;
87
85
  model?: string;
88
- /** Ceiling on the *outer* session's turns. Advisory; the budget gate is not. */
89
- maxTurns?: number;
90
86
  /**
91
- * Caps on Claude Code's own subagent tree.
87
+ * How hard the model thinks, per turn. Unset, the model's own default.
92
88
  *
93
- * These steer rather than enforce, and the distinction matters: the inner
94
- * tree is invisible to Dynamic Agents' scheduler and unreachable by its
95
- * cancellation sweep, so a cap it chooses to ignore has no backstop. What
96
- * actually bounds the spend is the egress gateway, which every inner call
97
- * also crosses.
89
+ * See {@link file://./config.ts ClaudeCodeConfig.effort} for what a level
90
+ * costs, and why an unrecognised one is worse here than a rejected one.
91
+ */
92
+ effort?: EffortLevel;
93
+ /**
94
+ * Caps on Claude Code's own subagent tree — advisory, for the reason
95
+ * {@link file://./config.ts ClaudeCodeConfig.maxSubagentDepth} gives.
98
96
  */
99
97
  maxSubagentDepth?: number;
100
98
  maxConcurrentSubagents?: number;
@@ -114,6 +112,14 @@ export interface LaunchOptions {
114
112
  * than trusted to the reader of that sentence.
115
113
  */
116
114
  env?: Record<string, string>;
115
+ /**
116
+ * Who this session's commits are attributed to — see
117
+ * {@link file://./config.ts ClaudeCodeConfig.author}.
118
+ */
119
+ author?: {
120
+ name: string;
121
+ email: string;
122
+ };
117
123
  }
118
124
  export interface Launch {
119
125
  command: string;
@@ -133,27 +139,20 @@ export interface Launch {
133
139
  * container is already an arbitrary-code-execution environment by design — the
134
140
  * install runs the repo's `postinstall`, the agent runs its test suite. Stripping
135
141
  * one door while the others stand open buys nothing and costs the agent its
136
- * context. (§4 of the design plan, cancelled 2026-08-21.)
142
+ * context.
137
143
  *
138
- * No `ANTHROPIC_BASE_URL` either, and that one is a genuine simplification over
139
- * the spike: `http-gateway` egress intercepts transparently, so the client talks
140
- * to the real hostname and the gateway sees it. Nothing has to be told to use a
141
- * proxy, which means nothing in the container can be told *not* to.
144
+ * No `ANTHROPIC_BASE_URL` either: `http-gateway` egress intercepts
145
+ * transparently, so the client talks to the real hostname and the gateway sees
146
+ * it. Nothing has to be told to use a proxy, which means nothing in the
147
+ * container can be told *not* to.
142
148
  *
143
149
  * ## `--permission-mode`, and the root guard behind it
144
150
  *
145
- * `-p` is headless. There is no terminal, so there is nobody to answer a
146
- * permission prompt — and Claude Code's headless path does not wait for one, it
147
- * **auto-denies**. Left unset the mode is `default`, and `default` gates Write,
148
- * Edit and every Bash command. A session in that state reads the repository
149
- * perfectly and cannot change one byte of it, while reporting prose that reads
150
- * like considered reluctance rather than a blocked tool. That cost this
151
- * deployment a day of "the container is fixed but nothing lands".
152
- *
153
- * So the mode is passed explicitly, and {@link DEFAULT_PERMISSION_MODE} is
154
- * `bypassPermissions` — see
155
- * {@link file://./config.ts ClaudeCodeConfig.permissionMode} for why the
156
- * permissive value is the correct default here rather than a concession.
151
+ * The mode is always passed explicitly, never left to the CLI's `default` — see
152
+ * {@link file://./config.ts ClaudeCodeConfig.permissionMode} for what an
153
+ * unset mode does to a headless session, and why
154
+ * {@link DEFAULT_PERMISSION_MODE} is the permissive value rather than a
155
+ * concession.
157
156
  *
158
157
  * **`IS_SANDBOX=1` ships in the same branch as the flag, and separating the two
159
158
  * breaks a session harder than passing no flag at all.** The container runs as
@@ -221,10 +220,22 @@ export type DrainOutcome = {
221
220
  done: false;
222
221
  cursor: DrainCursor;
223
222
  progress: ProgressEvent[];
223
+ /**
224
+ * The bucket as the client reported it **during this window**, if it did.
225
+ *
226
+ * Deliberately not carried on the cursor the way `result` is. A caller
227
+ * acts on this against whichever credential is leading *now*, so a reading
228
+ * replayed on every later chunk would let one window's observation retire
229
+ * a credential that was not even in use when it was taken. Absent means
230
+ * "this window learned nothing", which is the honest answer.
231
+ */
232
+ rateLimit?: RateLimitInfo;
224
233
  } | {
225
234
  done: true;
226
235
  cursor: DrainCursor;
227
236
  progress: ProgressEvent[];
237
+ /** The bucket as reported **in this window**. See the `done: false` arm. */
238
+ rateLimit?: RateLimitInfo;
228
239
  exitCode: number;
229
240
  /** Absent when the process died without ever emitting a `result` line. */
230
241
  result?: ClaudeCodeResult;
@@ -239,14 +250,69 @@ export interface DrainOptions {
239
250
  /**
240
251
  * How long this chunk may block before checkpointing and yielding.
241
252
  *
242
- * Must stay comfortably under the Workflow step timeout, and it is what stops
243
- * a run burning its whole chunk allowance in seconds: a drain that returned
244
- * the moment it had nothing to read would exhaust `MAX_CHUNKS_PER_BRANCH`
245
- * before the session finished thinking.
253
+ * **This is the coder path's soft limit**, justified the same way core's
254
+ * `CHUNK_SOFT_MS` is — by the step timeout — but checked as a deadline on
255
+ * reading the session's stdout stream rather than between turns, because a
256
+ * `claude -p` session runs its own loop inside the container and there are no
257
+ * core-visible turns to stop between. The stream is the only synchronisation
258
+ * point there is. {@link file://./config.ts ClaudeCodeConfig.windowMs} holds
259
+ * how the default is sized against that timeout, what the headroom it leaves
260
+ * is for, and why that headroom is an expectation rather than a guarantee.
261
+ *
262
+ * It is also what stops a run burning its whole chunk allowance in seconds: a
263
+ * drain that returned the moment it had nothing to read would exhaust
264
+ * `MAX_CHUNKS_PER_BRANCH` before the session finished thinking.
265
+ *
266
+ * **It is not the reporting interval**, and reading it as one is the mistake
267
+ * {@link DrainOptions.onProgress} exists to remove: a session that finishes
268
+ * inside a single window reaches no boundary at all, so a caller with nothing
269
+ * but the outcome learns everything at once, once the work is over.
246
270
  */
247
271
  windowMs: number;
272
+ /**
273
+ * Called with each note as it is parsed, rather than with all of them when the
274
+ * window ends.
275
+ *
276
+ * Notes are still returned on the outcome as well, so a caller that posts from
277
+ * here must drop what it is handed back, or the same note is posted twice. The
278
+ * keys are positional, so the gatekeeper would dedupe it, but paying for the
279
+ * second post to be discarded is not a plan.
280
+ *
281
+ * **Never awaited inside the read loop.** A post is a signed round trip to the
282
+ * gatekeeper — measured at ~700 ms — and awaiting one per note would stall
283
+ * reading the container's stream for as long as the session is talkative.
284
+ * Calls are chained instead, so they stay in order, and the chain is settled
285
+ * before the drain returns.
286
+ */
287
+ onProgress?: (event: ProgressEvent) => void | Promise<void>;
288
+ /**
289
+ * Called with a cursor that is safe to persist, no more often than
290
+ * {@link CHECKPOINT_MIN_MS}.
291
+ *
292
+ * **Only meaningful alongside `onProgress`, and that coupling is the whole
293
+ * point.** A caller normally commits the cursor after the drain returns,
294
+ * because a cursor written ahead of consuming events would skip events a retry
295
+ * never saw. A cursor offered here names a position whose notes have *already
296
+ * been handed to the sink*, so resuming from it loses nothing a person saw —
297
+ * which is only true because the sink posted them.
298
+ *
299
+ * Without it, a chunk that dies mid-window resumes from the last committed
300
+ * position, which is wherever the previous window ended. One production run
301
+ * lost six and a half minutes of a session that way and re-derived it by
302
+ * replaying the whole stream.
303
+ */
304
+ onCheckpoint?: (cursor: DrainCursor) => void | Promise<void>;
248
305
  now?: () => number;
249
306
  }
307
+ /**
308
+ * Whether a thrown value means the session this cursor names is gone for good.
309
+ *
310
+ * `EEXEC_LOST` is the runtime saying the container that held the execution was
311
+ * replaced. The session died with it, and no attachment will ever find it — so
312
+ * unlike `EEXEC_BUSY`, which is a live session reached the wrong way, there is
313
+ * nothing to recover and retrying only spends chunks discovering that again.
314
+ */
315
+ export declare function isExecLost(err: unknown): boolean;
250
316
  /**
251
317
  * Start a session, detached.
252
318
  *
@@ -257,13 +323,41 @@ export interface DrainOptions {
257
323
  * still running (`EEXEC_BUSY`). Without this the retry throws, every later retry
258
324
  * throws the same way, and a perfectly healthy session in the container becomes
259
325
  * unreachable.
326
+ *
327
+ * **The sync stays on the default, and `defer` is not an option here.** A
328
+ * deferred exec skips the post-command pull entirely and settles the outcome
329
+ * from the cursor instead — and its `cancel` reads the stream to the end first,
330
+ * so ending a window would block until the session exits rather than yielding
331
+ * the chunk. Both halves are exactly what this drain is built not to do.
260
332
  */
261
333
  export declare function startRun(runtime: SessionRuntime, options: LaunchOptions & {
262
334
  execId: string;
263
335
  timeoutMs: number;
264
336
  }): Promise<WorkspaceRuntimeExecHandle<"utf8">>;
265
- /** Re-attach to a session this isolate did not start. */
266
- export declare function attachRun(runtime: SessionRuntime, cursor: DrainCursor): Promise<WorkspaceRuntimeExecHandle<"utf8">>;
337
+ /** The clock and the wait, injectable so a spec can drive the bound. */
338
+ export interface AttachOptions {
339
+ now?: () => number;
340
+ wait?: (ms: number) => Promise<void>;
341
+ }
342
+ /**
343
+ * Re-attach to a session this isolate did not start.
344
+ *
345
+ * **Waits out a subscriber the previous window has not released yet**, and that
346
+ * wait is the point of this function rather than a refinement of it. The
347
+ * alternative is not "fail fast" — it is spending a Workflow retry to discover a
348
+ * condition that clears on its own in a second, when a chunk step's retries are
349
+ * what a deploy, a severed stub and a network drop have to be covered out of.
350
+ * Core sizes that budget and says why; what matters here is that this must not
351
+ * be charged to it. A deployment lost a twenty-one-minute session exactly that
352
+ * way — consecutive attach races used the retries up, and the redeploy that
353
+ * followed had no attempt left to be retried on. See {@link isExecSubscribed}.
354
+ *
355
+ * Everything else is rethrown on the first look, `EEXEC_LOST` above all: it is
356
+ * the runtime saying the container was replaced, `resume` turns it into a report
357
+ * for the model, and a retry loop would sit on it for {@link ATTACH_MAX_MS}
358
+ * learning nothing.
359
+ */
360
+ export declare function attachRun(runtime: SessionRuntime, cursor: DrainCursor, options?: AttachOptions): Promise<WorkspaceRuntimeExecHandle<"utf8">>;
267
361
  /**
268
362
  * Drain a session for one bounded window.
269
363
  *