@dynamicagents/plugins 0.12.0 → 0.14.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 +58 -77
- package/dist/browser/index.d.ts +0 -2
- package/dist/browser/index.js +21 -27
- package/dist/claude-code/cards.d.ts +55 -0
- package/dist/claude-code/cards.js +283 -0
- package/dist/claude-code/config.d.ts +28 -63
- package/dist/claude-code/config.js +0 -11
- package/dist/claude-code/egress.d.ts +8 -1
- package/dist/claude-code/egress.js +42 -4
- package/dist/claude-code/events.d.ts +106 -9
- package/dist/claude-code/events.js +201 -56
- package/dist/claude-code/github.d.ts +39 -0
- package/dist/claude-code/github.js +82 -0
- package/dist/claude-code/index.d.ts +16 -90
- package/dist/claude-code/index.js +14 -276
- package/dist/claude-code/model.d.ts +152 -0
- package/dist/claude-code/model.js +366 -0
- package/dist/claude-code/run.d.ts +230 -116
- package/dist/claude-code/run.js +380 -167
- package/dist/claude-code/session.d.ts +131 -0
- package/dist/claude-code/session.js +288 -0
- package/dist/claude-code/spec.d.ts +46 -0
- package/dist/claude-code/spec.js +83 -0
- package/dist/claude-code/verified.d.ts +15 -0
- package/dist/claude-code/verified.js +15 -0
- package/dist/computer/context.d.ts +1 -5
- package/dist/computer/context.js +7 -10
- package/dist/computer/file-lock.js +5 -4
- package/dist/computer/gate.d.ts +4 -4
- package/dist/computer/gate.js +5 -5
- package/dist/computer/index.d.ts +40 -258
- package/dist/computer/index.js +71 -324
- package/dist/computer/paths.d.ts +36 -16
- package/dist/computer/paths.js +42 -29
- package/dist/computer/proxy.d.ts +45 -0
- package/dist/computer/proxy.js +234 -0
- package/dist/computer/render.d.ts +1 -40
- package/dist/computer/render.js +2 -68
- package/dist/computer/tools-exec.js +12 -13
- package/dist/computer/tools-file.d.ts +9 -1
- package/dist/computer/tools-file.js +22 -82
- package/dist/computer/{tools-find.d.ts → tools-grep.d.ts} +1 -1
- package/dist/computer/tools-grep.js +129 -0
- package/dist/repo/checkout.d.ts +4 -5
- package/dist/repo/checkout.js +31 -8
- package/dist/repo/context.d.ts +35 -9
- package/dist/repo/context.js +101 -49
- package/dist/repo/index.d.ts +73 -28
- package/dist/repo/index.js +112 -96
- package/dist/repo/tools-checks.d.ts +3 -0
- package/dist/repo/tools-checks.js +112 -0
- package/dist/repo/tools-clone.js +1 -1
- package/dist/repo/tools-pr.d.ts +14 -0
- package/dist/repo/{tools-forge.js → tools-pr.js} +53 -36
- package/dist/repo/tools-review.d.ts +9 -0
- package/dist/repo/tools-review.js +67 -47
- package/dist/repo/tools-worktree.js +144 -60
- package/dist/repo/tools-worktrees.d.ts +12 -0
- package/dist/repo/tools-worktrees.js +48 -0
- package/dist/repo/url.d.ts +2 -2
- package/dist/repo/url.js +7 -7
- package/dist/scratch/index.d.ts +4 -4
- package/dist/scratch/index.js +12 -11
- package/dist/{computer → workspace}/advisory.d.ts +4 -4
- package/dist/{computer → workspace}/advisory.js +2 -2
- package/dist/{computer/host → workspace}/ca-trust.d.ts +8 -1
- package/dist/{computer/host → workspace}/ca-trust.js +12 -3
- package/dist/{computer/host → workspace}/container-deps.d.ts +4 -0
- package/dist/{computer/host → workspace}/container-deps.js +26 -1
- package/dist/{computer/host → workspace}/container-fault.d.ts +3 -3
- package/dist/{computer/host → workspace}/container-fault.js +23 -8
- package/dist/workspace/deps-snapshot.d.ts +110 -0
- package/dist/workspace/deps-snapshot.js +424 -0
- package/dist/workspace/exec.d.ts +94 -0
- package/dist/workspace/exec.js +153 -0
- package/dist/workspace/format.d.ts +36 -0
- package/dist/workspace/format.js +57 -0
- package/dist/{computer/host → workspace}/git-host.d.ts +21 -4
- package/dist/{computer/host → workspace}/git-host.js +72 -2
- package/dist/{computer/host → workspace}/git-identity.d.ts +1 -1
- package/dist/{computer/host → workspace}/git-identity.js +1 -1
- package/dist/workspace/index.d.ts +44 -47
- package/dist/workspace/index.js +52 -112
- package/dist/{computer/host → workspace}/install-job.d.ts +53 -4
- package/dist/{computer/host → workspace}/install-job.js +346 -178
- package/dist/{computer → workspace}/install.d.ts +17 -8
- package/dist/{computer → workspace}/install.js +24 -6
- package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +125 -27
- package/dist/{computer/host/workspace.js → workspace/object.js} +341 -58
- package/dist/workspace/open.d.ts +95 -0
- package/dist/workspace/open.js +63 -0
- package/dist/workspace/read.d.ts +16 -0
- package/dist/workspace/read.js +22 -0
- package/dist/{computer → workspace}/shell.d.ts +5 -5
- package/dist/{computer → workspace}/shell.js +6 -6
- package/dist/{computer/host → workspace}/wake.d.ts +3 -1
- package/package.json +17 -37
- package/dist/arc-agi/analysis.d.ts +0 -322
- package/dist/arc-agi/analysis.js +0 -802
- package/dist/arc-agi/client.d.ts +0 -82
- package/dist/arc-agi/client.js +0 -113
- package/dist/arc-agi/game-tools.d.ts +0 -24
- package/dist/arc-agi/game-tools.js +0 -26
- package/dist/arc-agi/index.d.ts +0 -40
- package/dist/arc-agi/index.js +0 -175
- package/dist/arc-agi/main-agent.d.ts +0 -24
- package/dist/arc-agi/main-agent.js +0 -62
- package/dist/arc-agi/recipe.d.ts +0 -48
- package/dist/arc-agi/recipe.js +0 -97
- package/dist/arc-agi/scorecard.d.ts +0 -118
- package/dist/arc-agi/scorecard.js +0 -163
- package/dist/arc-agi/soul.d.ts +0 -29
- package/dist/arc-agi/soul.js +0 -79
- package/dist/arc-agi/store.d.ts +0 -153
- package/dist/arc-agi/store.js +0 -215
- package/dist/arc-agi/tools.d.ts +0 -36
- package/dist/arc-agi/tools.js +0 -574
- package/dist/arc-agi/types.d.ts +0 -211
- package/dist/arc-agi/types.js +0 -19
- package/dist/claude-code/recipe.d.ts +0 -53
- package/dist/claude-code/recipe.js +0 -100
- package/dist/computer/read.d.ts +0 -133
- package/dist/computer/read.js +0 -200
- package/dist/computer/tools-find.js +0 -216
- package/dist/recall/index.d.ts +0 -105
- package/dist/recall/index.js +0 -181
- package/dist/repo/tools-forge.d.ts +0 -4
- package/dist/triage/index.d.ts +0 -360
- package/dist/triage/index.js +0 -311
- /package/dist/{computer/host → workspace}/sync.d.ts +0 -0
- /package/dist/{computer/host → workspace}/sync.js +0 -0
- /package/dist/{computer/host → workspace}/wake.js +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { CredentialStore, Lead } from "./credentials.js";
|
|
2
|
+
import { type DrainCursor, type DrainOutcome, type DrainOptions, type SessionRuntime } from "./run.js";
|
|
3
|
+
import { type ClaudeCodeConfig, type PermissionMode } from "./config.js";
|
|
4
|
+
/**
|
|
5
|
+
* What continuing an interrupted conversation takes — see
|
|
6
|
+
* {@link file://./run.ts LaunchOptions.resume}.
|
|
7
|
+
*/
|
|
8
|
+
export interface ResumeSession {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
/** Continue under a new id, leaving the original conversation whole. */
|
|
11
|
+
fork?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** What one session needs to be launched: see {@link ClaudeCodeSession.start}. */
|
|
14
|
+
export interface StartSession {
|
|
15
|
+
/** The whole of what this session is asked to do, or its next user turn. */
|
|
16
|
+
prompt: string;
|
|
17
|
+
/** The checkout. */
|
|
18
|
+
dir: string;
|
|
19
|
+
jsonSchema?: Record<string, unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* This session's mode, over the config's — see
|
|
22
|
+
* {@link file://./model.ts ClaudeCodeModelOptions.permissionMode}.
|
|
23
|
+
*/
|
|
24
|
+
permissionMode?: PermissionMode;
|
|
25
|
+
/** A Claude Code conversation to continue rather than start. */
|
|
26
|
+
resume?: ResumeSession;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Bind a config to a workspace runtime: start, follow up, resume, stop.
|
|
30
|
+
*
|
|
31
|
+
* What `claudeCodeModel` drives a run with, and what a host's `settle` stops
|
|
32
|
+
* one with. State lives in the {@link DrainCursor} the caller stores, so this
|
|
33
|
+
* holds none and a fresh isolate picks up exactly where the last one stopped.
|
|
34
|
+
*
|
|
35
|
+
* ## Two senses of "resume", and they are not the same mechanism
|
|
36
|
+
*
|
|
37
|
+
* {@link ClaudeCodeSession.resume} re-attaches to a running **exec** — the
|
|
38
|
+
* process is alive in the container and a drain that was cut reads on from the
|
|
39
|
+
* cursor's sequence. Nothing about the conversation changes; this is plumbing.
|
|
40
|
+
*
|
|
41
|
+
* {@link StartSession.resume} continues a Claude Code **conversation** that has
|
|
42
|
+
* already ended, in a new process with a new exec: the session's transcript is
|
|
43
|
+
* read back into context and the prompt is the next user turn. Where that
|
|
44
|
+
* transcript lives — and so how long this is possible at all — is
|
|
45
|
+
* {@link file://./run.ts SESSION_CONFIG_DIR}.
|
|
46
|
+
*/
|
|
47
|
+
export declare function claudeCodeSession(config: ClaudeCodeConfig): {
|
|
48
|
+
/**
|
|
49
|
+
* Spawn a session for one run and drain it.
|
|
50
|
+
*
|
|
51
|
+
* `runId` namespaces the exec id. Runs are concurrent, so two sessions in
|
|
52
|
+
* one workspace under one id would spawn over each other — see
|
|
53
|
+
* {@link execIdFor}.
|
|
54
|
+
*
|
|
55
|
+
* `resume` continues a conversation on this session's **own** exec rather
|
|
56
|
+
* than on a second one: a run given a conversation to continue has no first
|
|
57
|
+
* session of its own, so there is nothing for a follow-up exec to follow.
|
|
58
|
+
*/
|
|
59
|
+
start(runtime: SessionRuntime, runId: string, options: StartSession, sinks?: DrainOptions): Promise<DrainOutcome>;
|
|
60
|
+
/**
|
|
61
|
+
* Give a finished writing session one more turn, and drain it.
|
|
62
|
+
*
|
|
63
|
+
* `sessionId` is the one the session reported — on its `result` line, or on
|
|
64
|
+
* its cursor when it ended without one. This runs in the workspace the
|
|
65
|
+
* session did, because that workspace's {@link file://./run.ts SESSION_CONFIG_DIR}
|
|
66
|
+
* is where the transcript it is continuing can be found. A drain cut short goes
|
|
67
|
+
* on through {@link resume} with the cursor it stored, like any other
|
|
68
|
+
* session's.
|
|
69
|
+
*
|
|
70
|
+
* Never forked: a follow-up is one more turn of the same conversation, which
|
|
71
|
+
* is exactly what a fork is for *not* doing. For the same reason it runs
|
|
72
|
+
* under the session's own `permissionMode`.
|
|
73
|
+
*/
|
|
74
|
+
followUp(runtime: SessionRuntime, runId: string, options: {
|
|
75
|
+
sessionId: string;
|
|
76
|
+
prompt: string;
|
|
77
|
+
dir: string;
|
|
78
|
+
permissionMode?: PermissionMode;
|
|
79
|
+
}, sinks?: DrainOptions): Promise<DrainOutcome>;
|
|
80
|
+
/**
|
|
81
|
+
* Re-attach to a running session and drain it to its end.
|
|
82
|
+
*
|
|
83
|
+
* **A session whose container was replaced is reported, not thrown.** The
|
|
84
|
+
* attachment is the first thing to notice a replacement, and the caller is a
|
|
85
|
+
* recovered turn with a cursor it will happily keep presenting: a throw here
|
|
86
|
+
* retries the same dead id until the recoveries run out, and the run ends on
|
|
87
|
+
* a stack trace that names neither the session nor the cause. A
|
|
88
|
+
* terminal outcome ends it at the first attempt instead, with the exit code
|
|
89
|
+
* the runtime uses for a killed process and an explanation on `stderr`,
|
|
90
|
+
* which is exactly the channel a session that died without a result line
|
|
91
|
+
* already reports through. The wording is {@link containerLost}'s.
|
|
92
|
+
*/
|
|
93
|
+
resume(runtime: SessionRuntime, cursor: DrainCursor, sinks?: DrainOptions): Promise<DrainOutcome>;
|
|
94
|
+
/** Stop a session — `SIGTERM`, so its own process tree goes with it. */
|
|
95
|
+
stop(runtime: SessionRuntime, runId: string): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Is any credential usable right now, and if not, when?
|
|
98
|
+
*
|
|
99
|
+
* The same question {@link egress} answers per request, asked ahead of time
|
|
100
|
+
* so a host can decline to start a session it cannot pay for. That is not a
|
|
101
|
+
* micro-optimisation: an invocation carries an 18.7-27k-token cached prefix
|
|
102
|
+
* before it does anything, so starting one and letting the gateway refuse
|
|
103
|
+
* its first model call costs a container start and that prefix to learn what
|
|
104
|
+
* this returns for free — and reports it as a failed run rather than as a
|
|
105
|
+
* rate limit with a time on it.
|
|
106
|
+
*
|
|
107
|
+
* Reads only. Nothing here marks anything spent.
|
|
108
|
+
*/
|
|
109
|
+
credentials: (store: CredentialStore) => Promise<Lead>;
|
|
110
|
+
/**
|
|
111
|
+
* The `Fetcher` the workspace object installs as its egress policy.
|
|
112
|
+
*
|
|
113
|
+
* `store` is an **argument rather than a config field**, and that is the
|
|
114
|
+
* point: the credential pool's state belongs to whichever object has
|
|
115
|
+
* storage, and this same config object is also held by the agents that run
|
|
116
|
+
* sessions, which have none of their own for it. Making it a field would
|
|
117
|
+
* force them to invent one.
|
|
118
|
+
*/
|
|
119
|
+
egress: (store: CredentialStore) => {
|
|
120
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
121
|
+
connect(address: SocketAddress | string, options?: SocketOptions): Socket;
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
export type ClaudeCodeSession = ReturnType<typeof claudeCodeSession>;
|
|
125
|
+
/**
|
|
126
|
+
* Refuse a deployment with no credential when the model is built, naming this
|
|
127
|
+
* plugin, rather than at the first model call inside a run somebody is waiting
|
|
128
|
+
* on. The thunk is still what the gateway calls per request, so a rotation is
|
|
129
|
+
* picked up.
|
|
130
|
+
*/
|
|
131
|
+
export declare function requireCredentials(config: ClaudeCodeConfig): void;
|
|
@@ -0,0 +1,288 @@
|
|
|
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
|
+
import { claudeCodeEgress } from "./egress.js";
|
|
54
|
+
import { credentialPool } from "./credentials.js";
|
|
55
|
+
import { attachRun, containerLost, drainRun, execIdFor, followUpExecIdFor, freshCursor, isExecLost, killRun, startRun } from "./run.js";
|
|
56
|
+
import { DEFAULT_TIMEOUT_MS } from "./config.js";
|
|
57
|
+
/**
|
|
58
|
+
* Bind a config to a workspace runtime: start, follow up, resume, stop.
|
|
59
|
+
*
|
|
60
|
+
* What `claudeCodeModel` drives a run with, and what a host's `settle` stops
|
|
61
|
+
* one with. State lives in the {@link DrainCursor} the caller stores, so this
|
|
62
|
+
* holds none and a fresh isolate picks up exactly where the last one stopped.
|
|
63
|
+
*
|
|
64
|
+
* ## Two senses of "resume", and they are not the same mechanism
|
|
65
|
+
*
|
|
66
|
+
* {@link ClaudeCodeSession.resume} re-attaches to a running **exec** — the
|
|
67
|
+
* process is alive in the container and a drain that was cut reads on from the
|
|
68
|
+
* cursor's sequence. Nothing about the conversation changes; this is plumbing.
|
|
69
|
+
*
|
|
70
|
+
* {@link StartSession.resume} continues a Claude Code **conversation** that has
|
|
71
|
+
* already ended, in a new process with a new exec: the session's transcript is
|
|
72
|
+
* read back into context and the prompt is the next user turn. Where that
|
|
73
|
+
* transcript lives — and so how long this is possible at all — is
|
|
74
|
+
* {@link file://./run.ts SESSION_CONFIG_DIR}.
|
|
75
|
+
*/
|
|
76
|
+
export function claudeCodeSession(config) {
|
|
77
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
78
|
+
const launch = (prompt, dir, permissionMode = config.permissionMode) => ({
|
|
79
|
+
prompt,
|
|
80
|
+
dir,
|
|
81
|
+
...(config.model ? { model: config.model } : {}),
|
|
82
|
+
...(config.effort ? { effort: config.effort } : {}),
|
|
83
|
+
...(config.maxSubagentDepth === undefined
|
|
84
|
+
? {}
|
|
85
|
+
: { maxSubagentDepth: config.maxSubagentDepth }),
|
|
86
|
+
...(config.maxConcurrentSubagents === undefined
|
|
87
|
+
? {}
|
|
88
|
+
: { maxConcurrentSubagents: config.maxConcurrentSubagents }),
|
|
89
|
+
// Forwarded only when set, so `buildLaunch` owns the default in one place
|
|
90
|
+
// rather than this line resolving it and the flag being written twice.
|
|
91
|
+
...(permissionMode ? { permissionMode } : {}),
|
|
92
|
+
...(config.env ? { env: config.env } : {}),
|
|
93
|
+
...(config.author ? { author: config.author } : {}),
|
|
94
|
+
...(config.githubToken ? { github: true } : {})
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
/**
|
|
98
|
+
* Spawn a session for one run and drain it.
|
|
99
|
+
*
|
|
100
|
+
* `runId` namespaces the exec id. Runs are concurrent, so two sessions in
|
|
101
|
+
* one workspace under one id would spawn over each other — see
|
|
102
|
+
* {@link execIdFor}.
|
|
103
|
+
*
|
|
104
|
+
* `resume` continues a conversation on this session's **own** exec rather
|
|
105
|
+
* than on a second one: a run given a conversation to continue has no first
|
|
106
|
+
* session of its own, so there is nothing for a follow-up exec to follow.
|
|
107
|
+
*/
|
|
108
|
+
async start(runtime, runId, options, sinks = {}) {
|
|
109
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
110
|
+
try {
|
|
111
|
+
const execId = execIdFor(runId);
|
|
112
|
+
// `using`, so the attachment is released even when the drain throws.
|
|
113
|
+
const handle = __addDisposableResource(env_1, await startRun(runtime, {
|
|
114
|
+
...launch(options.prompt, options.dir, options.permissionMode),
|
|
115
|
+
...(options.jsonSchema ? { jsonSchema: options.jsonSchema } : {}),
|
|
116
|
+
...resumption(options.resume),
|
|
117
|
+
execId,
|
|
118
|
+
timeoutMs,
|
|
119
|
+
signal: sinks.signal
|
|
120
|
+
}), false);
|
|
121
|
+
return await drainRun(handle, freshCursor(execId), sinks);
|
|
122
|
+
}
|
|
123
|
+
catch (e_1) {
|
|
124
|
+
env_1.error = e_1;
|
|
125
|
+
env_1.hasError = true;
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
__disposeResources(env_1);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
/**
|
|
132
|
+
* Give a finished writing session one more turn, and drain it.
|
|
133
|
+
*
|
|
134
|
+
* `sessionId` is the one the session reported — on its `result` line, or on
|
|
135
|
+
* its cursor when it ended without one. This runs in the workspace the
|
|
136
|
+
* session did, because that workspace's {@link file://./run.ts SESSION_CONFIG_DIR}
|
|
137
|
+
* is where the transcript it is continuing can be found. A drain cut short goes
|
|
138
|
+
* on through {@link resume} with the cursor it stored, like any other
|
|
139
|
+
* session's.
|
|
140
|
+
*
|
|
141
|
+
* Never forked: a follow-up is one more turn of the same conversation, which
|
|
142
|
+
* is exactly what a fork is for *not* doing. For the same reason it runs
|
|
143
|
+
* under the session's own `permissionMode`.
|
|
144
|
+
*/
|
|
145
|
+
async followUp(runtime, runId, options, sinks = {}) {
|
|
146
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
147
|
+
try {
|
|
148
|
+
// Without one, `--resume` would be dropped and a new session started
|
|
149
|
+
// under the follow-up's id — unrelated work, reported as the follow-up.
|
|
150
|
+
if (!options.sessionId) {
|
|
151
|
+
throw new Error("claude-code: a follow-up needs the session id its result reported");
|
|
152
|
+
}
|
|
153
|
+
const execId = followUpExecIdFor(runId);
|
|
154
|
+
const handle = __addDisposableResource(env_2, await startRun(runtime, {
|
|
155
|
+
...launch(options.prompt, options.dir, options.permissionMode),
|
|
156
|
+
resume: options.sessionId,
|
|
157
|
+
execId,
|
|
158
|
+
timeoutMs,
|
|
159
|
+
signal: sinks.signal
|
|
160
|
+
}), false);
|
|
161
|
+
return await drainRun(handle, freshCursor(execId), sinks);
|
|
162
|
+
}
|
|
163
|
+
catch (e_2) {
|
|
164
|
+
env_2.error = e_2;
|
|
165
|
+
env_2.hasError = true;
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
__disposeResources(env_2);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
/**
|
|
172
|
+
* Re-attach to a running session and drain it to its end.
|
|
173
|
+
*
|
|
174
|
+
* **A session whose container was replaced is reported, not thrown.** The
|
|
175
|
+
* attachment is the first thing to notice a replacement, and the caller is a
|
|
176
|
+
* recovered turn with a cursor it will happily keep presenting: a throw here
|
|
177
|
+
* retries the same dead id until the recoveries run out, and the run ends on
|
|
178
|
+
* a stack trace that names neither the session nor the cause. A
|
|
179
|
+
* terminal outcome ends it at the first attempt instead, with the exit code
|
|
180
|
+
* the runtime uses for a killed process and an explanation on `stderr`,
|
|
181
|
+
* which is exactly the channel a session that died without a result line
|
|
182
|
+
* already reports through. The wording is {@link containerLost}'s.
|
|
183
|
+
*/
|
|
184
|
+
async resume(runtime, cursor, sinks = {}) {
|
|
185
|
+
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
186
|
+
try {
|
|
187
|
+
let handle;
|
|
188
|
+
try {
|
|
189
|
+
handle = await attachRun(runtime, cursor, {
|
|
190
|
+
signal: sinks.signal,
|
|
191
|
+
maxWaitMs: timeoutMs
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
if (!isExecLost(err))
|
|
196
|
+
throw err;
|
|
197
|
+
console.warn("[claude-code] the session's container was replaced", {
|
|
198
|
+
execId: cursor.execId,
|
|
199
|
+
err: String(err)
|
|
200
|
+
});
|
|
201
|
+
return {
|
|
202
|
+
done: true,
|
|
203
|
+
cursor,
|
|
204
|
+
progress: [],
|
|
205
|
+
exitCode: -1,
|
|
206
|
+
stderr: containerLost()
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const session = __addDisposableResource(env_3, handle, false);
|
|
210
|
+
return await drainRun(session, cursor, sinks);
|
|
211
|
+
}
|
|
212
|
+
catch (e_3) {
|
|
213
|
+
env_3.error = e_3;
|
|
214
|
+
env_3.hasError = true;
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
__disposeResources(env_3);
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
/** Stop a session — `SIGTERM`, so its own process tree goes with it. */
|
|
221
|
+
async stop(runtime, runId) {
|
|
222
|
+
// First, and allowed to fail: most sessions never had a follow-up, and
|
|
223
|
+
// one that did has usually finished its first exec already.
|
|
224
|
+
await killRun(runtime, followUpExecIdFor(runId)).catch(() => { });
|
|
225
|
+
await killRun(runtime, execIdFor(runId));
|
|
226
|
+
},
|
|
227
|
+
/**
|
|
228
|
+
* Is any credential usable right now, and if not, when?
|
|
229
|
+
*
|
|
230
|
+
* The same question {@link egress} answers per request, asked ahead of time
|
|
231
|
+
* so a host can decline to start a session it cannot pay for. That is not a
|
|
232
|
+
* micro-optimisation: an invocation carries an 18.7-27k-token cached prefix
|
|
233
|
+
* before it does anything, so starting one and letting the gateway refuse
|
|
234
|
+
* its first model call costs a container start and that prefix to learn what
|
|
235
|
+
* this returns for free — and reports it as a failed run rather than as a
|
|
236
|
+
* rate limit with a time on it.
|
|
237
|
+
*
|
|
238
|
+
* Reads only. Nothing here marks anything spent.
|
|
239
|
+
*/
|
|
240
|
+
credentials: (store) => credentialPool({ credentials: config.credentials, store }).lead(),
|
|
241
|
+
/**
|
|
242
|
+
* The `Fetcher` the workspace object installs as its egress policy.
|
|
243
|
+
*
|
|
244
|
+
* `store` is an **argument rather than a config field**, and that is the
|
|
245
|
+
* point: the credential pool's state belongs to whichever object has
|
|
246
|
+
* storage, and this same config object is also held by the agents that run
|
|
247
|
+
* sessions, which have none of their own for it. Making it a field would
|
|
248
|
+
* force them to invent one.
|
|
249
|
+
*/
|
|
250
|
+
egress: (store) => claudeCodeEgress({
|
|
251
|
+
credentials: config.credentials,
|
|
252
|
+
store,
|
|
253
|
+
// Forwarded only when set. Defaulting it to `[]` here would turn "the
|
|
254
|
+
// host said nothing" into "Anthropic only", which is the one reading
|
|
255
|
+
// the three-way semantics exist to keep distinct.
|
|
256
|
+
...(config.restrictToHosts === undefined
|
|
257
|
+
? {}
|
|
258
|
+
: { restrictToHosts: config.restrictToHosts }),
|
|
259
|
+
...(config.githubToken ? { githubToken: config.githubToken } : {}),
|
|
260
|
+
label: "claude-code"
|
|
261
|
+
})
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* A conversation to continue, as launch flags.
|
|
266
|
+
*
|
|
267
|
+
* One place rather than a spread at each call site, so `fork` can never travel
|
|
268
|
+
* without the id it forks — which {@link file://./run.ts buildLaunch} refuses,
|
|
269
|
+
* but refuses at launch rather than here.
|
|
270
|
+
*/
|
|
271
|
+
function resumption(resume) {
|
|
272
|
+
if (!resume)
|
|
273
|
+
return undefined;
|
|
274
|
+
return { resume: resume.sessionId, ...(resume.fork ? { fork: true } : {}) };
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Refuse a deployment with no credential when the model is built, naming this
|
|
278
|
+
* plugin, rather than at the first model call inside a run somebody is waiting
|
|
279
|
+
* on. The thunk is still what the gateway calls per request, so a rotation is
|
|
280
|
+
* picked up.
|
|
281
|
+
*/
|
|
282
|
+
export function requireCredentials(config) {
|
|
283
|
+
if (config.credentials().filter(Boolean).length === 0) {
|
|
284
|
+
throw new Error("claude-code: no credentials. Set at least one `claude setup-token` " +
|
|
285
|
+
"credential and pass it as " +
|
|
286
|
+
"`credentials: () => [env.CLAUDE_CODE_OAUTH_TOKEN_1]`.");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { SubAgentSpec } from "@dynamicagents/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/**
|
|
4
|
+
* The Claude Code writer, as data: what the parent's model is offered. The host
|
|
5
|
+
* binds it to a `SubAgent` class whose model is
|
|
6
|
+
* {@link file://./model.ts claudeCodeModel}, and adds the `prepare` and
|
|
7
|
+
* `settle` that claim and release its workspace. A planner is the same spec
|
|
8
|
+
* under another name, description and input, whose model runs under `plan` —
|
|
9
|
+
* see {@link file://./model.ts ClaudeCodeModelOptions.permissionMode}.
|
|
10
|
+
*
|
|
11
|
+
* A module of its own for its `zod` schemas. `zod` costs a Worker bundle
|
|
12
|
+
* ~460 KiB, and the workspace Durable Object imports this subpath for its
|
|
13
|
+
* egress gateway alone; with `sideEffects: false`, a module nothing it uses
|
|
14
|
+
* reaches is dropped from that bundle whole.
|
|
15
|
+
*
|
|
16
|
+
* It runs **detached**: a session runs for up to its `timeoutMs`, past the
|
|
17
|
+
* point a parent's turn is cut.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The writer's input: the task, and optionally the branch to add to.
|
|
21
|
+
*
|
|
22
|
+
* *Which* repository is not the parent model's to choose: the workspace is one
|
|
23
|
+
* Durable Object, one container and one checkout, keyed by caller and
|
|
24
|
+
* repository, and the parent already tracks which one is active. A repository
|
|
25
|
+
* named here would let a model name somebody else's — the same reasoning that
|
|
26
|
+
* keeps the workspace name out of model input in `/computer`.
|
|
27
|
+
*
|
|
28
|
+
* A branch is different: the model quotes it from an earlier report or a pull
|
|
29
|
+
* request, and the host's `prepare` resolves it before anything runs.
|
|
30
|
+
*
|
|
31
|
+
* Exported so a host can extend it with an input of its own that its `prepare`
|
|
32
|
+
* resolves.
|
|
33
|
+
*/
|
|
34
|
+
export declare const CLAUDE_CODE_WRITE_INPUT: z.ZodObject<{
|
|
35
|
+
task: z.ZodString;
|
|
36
|
+
continue: z.ZodOptional<z.ZodString>;
|
|
37
|
+
}, z.core.$strip>;
|
|
38
|
+
export type ClaudeCodeInput = z.infer<typeof CLAUDE_CODE_WRITE_INPUT>;
|
|
39
|
+
/**
|
|
40
|
+
* Sized deliberately. An invocation carries an 18.7-27k token cached prefix
|
|
41
|
+
* before it does anything — a ten-call burst billed twenty raw input tokens
|
|
42
|
+
* against 187,130 cache reads — so a session has to be worth starting. The
|
|
43
|
+
* description says that in the terms the parent's model can act on: one
|
|
44
|
+
* coherent change, not one file edit.
|
|
45
|
+
*/
|
|
46
|
+
export declare const CLAUDE_CODE_AGENT: SubAgentSpec<ClaudeCodeInput>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* The Claude Code writer, as data: what the parent's model is offered. The host
|
|
4
|
+
* binds it to a `SubAgent` class whose model is
|
|
5
|
+
* {@link file://./model.ts claudeCodeModel}, and adds the `prepare` and
|
|
6
|
+
* `settle` that claim and release its workspace. A planner is the same spec
|
|
7
|
+
* under another name, description and input, whose model runs under `plan` —
|
|
8
|
+
* see {@link file://./model.ts ClaudeCodeModelOptions.permissionMode}.
|
|
9
|
+
*
|
|
10
|
+
* A module of its own for its `zod` schemas. `zod` costs a Worker bundle
|
|
11
|
+
* ~460 KiB, and the workspace Durable Object imports this subpath for its
|
|
12
|
+
* egress gateway alone; with `sideEffects: false`, a module nothing it uses
|
|
13
|
+
* reaches is dropped from that bundle whole.
|
|
14
|
+
*
|
|
15
|
+
* It runs **detached**: a session runs for up to its `timeoutMs`, past the
|
|
16
|
+
* point a parent's turn is cut.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* The writer's input: the task, and optionally the branch to add to.
|
|
20
|
+
*
|
|
21
|
+
* *Which* repository is not the parent model's to choose: the workspace is one
|
|
22
|
+
* Durable Object, one container and one checkout, keyed by caller and
|
|
23
|
+
* repository, and the parent already tracks which one is active. A repository
|
|
24
|
+
* named here would let a model name somebody else's — the same reasoning that
|
|
25
|
+
* keeps the workspace name out of model input in `/computer`.
|
|
26
|
+
*
|
|
27
|
+
* A branch is different: the model quotes it from an earlier report or a pull
|
|
28
|
+
* request, and the host's `prepare` resolves it before anything runs.
|
|
29
|
+
*
|
|
30
|
+
* Exported so a host can extend it with an input of its own that its `prepare`
|
|
31
|
+
* resolves.
|
|
32
|
+
*/
|
|
33
|
+
export const CLAUDE_CODE_WRITE_INPUT = z.object({
|
|
34
|
+
task: z
|
|
35
|
+
.string()
|
|
36
|
+
.describe("The change to make, described as you would to an engineer: what should be true when it is done, and how to tell"),
|
|
37
|
+
continue: z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("The branch to add to — one an earlier claude_code report named, or an open pull request's head branch; omit to start a new branch")
|
|
41
|
+
});
|
|
42
|
+
/**
|
|
43
|
+
* Nothing reads it: the model is the CLI, whose system prompt is its own. It
|
|
44
|
+
* exists because a spec must declare a soul, and says so, so nobody tunes it.
|
|
45
|
+
*/
|
|
46
|
+
const SOUL = "Not a prompt: this sub-agent's model is a Claude Code session, which brings its own.";
|
|
47
|
+
/**
|
|
48
|
+
* Sized deliberately. An invocation carries an 18.7-27k token cached prefix
|
|
49
|
+
* before it does anything — a ten-call burst billed twenty raw input tokens
|
|
50
|
+
* against 187,130 cache reads — so a session has to be worth starting. The
|
|
51
|
+
* description says that in the terms the parent's model can act on: one
|
|
52
|
+
* coherent change, not one file edit.
|
|
53
|
+
*/
|
|
54
|
+
export const CLAUDE_CODE_AGENT = {
|
|
55
|
+
name: "claude_code",
|
|
56
|
+
description: [
|
|
57
|
+
"Hand a coding task to a Claude Code session running in your workspace",
|
|
58
|
+
"container, in whatever you have open there — a repository you checked out,",
|
|
59
|
+
"or a scratchpad. It has its own tools — it reads, edits, runs the test suite",
|
|
60
|
+
"and iterates — and it reports back what it did. It runs in the background:",
|
|
61
|
+
"the call returns at once, and its report arrives in a later turn.",
|
|
62
|
+
"",
|
|
63
|
+
"Give it **one coherent change**, described the way you would describe it to",
|
|
64
|
+
"an engineer: what should be true when it is done, and how to tell. It is",
|
|
65
|
+
"expensive to start and cheap to let run, so 'add the endpoint, its tests and",
|
|
66
|
+
"wire it up' is one session, not three. A session that is only asked to edit",
|
|
67
|
+
"a single line costs about what a substantial one costs.",
|
|
68
|
+
"",
|
|
69
|
+
"It cannot ask you anything mid-run. Anything it would need to ask, decide",
|
|
70
|
+
"first — or ask the user yourself before delegating.",
|
|
71
|
+
"",
|
|
72
|
+
"To correct or extend work that is already on a branch — the one a previous",
|
|
73
|
+
"session's report named, or an open pull request's head branch — set",
|
|
74
|
+
"`continue` to that branch: the new session starts from its tip and adds",
|
|
75
|
+
"commits to it, so pushing the branch updates its pull request. Without it, a",
|
|
76
|
+
"session starts a branch of its own, and changing an open pull request from",
|
|
77
|
+
"there means moving commits between branches."
|
|
78
|
+
].join("\n"),
|
|
79
|
+
inputSchema: CLAUDE_CODE_WRITE_INPUT,
|
|
80
|
+
soul: SOUL,
|
|
81
|
+
detached: true,
|
|
82
|
+
formatInput: (input) => input.task
|
|
83
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Claude Code version this package's egress gateway and stream parser were
|
|
3
|
+
* last verified against, and the one a deployment's image pins.
|
|
4
|
+
*
|
|
5
|
+
* Both are written against that version's traffic — its headers, its beta list,
|
|
6
|
+
* its `stream-json` lines — and the probe's capture,
|
|
7
|
+
* `test/fixtures/claude-code-probe-capture.json`, is that traffic, recorded. The
|
|
8
|
+
* specs read the capture, so they check the gateway and the parser against what
|
|
9
|
+
* this version actually sends and prints.
|
|
10
|
+
*
|
|
11
|
+
* Written by `npm run probe:claude-code -- --record`, never by hand: the value
|
|
12
|
+
* and the capture move together. AGENTS.md's "Updating Claude Code" is the
|
|
13
|
+
* procedure.
|
|
14
|
+
*/
|
|
15
|
+
export declare const VERIFIED_CLAUDE_CODE_VERSION = "2.1.296";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Claude Code version this package's egress gateway and stream parser were
|
|
3
|
+
* last verified against, and the one a deployment's image pins.
|
|
4
|
+
*
|
|
5
|
+
* Both are written against that version's traffic — its headers, its beta list,
|
|
6
|
+
* its `stream-json` lines — and the probe's capture,
|
|
7
|
+
* `test/fixtures/claude-code-probe-capture.json`, is that traffic, recorded. The
|
|
8
|
+
* specs read the capture, so they check the gateway and the parser against what
|
|
9
|
+
* this version actually sends and prints.
|
|
10
|
+
*
|
|
11
|
+
* Written by `npm run probe:claude-code -- --record`, never by hand: the value
|
|
12
|
+
* and the capture move together. AGENTS.md's "Updating Claude Code" is the
|
|
13
|
+
* procedure.
|
|
14
|
+
*/
|
|
15
|
+
export const VERIFIED_CLAUDE_CODE_VERSION = "2.1.296";
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { WorkspaceClient } from "@cloudflare/computer";
|
|
2
2
|
import { type ExecGate } from "./gate.js";
|
|
3
|
-
import type { WorkspaceAdvisory } from "
|
|
3
|
+
import type { WorkspaceAdvisory } from "../workspace/advisory.js";
|
|
4
4
|
import type { ComputerConfig } from "./index.js";
|
|
5
|
-
/** A command that has not finished in this long is a hung command. */
|
|
6
|
-
export declare const DEFAULT_TIMEOUT_MS: number;
|
|
7
|
-
/** Where checkouts live, in the container and in the workspace alike. */
|
|
8
|
-
export declare const DEFAULT_CWD = "/workspace";
|
|
9
5
|
/** What {@link computerContext} hands each group of tools. */
|
|
10
6
|
export interface ComputerContext {
|
|
11
7
|
config: ComputerConfig;
|
package/dist/computer/context.js
CHANGED
|
@@ -50,6 +50,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
50
50
|
var e = new Error(message);
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
|
+
import { DEFAULT_CWD, DEFAULT_TIMEOUT_MS } from "../workspace/exec.js";
|
|
53
54
|
import { withAbort } from "@dynamicagents/core";
|
|
54
55
|
import { execGate, writeGate } from "./gate.js";
|
|
55
56
|
/**
|
|
@@ -66,10 +67,6 @@ import { execGate, writeGate } from "./gate.js";
|
|
|
66
67
|
* says what actually failed.
|
|
67
68
|
*/
|
|
68
69
|
const DEFAULT_MAX_OUTPUT_CHARS = 16_000;
|
|
69
|
-
/** A command that has not finished in this long is a hung command. */
|
|
70
|
-
export const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
71
|
-
/** Where checkouts live, in the container and in the workspace alike. */
|
|
72
|
-
export const DEFAULT_CWD = "/workspace";
|
|
73
70
|
/**
|
|
74
71
|
* How often the install gate re-reads the install's status.
|
|
75
72
|
*
|
|
@@ -108,7 +105,7 @@ export function computerContext(workspace, config, advisories, fsWorkspace = wor
|
|
|
108
105
|
// is a read of another Durable Object's state. An error must not take out a
|
|
109
106
|
// working shell — running the command is exactly what would have happened
|
|
110
107
|
// before the gate existed, and a spurious blockage would strand the
|
|
111
|
-
//
|
|
108
|
+
// agent.
|
|
112
109
|
const read = async () => {
|
|
113
110
|
try {
|
|
114
111
|
signal?.throwIfAborted();
|
|
@@ -178,18 +175,18 @@ export function computerContext(workspace, config, advisories, fsWorkspace = wor
|
|
|
178
175
|
/**
|
|
179
176
|
* Open the workspace, run one file operation in it, and own the failure.
|
|
180
177
|
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
178
|
+
* Lines every file tool would otherwise repeat with only the verb changed,
|
|
179
|
+
* and a chance for the next one to be written without them. The catch is the
|
|
183
180
|
* point: a failed file operation is the model's to recover from, and it can
|
|
184
181
|
* only recover from what it is told, so this returns the sentence rather than
|
|
185
182
|
* throwing it.
|
|
186
183
|
*
|
|
187
|
-
* `
|
|
184
|
+
* `bash` deliberately does not use it. That one recognises `EEXEC_LOST`,
|
|
188
185
|
* logs, and carries an install warning through the failure path — a bespoke
|
|
189
186
|
* catch saying something this cannot.
|
|
190
187
|
*
|
|
191
|
-
* It opens through `fsWorkspace`, which is what makes the
|
|
192
|
-
* about these tools true: they answer while the container is down.
|
|
188
|
+
* It opens through `fsWorkspace`, which is what makes the context block's
|
|
189
|
+
* sentence about these tools true: they answer while the container is down.
|
|
193
190
|
*/
|
|
194
191
|
const inWorkspace = async (gerund, subject, body) => {
|
|
195
192
|
try {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One mutation per file at a time, across every tool set in this isolate.
|
|
3
3
|
*
|
|
4
|
-
* `
|
|
4
|
+
* `edit` reads, replaces and writes back over RPC, and the AI SDK runs one
|
|
5
5
|
* step's tool calls concurrently — so two edits to one file would both report
|
|
6
|
-
* success and the first would be lost.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* success and the first would be lost. The workspace's `writeFile` takes the
|
|
7
|
+
* same lock, so Think's `write` never lands between an edit's read and its
|
|
8
|
+
* write. Module-level because a parent and a sub-agent can hold separate tool
|
|
9
|
+
* sets over one workspace; another isolate is out of reach.
|
|
9
10
|
*/
|
|
10
11
|
const tails = new Map();
|
|
11
12
|
/**
|