@dynamicagents/plugins 0.13.0 → 0.15.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 (141) hide show
  1. package/README.md +14 -14
  2. package/dist/claude-code/cards.d.ts +4 -0
  3. package/dist/claude-code/cards.js +22 -0
  4. package/dist/claude-code/config.d.ts +10 -83
  5. package/dist/claude-code/config.js +0 -2
  6. package/dist/claude-code/credentials.d.ts +32 -50
  7. package/dist/claude-code/credentials.js +115 -74
  8. package/dist/claude-code/daemon/index.d.ts +24 -0
  9. package/dist/claude-code/daemon/index.js +203 -0
  10. package/dist/claude-code/egress.d.ts +35 -117
  11. package/dist/claude-code/egress.js +80 -325
  12. package/dist/claude-code/events.d.ts +14 -17
  13. package/dist/claude-code/events.js +19 -15
  14. package/dist/claude-code/harness-wire.d.ts +45 -0
  15. package/dist/claude-code/harness-wire.js +47 -0
  16. package/dist/claude-code/index.d.ts +19 -29
  17. package/dist/claude-code/index.js +17 -25
  18. package/dist/claude-code/language-model.d.ts +19 -0
  19. package/dist/claude-code/language-model.js +71 -0
  20. package/dist/claude-code/model.d.ts +77 -70
  21. package/dist/claude-code/model.js +328 -304
  22. package/dist/claude-code/placeholder.d.ts +12 -0
  23. package/dist/claude-code/placeholder.js +12 -0
  24. package/dist/claude-code/spec.d.ts +4 -4
  25. package/dist/claude-code/spec.js +4 -4
  26. package/dist/claude-code/verified.d.ts +1 -1
  27. package/dist/claude-code/verified.js +1 -1
  28. package/dist/computer/client.d.ts +88 -0
  29. package/dist/computer/client.js +6 -0
  30. package/dist/computer/context.d.ts +7 -7
  31. package/dist/computer/context.js +14 -18
  32. package/dist/computer/gate.d.ts +4 -23
  33. package/dist/computer/gate.js +1 -37
  34. package/dist/computer/index.d.ts +44 -43
  35. package/dist/computer/index.js +29 -50
  36. package/dist/computer/paths.d.ts +5 -27
  37. package/dist/computer/paths.js +7 -30
  38. package/dist/computer/proxy.d.ts +17 -31
  39. package/dist/computer/proxy.js +9 -44
  40. package/dist/computer/render.d.ts +3 -40
  41. package/dist/computer/render.js +3 -36
  42. package/dist/computer/sandbox.d.ts +55 -0
  43. package/dist/computer/sandbox.js +236 -0
  44. package/dist/computer/shell.d.ts +36 -0
  45. package/dist/computer/shell.js +41 -0
  46. package/dist/computer/tools-exec.js +3 -9
  47. package/dist/computer/tools-file.d.ts +1 -1
  48. package/dist/computer/tools-file.js +1 -1
  49. package/dist/{repo → repo-github}/checkout.d.ts +3 -4
  50. package/dist/{repo → repo-github}/checkout.js +3 -4
  51. package/dist/{repo → repo-github}/context.d.ts +14 -32
  52. package/dist/{repo → repo-github}/context.js +46 -81
  53. package/dist/{repo → repo-github}/index.d.ts +30 -13
  54. package/dist/{repo → repo-github}/index.js +25 -6
  55. package/dist/repo-github/mcp.d.ts +95 -0
  56. package/dist/repo-github/mcp.js +107 -0
  57. package/dist/repo-github/sandbox-egress.d.ts +68 -0
  58. package/dist/repo-github/sandbox-egress.js +272 -0
  59. package/dist/repo-github/sandbox-git.d.ts +30 -0
  60. package/dist/repo-github/sandbox-git.js +113 -0
  61. package/dist/{repo → repo-github}/tools-checks.js +43 -4
  62. package/dist/{repo → repo-github}/tools-clone.js +2 -21
  63. package/dist/{repo → repo-github}/tools-pr.d.ts +0 -1
  64. package/dist/{repo → repo-github}/tools-pr.js +85 -20
  65. package/dist/{repo → repo-github}/tools-review.d.ts +6 -0
  66. package/dist/{repo → repo-github}/tools-review.js +62 -176
  67. package/dist/{repo → repo-github}/url.d.ts +11 -0
  68. package/dist/{repo → repo-github}/url.js +12 -1
  69. package/dist/{workspace → sandbox}/advisory.d.ts +16 -35
  70. package/dist/{workspace → sandbox}/advisory.js +2 -29
  71. package/dist/sandbox/agent-exec.d.ts +31 -0
  72. package/dist/sandbox/agent-exec.js +62 -0
  73. package/dist/sandbox/ca-trust.d.ts +9 -0
  74. package/dist/sandbox/ca-trust.js +21 -0
  75. package/dist/sandbox/constants.d.ts +22 -0
  76. package/dist/sandbox/constants.js +22 -0
  77. package/dist/sandbox/daemon/composite.d.ts +9 -0
  78. package/dist/sandbox/daemon/composite.js +35 -0
  79. package/dist/sandbox/daemon/index.d.ts +47 -0
  80. package/dist/sandbox/daemon/index.js +116 -0
  81. package/dist/sandbox/egress.d.ts +81 -0
  82. package/dist/sandbox/egress.js +109 -0
  83. package/dist/sandbox/exec.d.ts +57 -0
  84. package/dist/sandbox/exec.js +52 -0
  85. package/dist/sandbox/format.d.ts +2 -0
  86. package/dist/sandbox/format.js +7 -0
  87. package/dist/sandbox/header.d.ts +50 -0
  88. package/dist/sandbox/header.js +94 -0
  89. package/dist/sandbox/index.d.ts +17 -0
  90. package/dist/sandbox/index.js +17 -0
  91. package/dist/{workspace/install.d.ts → sandbox/install-plan.d.ts} +10 -1
  92. package/dist/{workspace/install.js → sandbox/install-plan.js} +22 -4
  93. package/dist/sandbox/name.d.ts +27 -0
  94. package/dist/sandbox/name.js +32 -0
  95. package/dist/sandbox/object.d.ts +244 -0
  96. package/dist/sandbox/object.js +1139 -0
  97. package/dist/scratch/index.d.ts +1 -9
  98. package/dist/scratch/index.js +2 -18
  99. package/package.json +26 -22
  100. package/dist/claude-code/github.d.ts +0 -39
  101. package/dist/claude-code/github.js +0 -82
  102. package/dist/claude-code/run.d.ts +0 -474
  103. package/dist/claude-code/run.js +0 -901
  104. package/dist/claude-code/session.d.ts +0 -131
  105. package/dist/claude-code/session.js +0 -285
  106. package/dist/workspace/ca-trust.d.ts +0 -95
  107. package/dist/workspace/ca-trust.js +0 -312
  108. package/dist/workspace/container-deps.d.ts +0 -54
  109. package/dist/workspace/container-deps.js +0 -224
  110. package/dist/workspace/container-fault.d.ts +0 -38
  111. package/dist/workspace/container-fault.js +0 -73
  112. package/dist/workspace/exec.d.ts +0 -94
  113. package/dist/workspace/exec.js +0 -153
  114. package/dist/workspace/format.d.ts +0 -36
  115. package/dist/workspace/format.js +0 -57
  116. package/dist/workspace/git-host.d.ts +0 -79
  117. package/dist/workspace/git-host.js +0 -226
  118. package/dist/workspace/git-identity.d.ts +0 -57
  119. package/dist/workspace/git-identity.js +0 -147
  120. package/dist/workspace/index.d.ts +0 -49
  121. package/dist/workspace/index.js +0 -58
  122. package/dist/workspace/install-job.d.ts +0 -121
  123. package/dist/workspace/install-job.js +0 -866
  124. package/dist/workspace/object.d.ts +0 -502
  125. package/dist/workspace/object.js +0 -1427
  126. package/dist/workspace/open.d.ts +0 -95
  127. package/dist/workspace/open.js +0 -63
  128. package/dist/workspace/read.d.ts +0 -16
  129. package/dist/workspace/read.js +0 -22
  130. package/dist/workspace/shell.d.ts +0 -45
  131. package/dist/workspace/shell.js +0 -80
  132. package/dist/workspace/sync.d.ts +0 -111
  133. package/dist/workspace/sync.js +0 -124
  134. package/dist/workspace/wake.d.ts +0 -19
  135. package/dist/workspace/wake.js +0 -10
  136. /package/dist/{repo → repo-github}/tools-checks.d.ts +0 -0
  137. /package/dist/{repo → repo-github}/tools-clone.d.ts +0 -0
  138. /package/dist/{repo → repo-github}/tools-worktree.d.ts +0 -0
  139. /package/dist/{repo → repo-github}/tools-worktree.js +0 -0
  140. /package/dist/{repo → repo-github}/tools-worktrees.d.ts +0 -0
  141. /package/dist/{repo → repo-github}/tools-worktrees.js +0 -0
