@dynamicagents/plugins 0.14.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.
- package/README.md +14 -14
- package/dist/claude-code/cards.d.ts +2 -0
- package/dist/claude-code/cards.js +11 -0
- package/dist/claude-code/config.d.ts +10 -83
- package/dist/claude-code/config.js +0 -2
- package/dist/claude-code/credentials.d.ts +32 -50
- package/dist/claude-code/credentials.js +115 -74
- package/dist/claude-code/daemon/index.d.ts +24 -0
- package/dist/claude-code/daemon/index.js +203 -0
- package/dist/claude-code/egress.d.ts +35 -117
- package/dist/claude-code/egress.js +80 -325
- package/dist/claude-code/events.d.ts +14 -17
- package/dist/claude-code/events.js +19 -15
- package/dist/claude-code/harness-wire.d.ts +45 -0
- package/dist/claude-code/harness-wire.js +47 -0
- package/dist/claude-code/index.d.ts +19 -29
- package/dist/claude-code/index.js +17 -25
- package/dist/claude-code/language-model.d.ts +19 -0
- package/dist/claude-code/language-model.js +71 -0
- package/dist/claude-code/model.d.ts +77 -70
- package/dist/claude-code/model.js +327 -327
- package/dist/claude-code/placeholder.d.ts +12 -0
- package/dist/claude-code/placeholder.js +12 -0
- package/dist/claude-code/spec.d.ts +4 -4
- package/dist/claude-code/spec.js +4 -4
- package/dist/computer/client.d.ts +88 -0
- package/dist/computer/client.js +6 -0
- package/dist/computer/context.d.ts +7 -7
- package/dist/computer/context.js +14 -18
- package/dist/computer/gate.d.ts +4 -23
- package/dist/computer/gate.js +1 -37
- package/dist/computer/index.d.ts +44 -43
- package/dist/computer/index.js +29 -50
- package/dist/computer/paths.d.ts +5 -27
- package/dist/computer/paths.js +7 -30
- package/dist/computer/proxy.d.ts +17 -31
- package/dist/computer/proxy.js +9 -44
- package/dist/computer/render.d.ts +3 -40
- package/dist/computer/render.js +3 -36
- package/dist/computer/sandbox.d.ts +55 -0
- package/dist/computer/sandbox.js +236 -0
- package/dist/computer/shell.d.ts +36 -0
- package/dist/computer/shell.js +41 -0
- package/dist/computer/tools-exec.js +3 -9
- package/dist/computer/tools-file.d.ts +1 -1
- package/dist/computer/tools-file.js +1 -1
- package/dist/{repo → repo-github}/checkout.d.ts +3 -4
- package/dist/{repo → repo-github}/checkout.js +3 -4
- package/dist/{repo → repo-github}/context.d.ts +14 -32
- package/dist/{repo → repo-github}/context.js +46 -81
- package/dist/{repo → repo-github}/index.d.ts +30 -13
- package/dist/{repo → repo-github}/index.js +25 -6
- package/dist/repo-github/mcp.d.ts +95 -0
- package/dist/repo-github/mcp.js +107 -0
- package/dist/repo-github/sandbox-egress.d.ts +68 -0
- package/dist/repo-github/sandbox-egress.js +272 -0
- package/dist/repo-github/sandbox-git.d.ts +30 -0
- package/dist/repo-github/sandbox-git.js +113 -0
- package/dist/{repo → repo-github}/tools-checks.js +43 -4
- package/dist/{repo → repo-github}/tools-clone.js +2 -21
- package/dist/{repo → repo-github}/tools-pr.d.ts +0 -1
- package/dist/{repo → repo-github}/tools-pr.js +85 -20
- package/dist/{repo → repo-github}/tools-review.d.ts +6 -0
- package/dist/{repo → repo-github}/tools-review.js +62 -176
- package/dist/{repo → repo-github}/url.d.ts +11 -0
- package/dist/{repo → repo-github}/url.js +12 -1
- package/dist/{workspace → sandbox}/advisory.d.ts +16 -35
- package/dist/{workspace → sandbox}/advisory.js +2 -29
- package/dist/sandbox/agent-exec.d.ts +31 -0
- package/dist/sandbox/agent-exec.js +62 -0
- package/dist/sandbox/ca-trust.d.ts +9 -0
- package/dist/sandbox/ca-trust.js +21 -0
- package/dist/sandbox/constants.d.ts +22 -0
- package/dist/sandbox/constants.js +22 -0
- package/dist/sandbox/daemon/composite.d.ts +9 -0
- package/dist/sandbox/daemon/composite.js +35 -0
- package/dist/sandbox/daemon/index.d.ts +47 -0
- package/dist/sandbox/daemon/index.js +116 -0
- package/dist/sandbox/egress.d.ts +81 -0
- package/dist/sandbox/egress.js +109 -0
- package/dist/sandbox/exec.d.ts +57 -0
- package/dist/sandbox/exec.js +52 -0
- package/dist/sandbox/format.d.ts +2 -0
- package/dist/sandbox/format.js +7 -0
- package/dist/sandbox/header.d.ts +50 -0
- package/dist/sandbox/header.js +94 -0
- package/dist/sandbox/index.d.ts +17 -0
- package/dist/sandbox/index.js +17 -0
- package/dist/{workspace/install.d.ts → sandbox/install-plan.d.ts} +2 -2
- package/dist/{workspace/install.js → sandbox/install-plan.js} +2 -2
- package/dist/sandbox/name.d.ts +27 -0
- package/dist/sandbox/name.js +32 -0
- package/dist/sandbox/object.d.ts +244 -0
- package/dist/sandbox/object.js +1139 -0
- package/dist/scratch/index.d.ts +1 -9
- package/dist/scratch/index.js +2 -18
- package/package.json +22 -18
- package/dist/claude-code/github.d.ts +0 -39
- package/dist/claude-code/github.js +0 -82
- package/dist/claude-code/run.d.ts +0 -491
- package/dist/claude-code/run.js +0 -913
- package/dist/claude-code/session.d.ts +0 -131
- package/dist/claude-code/session.js +0 -288
- package/dist/workspace/ca-trust.d.ts +0 -95
- package/dist/workspace/ca-trust.js +0 -312
- package/dist/workspace/container-deps.d.ts +0 -58
- package/dist/workspace/container-deps.js +0 -249
- package/dist/workspace/container-fault.d.ts +0 -38
- package/dist/workspace/container-fault.js +0 -73
- package/dist/workspace/deps-snapshot.d.ts +0 -110
- package/dist/workspace/deps-snapshot.js +0 -424
- package/dist/workspace/exec.d.ts +0 -94
- package/dist/workspace/exec.js +0 -153
- package/dist/workspace/format.d.ts +0 -36
- package/dist/workspace/format.js +0 -57
- package/dist/workspace/git-host.d.ts +0 -79
- package/dist/workspace/git-host.js +0 -226
- package/dist/workspace/git-identity.d.ts +0 -57
- package/dist/workspace/git-identity.js +0 -147
- package/dist/workspace/index.d.ts +0 -50
- package/dist/workspace/index.js +0 -58
- package/dist/workspace/install-job.d.ts +0 -170
- package/dist/workspace/install-job.js +0 -934
- package/dist/workspace/object.d.ts +0 -522
- package/dist/workspace/object.js +0 -1541
- package/dist/workspace/open.d.ts +0 -95
- package/dist/workspace/open.js +0 -63
- package/dist/workspace/read.d.ts +0 -16
- package/dist/workspace/read.js +0 -22
- package/dist/workspace/shell.d.ts +0 -45
- package/dist/workspace/shell.js +0 -80
- package/dist/workspace/sync.d.ts +0 -111
- package/dist/workspace/sync.js +0 -124
- package/dist/workspace/wake.d.ts +0 -21
- package/dist/workspace/wake.js +0 -10
- /package/dist/{repo → repo-github}/tools-checks.d.ts +0 -0
- /package/dist/{repo → repo-github}/tools-clone.d.ts +0 -0
- /package/dist/{repo → repo-github}/tools-worktree.d.ts +0 -0
- /package/dist/{repo → repo-github}/tools-worktree.js +0 -0
- /package/dist/{repo → repo-github}/tools-worktrees.d.ts +0 -0
- /package/dist/{repo → repo-github}/tools-worktrees.js +0 -0
|
@@ -1,366 +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 {
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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;
|
|
67
36
|
export function claudeCodeModel(options) {
|
|
68
|
-
requireCredentials(options.config);
|
|
69
37
|
const { storage, runId } = options;
|
|
70
|
-
const
|
|
38
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
71
39
|
const key = (name) => `claude-code:${runId}:${name}`;
|
|
72
40
|
const KEYS = {
|
|
73
41
|
brief: key("brief"),
|
|
74
|
-
cursor: key("cursor"),
|
|
75
42
|
session: key("session"),
|
|
76
43
|
followUp: key("follow-up"),
|
|
77
44
|
followUpEnd: key("follow-up-end"),
|
|
78
|
-
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")
|
|
79
53
|
};
|
|
80
54
|
const run = async (call) => {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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++) {
|
|
111
95
|
try {
|
|
112
|
-
await
|
|
113
|
-
announced = record.sessionId;
|
|
96
|
+
return await untilAborted(fn(options.sandbox()), signal);
|
|
114
97
|
}
|
|
115
98
|
catch (err) {
|
|
116
|
-
|
|
99
|
+
if (signal?.aborted ||
|
|
100
|
+
!isPlatformTransientError(err) ||
|
|
101
|
+
tries >= TRANSIENT_TRIES)
|
|
102
|
+
throw err;
|
|
103
|
+
console.warn("[claude-code] a sandbox call failed — retrying", {
|
|
117
104
|
runId,
|
|
118
|
-
|
|
105
|
+
tries,
|
|
119
106
|
err: String(err)
|
|
120
107
|
});
|
|
108
|
+
await untilAborted(sleep(TRANSIENT_BACKOFF_MS * tries), signal);
|
|
121
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
|
|
122
138
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
* dedupes on the key, so a run's notes must not collide with another run's
|
|
127
|
-
* — or with its own follow-up's, which counts from zero again.
|
|
128
|
-
*/
|
|
129
|
-
const sinks = {
|
|
130
|
-
onProgress: (note) => options.note(`${current}:${note.key}`, note.text, note.detail),
|
|
131
|
-
// The drain offers a cursor the moment the session id appears, which is
|
|
132
|
-
// what makes this the earliest the handle can be recorded — seconds in,
|
|
133
|
-
// rather than at the end a killed session never reaches.
|
|
134
|
-
onCheckpoint: async (cursor) => {
|
|
135
|
-
await storage.put(KEYS.cursor, cursor);
|
|
136
|
-
if (cursor.sessionId && cursor.sessionId !== announced) {
|
|
137
|
-
await announce({ sessionId: cursor.sessionId });
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
...(signal ? { signal } : {})
|
|
139
|
+
const save = async (next) => {
|
|
140
|
+
state = next;
|
|
141
|
+
await storage.put(KEYS.turn(step), state);
|
|
141
142
|
};
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const fileLost = async (execId, reason) => {
|
|
153
|
-
const card = lostCard(reason instanceof Error ? reason.message : String(reason));
|
|
154
|
-
await options
|
|
155
|
-
.note(`${execId}:lost`, card.text, card.detail)
|
|
156
|
-
.catch((err) => console.warn("[claude-code] a lost session's end was not filed", {
|
|
157
|
-
execId,
|
|
158
|
-
err: String(err)
|
|
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
|
+
: {})
|
|
159
153
|
}));
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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;
|
|
177
189
|
try {
|
|
178
|
-
|
|
179
|
-
cursor?.execId === execId
|
|
180
|
-
? await session.resume(workspace.runtime, cursor, sinks)
|
|
181
|
-
: await begin(workspace.runtime);
|
|
190
|
+
polled = await rpc((sandbox) => sandbox.poll(runId, operationId, POLL_WAIT_MS, state.from));
|
|
182
191
|
}
|
|
183
192
|
catch (err) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
stalled = !moved;
|
|
192
|
-
console.warn("[claude-code] the session's stream was cut — re-attaching", {
|
|
193
|
-
execId,
|
|
194
|
-
err: String(err)
|
|
195
|
-
});
|
|
196
|
-
const fresh = await options.workspace();
|
|
197
|
-
workspace[Symbol.dispose]();
|
|
198
|
-
workspace = fresh;
|
|
193
|
+
if (signal?.aborted)
|
|
194
|
+
continue;
|
|
195
|
+
throw err;
|
|
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");
|
|
199
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;
|
|
200
229
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}));
|
|
209
|
-
const reason = signal?.reason ?? new Error("the session was stopped");
|
|
210
|
-
await fileLost(execId, reason);
|
|
211
|
-
throw reason;
|
|
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);
|
|
212
237
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
let ended = await storage.get(KEYS.session);
|
|
224
|
-
if (!ended) {
|
|
225
|
-
let prompt = await storage.get(KEYS.brief);
|
|
226
|
-
if (prompt === undefined) {
|
|
227
|
-
const task = lastUserText(call.prompt);
|
|
228
|
-
prompt = options.brief ? await options.brief(task) : task;
|
|
229
|
-
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"}`);
|
|
230
248
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
...(options.resume ? { resume: options.resume } : {})
|
|
240
|
-
}, sinks));
|
|
241
|
-
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 });
|
|
242
257
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}, sinks));
|
|
270
|
-
await storage.put(KEYS.followUpEnd, followUpEnd);
|
|
271
|
-
}
|
|
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 };
|
|
272
284
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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);
|
|
302
|
+
}
|
|
303
|
+
ended = await turn("session", prompt, {
|
|
304
|
+
...header,
|
|
305
|
+
...(options.resume?.fork ? { fork: true } : {})
|
|
276
306
|
});
|
|
277
|
-
await storage.put(KEYS.
|
|
278
|
-
return text;
|
|
279
|
-
}
|
|
280
|
-
catch (e_1) {
|
|
281
|
-
env_1.error = e_1;
|
|
282
|
-
env_1.hasError = true;
|
|
307
|
+
await storage.put(KEYS.session, ended);
|
|
283
308
|
}
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
}
|
|
286
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;
|
|
287
335
|
};
|
|
336
|
+
return reportingModel(options.model ?? "claude-code", run);
|
|
337
|
+
}
|
|
338
|
+
function sessionEnd(end, exitCode, seen) {
|
|
339
|
+
const sessionId = end.sessionId ?? seen;
|
|
288
340
|
return {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
// The stream is returned at once and the session runs inside it, so
|
|
295
|
-
// nothing waits on a promise for the length of a session.
|
|
296
|
-
const stream = new ReadableStream({
|
|
297
|
-
async start(controller) {
|
|
298
|
-
controller.enqueue({ type: "stream-start", warnings: [] });
|
|
299
|
-
try {
|
|
300
|
-
const text = await run(call);
|
|
301
|
-
controller.enqueue({ type: "text-start", id: "report" });
|
|
302
|
-
controller.enqueue({
|
|
303
|
-
type: "text-delta",
|
|
304
|
-
id: "report",
|
|
305
|
-
delta: text
|
|
306
|
-
});
|
|
307
|
-
controller.enqueue({ type: "text-end", id: "report" });
|
|
308
|
-
controller.enqueue({
|
|
309
|
-
type: "finish",
|
|
310
|
-
usage: USAGE,
|
|
311
|
-
finishReason: { unified: "stop", raw: undefined }
|
|
312
|
-
});
|
|
313
|
-
controller.close();
|
|
314
|
-
}
|
|
315
|
-
catch (err) {
|
|
316
|
-
controller.error(err);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
return { stream };
|
|
321
|
-
},
|
|
322
|
-
async doGenerate(call) {
|
|
323
|
-
const text = await run(call);
|
|
324
|
-
return {
|
|
325
|
-
content: [{ type: "text", text }],
|
|
326
|
-
finishReason: { unified: "stop", raw: undefined },
|
|
327
|
-
usage: USAGE,
|
|
328
|
-
warnings: []
|
|
329
|
-
};
|
|
330
|
-
}
|
|
341
|
+
exitCode,
|
|
342
|
+
...(end.result ? { result: end.result } : {}),
|
|
343
|
+
...(end.stderr ? { stderr: end.stderr } : {}),
|
|
344
|
+
...(end.rateLimit ? { rateLimit: end.rateLimit } : {}),
|
|
345
|
+
...(sessionId ? { sessionId } : {})
|
|
331
346
|
};
|
|
332
347
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
*
|
|
336
|
-
* An exec's events reach the sub-agent as a stream over RPC, relayed by the
|
|
337
|
-
* workspace object from the container's own RPC session — so a drop anywhere on
|
|
338
|
-
* that path cuts the stream while the session runs on. Measured: a container's
|
|
339
|
-
* session dropped after 21 minutes and was attached again within a second, the
|
|
340
|
-
* process untouched. The message is workerd's for a stream whose far side went
|
|
341
|
-
* away, and it carries no code.
|
|
342
|
-
*/
|
|
343
|
-
function isStreamCut(err) {
|
|
344
|
-
if (isPlatformTransientError(err))
|
|
345
|
-
return true;
|
|
346
|
-
for (let e = err; e instanceof Error; e = e.cause) {
|
|
347
|
-
if (e.message.includes("ReadableStream received over RPC disconnected"))
|
|
348
|
-
return true;
|
|
349
|
-
}
|
|
350
|
-
return false;
|
|
348
|
+
function sleep(ms) {
|
|
349
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
351
350
|
}
|
|
352
|
-
/**
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
+
});
|
|
366
366
|
}
|