@@ -1,342 +1,366 @@
1
- var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
- if (value !== null && value !== void 0) {
3
- if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
- var dispose, inner;
5
- if (async) {
6
- if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
- dispose = value[Symbol.asyncDispose];
8
- }
9
- if (dispose === void 0) {
10
- if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
- dispose = value[Symbol.dispose];
12
- if (async) inner = dispose;
13
- }
14
- if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
- if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
- env.stack.push({ value: value, dispose: dispose, async: async });
17
- }
18
- else if (async) {
19
- env.stack.push({ async: true });
20
- }
21
- return value;
22
- };
23
- var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
- return function (env) {
25
- function fail(e) {
26
- env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
- env.hasError = true;
28
- }
29
- var r, s = 0;
30
- function next() {
31
- while (r = env.stack.pop()) {
32
- try {
33
- if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
- if (r.dispose) {
35
- var result = r.dispose.call(r.value);
36
- if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
- }
38
- else s |= 1;
39
- }
40
- catch (e) {
41
- fail(e);
42
- }
43
- }
44
- if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
- if (env.hasError) throw env.error;
46
- }
47
- return next();
48
- };
49
- })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
- var e = new Error(message);
51
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
- });
53
1
  import { isPlatformTransientError } from "agents";
54
- import { claudeCodeSession, requireCredentials } from "./session.js";
55
- import { execIdFor, followUpExecIdFor } from "./run.js";
56
- /** Zeroed: the session's tokens are the CLI's context, not this conversation's. */
57
- const USAGE = {
58
- inputTokens: {
59
- total: 0,
60
- noCache: 0,
61
- cacheRead: 0,
62
- cacheWrite: 0
63
- },
64
- outputTokens: { total: 0, text: 0, reasoning: 0 }
65
- };
2
+ import { SANDBOX_CONTRACT } from "../sandbox/constants.js";
3
+ import { withRunHeader } from "../sandbox/header.js";
4
+ import { containerLostCard, lostCard } from "./cards.js";
5
+ import { decodeNote, decodeRunEnd, decodeSessionId } from "./harness-wire.js";
6
+ import { lastUserText, reportingModel } from "./language-model.js";
7
+ /**
8
+ * Three hours: a ceiling on a session that has stopped making progress, not a
9
+ * budget for one that is. No timer in the sandbox races it, since the
10
+ * container's idle stop never fires while an operation is open.
11
+ */
12
+ export const DEFAULT_TIMEOUT_MS = 3 * 60 * 60_000;
13
+ /** What a session is told when it resumes in a new container. */
14
+ export const CONTAINER_LOST_PROMPT = "The container this session was running in was lost, and the session has " +
15
+ "resumed in a new one. Its disk is as of the sandbox's last snapshot, so " +
16
+ "files written since may be gone, and nothing you started is still running. " +
17
+ "Check the checkout's state before carrying on with the task.";
18
+ /**
19
+ * Attempts after the first. A turn that kills its container every time — an
20
+ * install that runs it out of memory — would otherwise be resubmitted forever.
21
+ */
22
+ const LOST_RESUBMITS = 3;
23
+ /** Why an operation settled that a new attempt may get past. */
24
+ const LOST_REASONS = new Set(["container_lost", "container_unavailable"]);
25
+ /** Polls in a row a note may fail to file before it is given up on. */
26
+ const NOTE_TRIES = 3;
27
+ /** The sandbox's own ceiling on one poll's wait. */
28
+ const POLL_WAIT_MS = 20_000;
29
+ /**
30
+ * Failed calls in a row before one is thrown. A sandbox reset costs one call;
31
+ * an isolate this run's object has been replaced on fails every call, and is
32
+ * left to the restart's recovery of the turn.
33
+ */
34
+ const TRANSIENT_TRIES = 8;
35
+ const TRANSIENT_BACKOFF_MS = 1_000;
66
36
  export function claudeCodeModel(options) {
67
- requireCredentials(options.config);
68
37
  const { storage, runId } = options;
69
- const session = claudeCodeSession(options.config);
38
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
70
39
  const key = (name) => `claude-code:${runId}:${name}`;
71
40
  const KEYS = {
72
41
  brief: key("brief"),
73
- cursor: key("cursor"),
74
42
  session: key("session"),
75
43
  followUp: key("follow-up"),
76
44
  followUpEnd: key("follow-up-end"),
77
- report: key("report")
45
+ report: key("report"),
46
+ turn: (step) => key(`turn:${step}`),
47
+ /**
48
+ * Where a run that started in a workspace container kept its place. Such a
49
+ * run can still be resumed by a task waiting on input, and the guard below
50
+ * is what stops it being briefed again.
51
+ */
52
+ cursor: key("cursor")
78
53
  };
79
54
  const run = async (call) => {
80
- const env_1 = { stack: [], error: void 0, hasError: false };
81
- try {
82
- // The once-guarantee: a run that already reported says so again.
83
- const reported = await storage.get(KEYS.report);
84
- if (reported !== undefined)
85
- return reported;
86
- const signal = call.abortSignal;
87
- // Opened again after a cut stream — see `drain`. Whichever is current is
88
- // released when the run ends.
89
- let workspace = await options.workspace();
90
- const _release = __addDisposableResource(env_1, { [Symbol.dispose]: () => workspace[Symbol.dispose]() }, false);
91
- /** The exec being drained, which leads each note's key. */
92
- let current = "";
93
- /**
94
- * The handle already recorded, so one id is not announced twice over — and
95
- * set only once the host has taken it, so a record that failed is tried
96
- * again at the next checkpoint rather than given up on.
97
- */
98
- let announced = "";
99
- /**
100
- * Tell the host the session's handle, and never fail the run over it.
101
- *
102
- * Not on the note chain: a note is one line of a transcript, and this is the
103
- * only way back to the conversation. It is awaited where it is called so a
104
- * handle is recorded before whatever comes next can lose it.
105
- */
106
- const announce = async (record) => {
107
- const sink = options.onSession;
108
- if (!sink)
109
- return;
55
+ const reported = await storage.get(KEYS.report);
56
+ if (reported !== undefined)
57
+ return reported;
58
+ if ((await storage.get(KEYS.cursor)) !== undefined &&
59
+ (await storage.get(KEYS.turn("session"))) === undefined) {
60
+ throw new Error("claude-code: this run's session started in a workspace container, " +
61
+ "which a sandbox cannot attach to, so it ended when the deployment moved to sandboxes");
62
+ }
63
+ const signal = call.abortSignal;
64
+ /** Set only once the host has taken the handle, so a failed record is tried again. */
65
+ let announced = "";
66
+ const announce = async (record) => {
67
+ const sink = options.onSession;
68
+ if (!sink || (record.end === undefined && announced === record.sessionId))
69
+ return;
70
+ try {
71
+ await sink(record);
72
+ announced = record.sessionId;
73
+ }
74
+ catch (err) {
75
+ console.warn("[claude-code] the session's handle was not recorded", {
76
+ runId,
77
+ sessionId: record.sessionId,
78
+ err: String(err)
79
+ });
80
+ }
81
+ };
82
+ const file = async (noteKey, text, detail) => {
83
+ await options.note(noteKey, text, detail).catch((err) => console.warn("[claude-code] a note was not filed", {
84
+ runId,
85
+ key: noteKey,
86
+ err: String(err)
87
+ }));
88
+ };
89
+ /**
90
+ * One sandbox call, through a new stub after each transient failure. The
91
+ * abort ends the wait, not the call: an RPC cannot be taken back.
92
+ */
93
+ const rpc = async (fn) => {
94
+ for (let tries = 1;; tries++) {
110
95
  try {
111
- await sink(record);
112
- announced = record.sessionId;
96
+ return await untilAborted(fn(options.sandbox()), signal);
113
97
  }
114
98
  catch (err) {
115
- console.warn("[claude-code] the session's handle was not recorded", {
99
+ if (signal?.aborted ||
100
+ !isPlatformTransientError(err) ||
101
+ tries >= TRANSIENT_TRIES)
102
+ throw err;
103
+ console.warn("[claude-code] a sandbox call failed — retrying", {
116
104
  runId,
117
- sessionId: record.sessionId,
105
+ tries,
118
106
  err: String(err)
119
107
  });
108
+ await untilAborted(sleep(TRANSIENT_BACKOFF_MS * tries), signal);
120
109
  }
110
+ }
111
+ };
112
+ /** Stop the session, file its end, and end the run with `reason`. */
113
+ const stop = async (operationId, reason) => {
114
+ await options
115
+ .sandbox()
116
+ .stopRun(runId)
117
+ .catch((err) => console.warn("[claude-code] could not stop the session", {
118
+ runId,
119
+ err: String(err)
120
+ }));
121
+ const card = lostCard(reason instanceof Error ? reason.message : String(reason));
122
+ await file(`${operationId}:lost`, card.text, card.detail);
123
+ throw reason;
124
+ };
125
+ /**
126
+ * One step of the run, to its end: the brief's session, or the follow-up.
127
+ * Both count against one limit, from the session's first submission.
128
+ */
129
+ const turn = async (step, prompt, header) => {
130
+ const origin = step === "session"
131
+ ? undefined
132
+ : (await storage.get(KEYS.turn("session")))?.startedAt;
133
+ const stored = await storage.get(KEYS.turn(step));
134
+ let state = stored ?? {
135
+ attempt: 0,
136
+ startedAt: origin ?? Date.now(),
137
+ from: 0
121
138
  };
122
- /**
123
- * Each note, filed as it is parsed, then the cursor behind it. Keyed on the
124
- * exec as well as the position: the parent's transcript is one per task and
125
- * dedupes on the key, so a run's notes must not collide with another run's
126
- * — or with its own follow-up's, which counts from zero again.
127
- */
128
- const sinks = {
129
- onProgress: (note) => options.note(`${current}:${note.key}`, note.text, note.detail),
130
- // The drain offers a cursor the moment the session id appears, which is
131
- // what makes this the earliest the handle can be recorded — seconds in,
132
- // rather than at the end a killed session never reaches.
133
- onCheckpoint: async (cursor) => {
134
- await storage.put(KEYS.cursor, cursor);
135
- if (cursor.sessionId && cursor.sessionId !== announced) {
136
- await announce({ sessionId: cursor.sessionId });
137
- }
138
- },
139
- ...(signal ? { signal } : {})
139
+ const save = async (next) => {
140
+ state = next;
141
+ await storage.put(KEYS.turn(step), state);
140
142
  };
141
- /**
142
- * Drain one exec to its end, starting it or re-attaching from the cursor.
143
- *
144
- * **A cut stream is re-attached here, not failed.** The session is still
145
- * running, and a failed run is stopped — the one outcome that loses it. The
146
- * retry opens the workspace afresh, since the stub the stream came over may
147
- * not survive the cut. Two attempts in a row that end in a cut with no
148
- * checkpoint stored fail the run as before: a stream that breaks every time
149
- * it opens is not a blip.
150
- */
151
- const drain = async (execId, begin) => {
152
- current = execId;
153
- let outcome;
154
- let stalled = false;
155
- while (!outcome) {
156
- const cursor = await storage.get(KEYS.cursor);
143
+ await save(state);
144
+ const resume = options.resume;
145
+ try {
146
+ await rpc((sandbox) => sandbox.createRun(runId, {
147
+ cwd: options.dir,
148
+ ...(resume
149
+ ? resume.fork
150
+ ? { forkOf: resume.run }
151
+ : { continues: resume.run }
152
+ : {})
153
+ }));
154
+ }
155
+ catch (err) {
156
+ // Recovering, the last invocation's turn may still be running: an
157
+ // abort stops it. A step that never began has nothing to stop.
158
+ if (signal?.aborted && stored)
159
+ await stop(`${runId}:${step}:a${state.attempt}`, signal.reason ?? err);
160
+ throw err;
161
+ }
162
+ for (;;) {
163
+ const operationId = `${runId}:${step}:a${state.attempt}`;
164
+ // A resubmission that reached the CLI is told what happened, in the
165
+ // conversation it already had; one that never did is the step again.
166
+ const { fork: _fork, ...resumed } = header;
167
+ const text = state.attempt > 0 && state.sessionId
168
+ ? withRunHeader(resumed, CONTAINER_LOST_PROMPT)
169
+ : withRunHeader(header, prompt);
170
+ try {
171
+ await rpc((sandbox) => sandbox.submit(runId, text, operationId));
172
+ }
173
+ catch (err) {
174
+ // The submission may have landed: an abort stops what it started.
175
+ if (signal?.aborted)
176
+ await stop(operationId, signal.reason ?? err);
177
+ throw err;
178
+ }
179
+ let result;
180
+ /** Polls in a row that stopped at one unfiled note. */
181
+ let stuck = 0;
182
+ while (!result) {
183
+ if (signal?.aborted)
184
+ await stop(operationId, signal.reason ?? new Error("the session was stopped"));
185
+ if (Date.now() - state.startedAt > timeoutMs) {
186
+ await stop(operationId, new Error(`the session ran past its limit of ${Math.round(timeoutMs / 60_000)} minutes`));
187
+ }
188
+ let polled;
157
189
  try {
158
- outcome =
159
- cursor?.execId === execId
160
- ? await session.resume(workspace.runtime, cursor, sinks)
161
- : await begin(workspace.runtime);
190
+ polled = await rpc((sandbox) => sandbox.poll(runId, operationId, POLL_WAIT_MS, state.from));
162
191
  }
163
192
  catch (err) {
164
- const after = await storage.get(KEYS.cursor);
165
- const moved = after?.execId === execId &&
166
- (cursor?.execId !== execId || after.seq > cursor.seq);
167
- if (!isStreamCut(err) || signal?.aborted || (stalled && !moved))
168
- throw err;
169
- stalled = !moved;
170
- console.warn("[claude-code] the session's stream was cut — re-attaching", {
171
- execId,
172
- err: String(err)
173
- });
174
- const fresh = await options.workspace();
175
- workspace[Symbol.dispose]();
176
- workspace = fresh;
193
+ if (signal?.aborted)
194
+ continue;
195
+ throw err;
177
196
  }
197
+ if (polled.contract !== SANDBOX_CONTRACT) {
198
+ throw new Error(`claude-code: the sandbox answered with contract ${polled.contract} and this run expects ${SANDBOX_CONTRACT}: ` +
199
+ "a sandbox kept busy on older code through a deploy answered it");
200
+ }
201
+ // In order, and the position never past a note that was not filed:
202
+ // the next poll sends it again. One that never files is given up on
203
+ // after a few polls, so the session's end still arrives.
204
+ let filedTo = polled.next;
205
+ for (const [i, message] of polled.messages.entries()) {
206
+ const taken = await take(message);
207
+ if (taken.failed && stuck < NOTE_TRIES) {
208
+ stuck += 1;
209
+ filedTo = state.from + i;
210
+ break;
211
+ }
212
+ if (taken.failed) {
213
+ console.warn("[claude-code] a note was given up on", {
214
+ runId,
215
+ key: message.id
216
+ });
217
+ }
218
+ stuck = 0;
219
+ if (taken.sessionId !== undefined) {
220
+ if (taken.sessionId !== state.sessionId)
221
+ await save({ ...state, sessionId: taken.sessionId });
222
+ await announce({ sessionId: taken.sessionId });
223
+ }
224
+ }
225
+ if (filedTo !== state.from)
226
+ await save({ ...state, from: filedTo });
227
+ if (filedTo === polled.next)
228
+ result = polled.result;
178
229
  }
179
- await storage.put(KEYS.cursor, outcome.cursor);
180
- if (!outcome.done) {
181
- // Only the signal stops a drain short of the end: the turn was
182
- // cancelled, so the session goes with it.
183
- await session.stop(workspace.runtime, runId).catch((err) => console.warn("[claude-code] could not stop a cancelled session", {
184
- runId,
185
- err: String(err)
186
- }));
187
- throw signal?.reason ?? new Error("the session was stopped");
230
+ if (result.status === "done") {
231
+ const end = decodeRunEnd(result.text);
232
+ if (!end?.result) {
233
+ throw new Error(`claude-code: the sandbox settled the session with no end it could read: ${String(result.text).slice(0, 200)}`);
234
+ }
235
+ // No code is a process a signal ended.
236
+ return sessionEnd(end, end.exitCode ?? -1, state.sessionId);
188
237
  }
189
- return {
190
- exitCode: outcome.exitCode,
191
- ...(outcome.result ? { result: outcome.result } : {}),
192
- ...(outcome.stderr ? { stderr: outcome.stderr } : {}),
193
- ...(outcome.rateLimit ? { rateLimit: outcome.rateLimit } : {}),
194
- ...(outcome.cursor.sessionId
195
- ? { sessionId: outcome.cursor.sessionId }
196
- : {})
197
- };
198
- };
199
- let ended = await storage.get(KEYS.session);
200
- if (!ended) {
201
- let prompt = await storage.get(KEYS.brief);
202
- if (prompt === undefined) {
203
- const task = lastUserText(call.prompt);
204
- prompt = options.brief ? await options.brief(task) : task;
205
- await storage.put(KEYS.brief, prompt);
238
+ const reason = result.reason ?? "";
239
+ const end = decodeRunEnd(reason);
240
+ if (end)
241
+ return sessionEnd(end, end.exitCode ?? 1, state.sessionId);
242
+ if (signal?.aborted)
243
+ await stop(operationId, signal.reason ?? new Error("the session was stopped"));
244
+ if (!LOST_REASONS.has(reason) || state.attempt >= LOST_RESUBMITS) {
245
+ const card = lostCard(reason || "the sandbox gave no reason");
246
+ await file(`${operationId}:lost`, card.text, card.detail);
247
+ throw new Error(`claude-code: the sandbox ended the session: ${reason || "no reason given"}`);
206
248
  }
207
- const brief = prompt;
208
- ended = await drain(execIdFor(runId), (runtime) => session.start(runtime, runId, {
209
- prompt: brief,
210
- dir: options.dir,
211
- ...(options.jsonSchema ? { jsonSchema: options.jsonSchema } : {}),
212
- ...(options.permissionMode
213
- ? { permissionMode: options.permissionMode }
214
- : {}),
215
- ...(options.resume ? { resume: options.resume } : {})
216
- }, sinks));
217
- await storage.put(KEYS.session, ended);
249
+ console.warn("[claude-code] the session's container was lost — resuming", {
250
+ runId,
251
+ operationId,
252
+ reason
253
+ });
254
+ const card = containerLostCard(reason);
255
+ await file(`${operationId}:container-lost`, card.text, card.detail);
256
+ await save({ ...state, attempt: state.attempt + 1, from: 0 });
218
257
  }
219
- // After the end is stored, so a turn that dies here does not report an end
220
- // the run has not kept — and outside the guard above, so a turn cut between
221
- // storing the end and telling the host tells it on the next attempt. Telling
222
- // it twice is harmless: its record is keyed on the id, see `SessionRecord`.
223
- if (ended.sessionId)
224
- await announce({ sessionId: ended.sessionId, end: ended });
225
- let followUpEnd = await storage.get(KEYS.followUpEnd);
226
- if (!followUpEnd && options.followUp) {
227
- let asked = await storage.get(KEYS.followUp);
228
- if (!asked) {
229
- const prompt = await options.followUp(ended);
230
- asked = prompt ? { prompt } : {};
231
- await storage.put(KEYS.followUp, asked);
232
- }
233
- // The cursor's id as well as the result's: a session that was stopped
234
- // before it reported still has a conversation worth one more turn.
235
- const sessionId = ended.result?.sessionId ?? ended.sessionId;
236
- if (asked.prompt && sessionId) {
237
- const prompt = asked.prompt;
238
- followUpEnd = await drain(followUpExecIdFor(runId), (runtime) => session.followUp(runtime, runId, {
239
- sessionId,
240
- prompt,
241
- dir: options.dir,
242
- ...(options.permissionMode
243
- ? { permissionMode: options.permissionMode }
244
- : {})
245
- }, sinks));
246
- await storage.put(KEYS.followUpEnd, followUpEnd);
247
- }
258
+ };
259
+ /** File one polled message's note, or read the session id it carries. */
260
+ const take = async (message) => {
261
+ const part = message.message.parts[0];
262
+ if (part?.type !== "text")
263
+ return {};
264
+ const sessionId = decodeSessionId(part.text);
265
+ if (sessionId !== undefined)
266
+ return { sessionId };
267
+ const note = decodeNote(part.text);
268
+ if (!note)
269
+ return {};
270
+ // Keyed on the message, which is the operation and the note's position:
271
+ // the parent's transcript dedupes on the key, so a refiled poll is free.
272
+ try {
273
+ const at = message.message.createdAt;
274
+ await options.note(message.id, note.text, note.detail, at > 0 ? at : undefined);
275
+ return {};
276
+ }
277
+ catch (err) {
278
+ console.warn("[claude-code] a note was not filed — polling it again", {
279
+ runId,
280
+ key: message.id,
281
+ err: String(err)
282
+ });
283
+ return { failed: true };
284
+ }
285
+ };
286
+ const header = {
287
+ cwd: options.dir,
288
+ ...(options.permissionMode === "plan"
289
+ ? { mode: "plan" }
290
+ : { mode: "build" }),
291
+ ...(options.schema !== undefined ? { schema: options.schema } : {}),
292
+ ...(options.effort !== undefined ? { effort: options.effort } : {}),
293
+ ...(options.model !== undefined ? { model: options.model } : {})
294
+ };
295
+ let ended = await storage.get(KEYS.session);
296
+ if (!ended) {
297
+ let prompt = await storage.get(KEYS.brief);
298
+ if (prompt === undefined) {
299
+ const task = lastUserText(call.prompt);
300
+ prompt = options.brief ? await options.brief(task) : task;
301
+ await storage.put(KEYS.brief, prompt);
248
302
  }
249
- const text = await options.report({
250
- session: ended,
251
- ...(followUpEnd ? { followUp: followUpEnd } : {})
303
+ ended = await turn("session", prompt, {
304
+ ...header,
305
+ ...(options.resume?.fork ? { fork: true } : {})
252
306
  });
253
- await storage.put(KEYS.report, text);
254
- return text;
307
+ await storage.put(KEYS.session, ended);
255
308
  }
256
- catch (e_1) {
257
- env_1.error = e_1;
258
- env_1.hasError = true;
259
- }
260
- finally {
261
- __disposeResources(env_1);
309
+ if (ended.sessionId)
310
+ await announce({ sessionId: ended.sessionId, end: ended });
311
+ let followUpEnd = await storage.get(KEYS.followUpEnd);
312
+ if (!followUpEnd && options.followUp) {
313
+ let asked = await storage.get(KEYS.followUp);
314
+ if (!asked) {
315
+ const prompt = await options.followUp(ended);
316
+ asked = prompt ? { prompt } : {};
317
+ await storage.put(KEYS.followUp, asked);
318
+ }
319
+ // A session with no conversation has nothing to follow up. Its schema
320
+ // was the session's answer, not the follow-up's — except a plan's, which
321
+ // answers to one every turn.
322
+ if (asked.prompt && (ended.result?.sessionId ?? ended.sessionId)) {
323
+ const { schema: _schema, ...unbound } = header;
324
+ const followUpHeader = header.mode === "plan" ? header : unbound;
325
+ followUpEnd = await turn("follow-up", asked.prompt, followUpHeader);
326
+ await storage.put(KEYS.followUpEnd, followUpEnd);
327
+ }
262
328
  }
329
+ const text = await options.report({
330
+ session: ended,
331
+ ...(followUpEnd ? { followUp: followUpEnd } : {})
332
+ });
333
+ await storage.put(KEYS.report, text);
334
+ return text;
263
335
  };
336
+ return reportingModel(options.model ?? "claude-code", run);
337
+ }
338
+ function sessionEnd(end, exitCode, seen) {
339
+ const sessionId = end.sessionId ?? seen;
264
340
  return {
265
- specificationVersion: "v3",
266
- provider: "claude-code",
267
- modelId: options.config.model ?? "claude-code",
268
- supportedUrls: {},
269
- async doStream(call) {
270
- // The stream is returned at once and the session runs inside it, so
271
- // nothing waits on a promise for the length of a session.
272
- const stream = new ReadableStream({
273
- async start(controller) {
274
- controller.enqueue({ type: "stream-start", warnings: [] });
275
- try {
276
- const text = await run(call);
277
- controller.enqueue({ type: "text-start", id: "report" });
278
- controller.enqueue({
279
- type: "text-delta",
280
- id: "report",
281
- delta: text
282
- });
283
- controller.enqueue({ type: "text-end", id: "report" });
284
- controller.enqueue({
285
- type: "finish",
286
- usage: USAGE,
287
- finishReason: { unified: "stop", raw: undefined }
288
- });
289
- controller.close();
290
- }
291
- catch (err) {
292
- controller.error(err);
293
- }
294
- }
295
- });
296
- return { stream };
297
- },
298
- async doGenerate(call) {
299
- const text = await run(call);
300
- return {
301
- content: [{ type: "text", text }],
302
- finishReason: { unified: "stop", raw: undefined },
303
- usage: USAGE,
304
- warnings: []
305
- };
306
- }
341
+ exitCode,
342
+ ...(end.result ? { result: end.result } : {}),
343
+ ...(end.stderr ? { stderr: end.stderr } : {}),
344
+ ...(end.rateLimit ? { rateLimit: end.rateLimit } : {}),
345
+ ...(sessionId ? { sessionId } : {})
307
346
  };
308
347
  }
309
- /**
310
- * Whether a drain failed because the transport under a live session gave way.
311
- *
312
- * An exec's events reach the sub-agent as a stream over RPC, relayed by the
313
- * workspace object from the container's own RPC session — so a drop anywhere on
314
- * that path cuts the stream while the session runs on. Measured: a container's
315
- * session dropped after 21 minutes and was attached again within a second, the
316
- * process untouched. The message is workerd's for a stream whose far side went
317
- * away, and it carries no code.
318
- */
319
- function isStreamCut(err) {
320
- if (isPlatformTransientError(err))
321
- return true;
322
- for (let e = err; e instanceof Error; e = e.cause) {
323
- if (e.message.includes("ReadableStream received over RPC disconnected"))
324
- return true;
325
- }
326
- return false;
348
+ function sleep(ms) {
349
+ return new Promise((resolve) => setTimeout(resolve, ms));
327
350
  }
328
- /**
329
- * The task: the last user message's text. On a continued turn the prompt ends
330
- * with the interrupted assistant message, so the last message is not it.
331
- */
332
- function lastUserText(prompt) {
333
- for (let i = prompt.length - 1; i >= 0; i--) {
334
- const message = prompt[i];
335
- if (message.role !== "user")
336
- continue;
337
- return message.content
338
- .map((part) => (part.type === "text" ? part.text : ""))
339
- .join("");
340
- }
341
- throw new Error("claude-code: the run has no task — no user message");
351
+ /** `promise`, or the abort's reason as soon as `signal` fires. */
352
+ function untilAborted(promise, signal) {
353
+ if (!signal)
354
+ return promise;
355
+ return new Promise((resolve, reject) => {
356
+ const onAbort = () => reject(signal.reason);
357
+ if (signal.aborted) {
358
+ onAbort();
359
+ return;
360
+ }
361
+ signal.addEventListener("abort", onAbort, { once: true });
362
+ promise
363
+ .then(resolve, reject)
364
+ .finally(() => signal.removeEventListener("abort", onAbort));
365
+ });
342
366
  }