@bridge4dev/runner 0.36.0 → 0.38.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/dist/agent-binary.d.ts +28 -0
- package/dist/agent-binary.js +99 -0
- package/dist/index.js +14 -2
- package/dist/protocol.d.ts +74 -2
- package/dist/protocol.js +26 -12
- package/dist/self-update.js +49 -1
- package/dist/supervisor.d.ts +51 -3
- package/dist/supervisor.js +484 -246
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/ws-client.d.ts +28 -0
- package/dist/ws-client.js +33 -4
- package/package.json +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platform packages the SDK will look for, in its own order (`sdk.mjs`).
|
|
3
|
+
*
|
|
4
|
+
* BOTH linux variants are accepted rather than detecting musl the way the SDK
|
|
5
|
+
* does: the question here is «did the optional package get installed at all»,
|
|
6
|
+
* and answering it must never be the thing that blocks an update. A glibc
|
|
7
|
+
* binary on a musl host is a different failure — and since #225 it announces
|
|
8
|
+
* itself in the session feed instead of hiding.
|
|
9
|
+
*/
|
|
10
|
+
export declare function nativeCandidates(platform?: NodeJS.Platform, arch?: string): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Absolute path to the Claude CLI inside an installed runner package, or null.
|
|
13
|
+
*
|
|
14
|
+
* Resolved from the SDK's own file, exactly like the SDK resolves it — npm may
|
|
15
|
+
* hoist the platform package next to the SDK, next to the runner, or several
|
|
16
|
+
* levels up, and only the module resolver knows which happened here.
|
|
17
|
+
*/
|
|
18
|
+
export declare function findClaudeCli(packageDir: string): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* The same question about THIS process's own installation.
|
|
21
|
+
*
|
|
22
|
+
* Goes through the SDK's entry point rather than a path guess, so it answers
|
|
23
|
+
* correctly in every layout the runner runs in — a global npm install, a
|
|
24
|
+
* dedicated-user prefix, and the pnpm store of a source checkout, where the
|
|
25
|
+
* platform package is reachable from the SDK and from nowhere else.
|
|
26
|
+
*/
|
|
27
|
+
export declare function claudeCliPath(): string | null;
|
|
28
|
+
//# sourceMappingURL=agent-binary.d.ts.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Is the binary this runner would actually launch Claude with present?
|
|
6
|
+
*
|
|
7
|
+
* Not a hypothetical check (ticket #225, гоча #297). The Claude CLI does not
|
|
8
|
+
* live in this package: `@anthropic-ai/claude-agent-sdk` keeps it in a ~300 MB
|
|
9
|
+
* **optional** platform package, one per platform+arch. For npm the failure of
|
|
10
|
+
* an optional dependency is not an error — `npm install -g` exits 0, the runner
|
|
11
|
+
* reports a successful update and restarts into a build that cannot start a
|
|
12
|
+
* single Claude session. That is exactly what happened to transitway-dev-01 on
|
|
13
|
+
* 2026-08-11: every launch threw
|
|
14
|
+
* `Native CLI binary for linux-x64 not found` synchronously, before any event
|
|
15
|
+
* could be emitted, and a live session went silent for an hour and a half.
|
|
16
|
+
*
|
|
17
|
+
* The runner's own smoke test could not catch it: `devbridge-runner --version`
|
|
18
|
+
* loads the module graph, and the SDK resolves the platform binary lazily — on
|
|
19
|
+
* the first `query()`, which is a session, not a startup.
|
|
20
|
+
*/
|
|
21
|
+
const SDK_PACKAGE = '@anthropic-ai/claude-agent-sdk';
|
|
22
|
+
/**
|
|
23
|
+
* The platform packages the SDK will look for, in its own order (`sdk.mjs`).
|
|
24
|
+
*
|
|
25
|
+
* BOTH linux variants are accepted rather than detecting musl the way the SDK
|
|
26
|
+
* does: the question here is «did the optional package get installed at all»,
|
|
27
|
+
* and answering it must never be the thing that blocks an update. A glibc
|
|
28
|
+
* binary on a musl host is a different failure — and since #225 it announces
|
|
29
|
+
* itself in the session feed instead of hiding.
|
|
30
|
+
*/
|
|
31
|
+
export function nativeCandidates(platform = process.platform, arch = process.arch) {
|
|
32
|
+
const exe = platform === 'win32' ? 'claude.exe' : 'claude';
|
|
33
|
+
const packages = platform === 'android'
|
|
34
|
+
? [`${SDK_PACKAGE}-linux-${arch}-android`]
|
|
35
|
+
: platform === 'linux'
|
|
36
|
+
? [`${SDK_PACKAGE}-linux-${arch}`, `${SDK_PACKAGE}-linux-${arch}-musl`]
|
|
37
|
+
: [`${SDK_PACKAGE}-${platform}-${arch}`];
|
|
38
|
+
return packages.map((name) => `${name}/${exe}`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Absolute path to the Claude CLI inside an installed runner package, or null.
|
|
42
|
+
*
|
|
43
|
+
* Resolved from the SDK's own file, exactly like the SDK resolves it — npm may
|
|
44
|
+
* hoist the platform package next to the SDK, next to the runner, or several
|
|
45
|
+
* levels up, and only the module resolver knows which happened here.
|
|
46
|
+
*/
|
|
47
|
+
export function findClaudeCli(packageDir) {
|
|
48
|
+
const found = resolveFromSdkEntry(path.join(packageDir, 'node_modules', SDK_PACKAGE, 'sdk.mjs'));
|
|
49
|
+
// Inside THIS installation, or it does not count. Node's resolver walks up the
|
|
50
|
+
// directory tree and consults the global folders, so a package that is absent
|
|
51
|
+
// from the build we just installed can still be found in the one we are about
|
|
52
|
+
// to retire — and answering «present» from there is exactly the false green
|
|
53
|
+
// this check exists to prevent. A global npm install keeps its dependencies
|
|
54
|
+
// under its own package directory, so containment is also simply true.
|
|
55
|
+
return found && isInside(packageDir, found) ? found : null;
|
|
56
|
+
}
|
|
57
|
+
function isInside(dir, file) {
|
|
58
|
+
const relative = path.relative(path.resolve(dir), path.resolve(file));
|
|
59
|
+
return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The same question about THIS process's own installation.
|
|
63
|
+
*
|
|
64
|
+
* Goes through the SDK's entry point rather than a path guess, so it answers
|
|
65
|
+
* correctly in every layout the runner runs in — a global npm install, a
|
|
66
|
+
* dedicated-user prefix, and the pnpm store of a source checkout, where the
|
|
67
|
+
* platform package is reachable from the SDK and from nowhere else.
|
|
68
|
+
*/
|
|
69
|
+
export function claudeCliPath() {
|
|
70
|
+
let sdkEntry;
|
|
71
|
+
try {
|
|
72
|
+
sdkEntry = createRequire(import.meta.url).resolve(SDK_PACKAGE);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return resolveFromSdkEntry(sdkEntry);
|
|
78
|
+
}
|
|
79
|
+
function resolveFromSdkEntry(sdkEntry) {
|
|
80
|
+
let resolve;
|
|
81
|
+
try {
|
|
82
|
+
resolve = createRequire(sdkEntry).resolve;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
for (const candidate of nativeCandidates()) {
|
|
88
|
+
try {
|
|
89
|
+
const resolved = resolve(candidate);
|
|
90
|
+
if (fs.existsSync(resolved))
|
|
91
|
+
return resolved;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Not installed under this name — try the next candidate.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=agent-binary.js.map
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { promisify } from 'node:util';
|
|
|
8
8
|
import { ClaudeAdapter } from './adapters/claude.js';
|
|
9
9
|
import { CodexAdapter } from './adapters/codex.js';
|
|
10
10
|
import { ensureCodexHome } from './adapters/codex-home.js';
|
|
11
|
+
import { claudeCliPath } from './agent-binary.js';
|
|
11
12
|
import { loadConfig, requireConfig, saveConfig } from './config.js';
|
|
12
13
|
import { log } from './log.js';
|
|
13
14
|
import { installIsWritable, installPrefixFor, isSupervisedProcess, manualUpdateCommand, resolveInstalledPackageDir, } from './self-update.js';
|
|
@@ -62,8 +63,14 @@ function argValue(args, flag) {
|
|
|
62
63
|
*/
|
|
63
64
|
function installedAgents() {
|
|
64
65
|
const agents = [];
|
|
65
|
-
// Claude
|
|
66
|
-
|
|
66
|
+
// Claude comes with the Agent SDK — but «comes with» is a claim about THIS
|
|
67
|
+
// installation, not a law (ticket #225). The CLI is an optional platform
|
|
68
|
+
// package, and an update that silently lost it leaves a runner that reports
|
|
69
|
+
// Claude, accepts Claude sessions, and cannot start a single one. Reported as
|
|
70
|
+
// measured, so the dashboard greys the agent out instead of offering a
|
|
71
|
+
// session that dies before its first word.
|
|
72
|
+
if (claudeCliPath())
|
|
73
|
+
agents.push('claude');
|
|
67
74
|
if (hasExecutable('codex'))
|
|
68
75
|
agents.push('codex');
|
|
69
76
|
return agents;
|
|
@@ -389,6 +396,11 @@ function runnerCapabilities(apiUrlOverride) {
|
|
|
389
396
|
'agent_prompt_state',
|
|
390
397
|
'propose_commit_message',
|
|
391
398
|
'recall_message',
|
|
399
|
+
// The answer to a parked question, as a command that ANSWERS rather than
|
|
400
|
+
// a frame written into a socket and hoped for. Announced here because
|
|
401
|
+
// this list is what `runCommand` actually dispatches on, and the API
|
|
402
|
+
// falls back to the old frame for runners that do not name it.
|
|
403
|
+
'answer_question',
|
|
392
404
|
'compact_context',
|
|
393
405
|
...(checkpointsEnabled
|
|
394
406
|
? [
|
package/dist/protocol.d.ts
CHANGED
|
@@ -272,6 +272,78 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
272
272
|
workMode?: unknown;
|
|
273
273
|
}>;
|
|
274
274
|
export type SessionDescriptor = z.infer<typeof SessionDescriptorSchema>;
|
|
275
|
+
/**
|
|
276
|
+
* One human answer to a parked agent question, minus the session it belongs to.
|
|
277
|
+
*
|
|
278
|
+
* Written once and used twice on purpose: as the body of the `question_answer`
|
|
279
|
+
* frame every runner since 0.14 understands, and as the arguments of the
|
|
280
|
+
* `answer_question` COMMAND that replaced it (session 18). The two must not
|
|
281
|
+
* drift — an answer that the frame accepts and the command rejects would fail
|
|
282
|
+
* exactly on the machines whose connection is bad enough to need the command.
|
|
283
|
+
*/
|
|
284
|
+
export declare const QuestionAnswerShape: {
|
|
285
|
+
readonly askId: z.ZodString;
|
|
286
|
+
readonly action: z.ZodEnum<["answer", "discuss"]>;
|
|
287
|
+
readonly answers: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
288
|
+
questionId: z.ZodString;
|
|
289
|
+
values: z.ZodArray<z.ZodString, "many">;
|
|
290
|
+
custom: z.ZodOptional<z.ZodString>;
|
|
291
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
292
|
+
}, "strip", z.ZodTypeAny, {
|
|
293
|
+
values: string[];
|
|
294
|
+
questionId: string;
|
|
295
|
+
custom?: string | undefined;
|
|
296
|
+
notes?: string | undefined;
|
|
297
|
+
}, {
|
|
298
|
+
values: string[];
|
|
299
|
+
questionId: string;
|
|
300
|
+
custom?: string | undefined;
|
|
301
|
+
notes?: string | undefined;
|
|
302
|
+
}>, "many">>;
|
|
303
|
+
readonly text: z.ZodOptional<z.ZodString>;
|
|
304
|
+
};
|
|
305
|
+
/** The `answer_question` command's arguments — the shape above on its own. */
|
|
306
|
+
export declare const QuestionAnswerArgsSchema: z.ZodObject<{
|
|
307
|
+
readonly askId: z.ZodString;
|
|
308
|
+
readonly action: z.ZodEnum<["answer", "discuss"]>;
|
|
309
|
+
readonly answers: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
310
|
+
questionId: z.ZodString;
|
|
311
|
+
values: z.ZodArray<z.ZodString, "many">;
|
|
312
|
+
custom: z.ZodOptional<z.ZodString>;
|
|
313
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
314
|
+
}, "strip", z.ZodTypeAny, {
|
|
315
|
+
values: string[];
|
|
316
|
+
questionId: string;
|
|
317
|
+
custom?: string | undefined;
|
|
318
|
+
notes?: string | undefined;
|
|
319
|
+
}, {
|
|
320
|
+
values: string[];
|
|
321
|
+
questionId: string;
|
|
322
|
+
custom?: string | undefined;
|
|
323
|
+
notes?: string | undefined;
|
|
324
|
+
}>, "many">>;
|
|
325
|
+
readonly text: z.ZodOptional<z.ZodString>;
|
|
326
|
+
}, "strip", z.ZodTypeAny, {
|
|
327
|
+
askId: string;
|
|
328
|
+
action: "answer" | "discuss";
|
|
329
|
+
text?: string | undefined;
|
|
330
|
+
answers?: {
|
|
331
|
+
values: string[];
|
|
332
|
+
questionId: string;
|
|
333
|
+
custom?: string | undefined;
|
|
334
|
+
notes?: string | undefined;
|
|
335
|
+
}[] | undefined;
|
|
336
|
+
}, {
|
|
337
|
+
askId: string;
|
|
338
|
+
action: "answer" | "discuss";
|
|
339
|
+
text?: string | undefined;
|
|
340
|
+
answers?: {
|
|
341
|
+
values: string[];
|
|
342
|
+
questionId: string;
|
|
343
|
+
custom?: string | undefined;
|
|
344
|
+
notes?: string | undefined;
|
|
345
|
+
}[] | undefined;
|
|
346
|
+
}>;
|
|
275
347
|
export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
276
348
|
type: z.ZodLiteral<"hello_ack">;
|
|
277
349
|
serverId: z.ZodString;
|
|
@@ -1104,8 +1176,6 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1104
1176
|
requestId: string;
|
|
1105
1177
|
note?: string | undefined;
|
|
1106
1178
|
}>, z.ZodObject<{
|
|
1107
|
-
type: z.ZodLiteral<"question_answer">;
|
|
1108
|
-
sessionId: z.ZodString;
|
|
1109
1179
|
askId: z.ZodString;
|
|
1110
1180
|
action: z.ZodEnum<["answer", "discuss"]>;
|
|
1111
1181
|
answers: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -1125,6 +1195,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1125
1195
|
notes?: string | undefined;
|
|
1126
1196
|
}>, "many">>;
|
|
1127
1197
|
text: z.ZodOptional<z.ZodString>;
|
|
1198
|
+
type: z.ZodLiteral<"question_answer">;
|
|
1199
|
+
sessionId: z.ZodString;
|
|
1128
1200
|
}, "strip", z.ZodTypeAny, {
|
|
1129
1201
|
sessionId: string;
|
|
1130
1202
|
type: "question_answer";
|
package/dist/protocol.js
CHANGED
|
@@ -183,6 +183,31 @@ export const SessionDescriptorSchema = z.object({
|
|
|
183
183
|
// project). Preferred over the [mcp] section of config.toml when present.
|
|
184
184
|
mcp: z.object({ url: z.string().url(), token: z.string().min(1) }).optional(),
|
|
185
185
|
});
|
|
186
|
+
/**
|
|
187
|
+
* One human answer to a parked agent question, minus the session it belongs to.
|
|
188
|
+
*
|
|
189
|
+
* Written once and used twice on purpose: as the body of the `question_answer`
|
|
190
|
+
* frame every runner since 0.14 understands, and as the arguments of the
|
|
191
|
+
* `answer_question` COMMAND that replaced it (session 18). The two must not
|
|
192
|
+
* drift — an answer that the frame accepts and the command rejects would fail
|
|
193
|
+
* exactly on the machines whose connection is bad enough to need the command.
|
|
194
|
+
*/
|
|
195
|
+
export const QuestionAnswerShape = {
|
|
196
|
+
askId: z.string().min(1).max(64),
|
|
197
|
+
action: z.enum(['answer', 'discuss']),
|
|
198
|
+
answers: z
|
|
199
|
+
.array(z.object({
|
|
200
|
+
questionId: z.string().min(1).max(120),
|
|
201
|
+
values: z.array(z.string().max(2_000)).max(16),
|
|
202
|
+
custom: z.string().max(10_000).optional(),
|
|
203
|
+
notes: z.string().max(2_000).optional(),
|
|
204
|
+
}))
|
|
205
|
+
.max(4)
|
|
206
|
+
.optional(),
|
|
207
|
+
text: z.string().max(20_000).optional(),
|
|
208
|
+
};
|
|
209
|
+
/** The `answer_question` command's arguments — the shape above on its own. */
|
|
210
|
+
export const QuestionAnswerArgsSchema = z.object(QuestionAnswerShape);
|
|
186
211
|
export const GatewayFrameSchema = z.discriminatedUnion('type', [
|
|
187
212
|
z.object({
|
|
188
213
|
type: z.literal('hello_ack'),
|
|
@@ -238,18 +263,7 @@ export const GatewayFrameSchema = z.discriminatedUnion('type', [
|
|
|
238
263
|
z.object({
|
|
239
264
|
type: z.literal('question_answer'),
|
|
240
265
|
sessionId: z.string().uuid(),
|
|
241
|
-
|
|
242
|
-
action: z.enum(['answer', 'discuss']),
|
|
243
|
-
answers: z
|
|
244
|
-
.array(z.object({
|
|
245
|
-
questionId: z.string().min(1).max(120),
|
|
246
|
-
values: z.array(z.string().max(2_000)).max(16),
|
|
247
|
-
custom: z.string().max(10_000).optional(),
|
|
248
|
-
notes: z.string().max(2_000).optional(),
|
|
249
|
-
}))
|
|
250
|
-
.max(4)
|
|
251
|
-
.optional(),
|
|
252
|
-
text: z.string().max(20_000).optional(),
|
|
266
|
+
...QuestionAnswerShape,
|
|
253
267
|
}),
|
|
254
268
|
z.object({ type: z.literal('session_stop'), sessionId: z.string().uuid() }),
|
|
255
269
|
z.object({ type: z.literal('session_interrupt'), sessionId: z.string().uuid() }),
|
package/dist/self-update.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
+
import { findClaudeCli } from './agent-binary.js';
|
|
6
7
|
import { log } from './log.js';
|
|
7
8
|
import { stateDir } from './paths.js';
|
|
8
9
|
import { RUNNER_VERSION } from './version.js';
|
|
@@ -235,10 +236,19 @@ function installArgs(source, prefix) {
|
|
|
235
236
|
// a directory the daemon cannot write — and, on the rarer host where it can,
|
|
236
237
|
// npm cheerfully installs a SECOND copy somewhere the service does not exec,
|
|
237
238
|
// reports success, and the runner restarts on the old version forever.
|
|
239
|
+
//
|
|
240
|
+
// `--include=optional` is npm's default and is stated anyway (ticket #225):
|
|
241
|
+
// the Claude CLI ships as an OPTIONAL platform package, and a single
|
|
242
|
+
// `omit=optional` inherited from an `.npmrc`, an environment variable or a CI
|
|
243
|
+
// habit turns an update into a runner that cannot start a single Claude
|
|
244
|
+
// session — silently, because for npm a failed optional dependency is not a
|
|
245
|
+
// failure at all. The flag makes this deployment's intent explicit rather
|
|
246
|
+
// than dependent on whatever configuration the machine happens to carry.
|
|
238
247
|
return [
|
|
239
248
|
'install',
|
|
240
249
|
'-g',
|
|
241
250
|
'--ignore-scripts',
|
|
251
|
+
'--include=optional',
|
|
242
252
|
'--loglevel=error',
|
|
243
253
|
...(prefix ? ['--prefix', prefix] : []),
|
|
244
254
|
source,
|
|
@@ -259,7 +269,7 @@ export function manualUpdateCommand(tarballUrl, options = {}) {
|
|
|
259
269
|
const prefix = options.prefix === undefined ? installPrefixFor(packageDir) : options.prefix;
|
|
260
270
|
const user = options.user ?? os.userInfo().username;
|
|
261
271
|
const uid = options.uid ?? (typeof process.getuid === 'function' ? process.getuid() : -1);
|
|
262
|
-
const install = ['npm install -g --ignore-scripts --loglevel=error']
|
|
272
|
+
const install = ['npm install -g --ignore-scripts --include=optional --loglevel=error']
|
|
263
273
|
.concat(prefix ? [`--prefix ${prefix}`] : [])
|
|
264
274
|
.concat([tarballUrl])
|
|
265
275
|
.join(' ');
|
|
@@ -420,6 +430,44 @@ export async function selfUpdate(options) {
|
|
|
420
430
|
`Restore it on the server with: npm install -g${prefix ? ` --prefix ${prefix}` : ''} ${rollbackTarball}`, { rollbackTarball, ...(toVersion ? { toVersion } : {}) });
|
|
421
431
|
}
|
|
422
432
|
}
|
|
433
|
+
// The binary the sessions will actually be started with (ticket #225).
|
|
434
|
+
//
|
|
435
|
+
// The probe above is not enough and 2026-08-11 proved it: `--version` loads
|
|
436
|
+
// the module graph, but the SDK resolves the Claude CLI lazily — on the first
|
|
437
|
+
// `query()`, i.e. inside a session, long after this update reported success.
|
|
438
|
+
// A missing OPTIONAL platform package therefore sailed through every check
|
|
439
|
+
// here, the daemon restarted, and the machine spent an hour and a half
|
|
440
|
+
// answering «продолжай» with nothing.
|
|
441
|
+
//
|
|
442
|
+
// One repair attempt first, because that is what the failure usually deserves:
|
|
443
|
+
// a 300 MB optional package that did not download is a network hiccup, not a
|
|
444
|
+
// broken release, and reinstalling it is cheaper for the user than a rollback.
|
|
445
|
+
if (!findClaudeCli(newPackageDir)) {
|
|
446
|
+
log.warn('self-update: the Claude CLI is missing from the new build — repairing', {
|
|
447
|
+
packageDir: newPackageDir,
|
|
448
|
+
});
|
|
449
|
+
try {
|
|
450
|
+
await installGlobal(exec, options.tarballUrl, prefix);
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
log.warn('self-update: the repair install failed', { error: describe(error) });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (!findClaudeCli(newPackageDir)) {
|
|
457
|
+
log.error('self-update: still no Claude CLI after the repair — rolling back', {
|
|
458
|
+
packageDir: newPackageDir,
|
|
459
|
+
});
|
|
460
|
+
const detail = 'the Claude CLI (an optional platform package of @anthropic-ai/claude-agent-sdk) did not install, ' +
|
|
461
|
+
'so no Claude session could have started on this build';
|
|
462
|
+
try {
|
|
463
|
+
await installGlobal(exec, rollbackTarball, prefix);
|
|
464
|
+
return fail(`The new version was not activated (${detail}). The previous version was restored and the runner keeps working.`, { rollbackTarball, ...(toVersion ? { toVersion } : {}) });
|
|
465
|
+
}
|
|
466
|
+
catch (rollbackError) {
|
|
467
|
+
return fail(`The new version is broken (${detail}) and the rollback failed too (${describe(rollbackError)}). ` +
|
|
468
|
+
`Restore it on the server with: npm install -g --include=optional${prefix ? ` --prefix ${prefix}` : ''} ${rollbackTarball}`, { rollbackTarball, ...(toVersion ? { toVersion } : {}) });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
423
471
|
// The service unit may be pinned to a file inside the directory this update
|
|
424
472
|
// just replaced — early versions wrote the resolved script path, and a package
|
|
425
473
|
// rename moves it. Then the restart we are about to ask for would fail with
|
package/dist/supervisor.d.ts
CHANGED
|
@@ -19,6 +19,12 @@ export interface SupervisorOptions {
|
|
|
19
19
|
runnerToken?: string;
|
|
20
20
|
/** Test seam for the `self_update` command. */
|
|
21
21
|
selfUpdate?: typeof selfUpdate;
|
|
22
|
+
/**
|
|
23
|
+
* How long a freshly started agent process may say nothing before the session
|
|
24
|
+
* says so out loud (ticket #225). Test seam — the default is a minute, and a
|
|
25
|
+
* test that had to wait one would not be written.
|
|
26
|
+
*/
|
|
27
|
+
startupSilenceMs?: number;
|
|
22
28
|
/**
|
|
23
29
|
* Called after a successful update, once the reply is on the wire. The daemon
|
|
24
30
|
* exits here and systemd starts the new build; without a handler the runner
|
|
@@ -113,11 +119,53 @@ export declare class Supervisor {
|
|
|
113
119
|
* free CHAT session with no prompt at all (the agent boots, reports its
|
|
114
120
|
* capabilities and waits for the first message).
|
|
115
121
|
*
|
|
116
|
-
* Returns
|
|
117
|
-
*
|
|
118
|
-
*
|
|
122
|
+
* Returns what came of it: a caller holding a user message needs to know,
|
|
123
|
+
* because anything but `ok` means the message has to stay queued rather than
|
|
124
|
+
* be marked delivered (session 9).
|
|
125
|
+
*
|
|
126
|
+
* NEVER throws (ticket #225). Everything from reading the project prompt to
|
|
127
|
+
* the adapter's own constructor runs inside one guard, because the caller
|
|
128
|
+
* chain above cannot tell the difference between «did not start» and
|
|
129
|
+
* «threw»: the message path swallows the exception into a log line, and the
|
|
130
|
+
* reconnect path lets it abort the restore of every OTHER session on the
|
|
131
|
+
* machine. A launch that fails is a state this session reports, not an
|
|
132
|
+
* exception somebody else has to remember to catch.
|
|
119
133
|
*/
|
|
120
134
|
private launchAgent;
|
|
135
|
+
/**
|
|
136
|
+
* The agent process could not be started at all (ticket #225).
|
|
137
|
+
*
|
|
138
|
+
* Three things have to happen here, and until this ticket none of them did:
|
|
139
|
+
* the reason is said WHERE THE PERSON IS LOOKING (an `error` event is the
|
|
140
|
+
* feed's red line), the session stops claiming to be working, and the stack
|
|
141
|
+
* reaches journald for whoever has to fix the machine. The status is the
|
|
142
|
+
* honest one for a session with no process — the agent is not running, and a
|
|
143
|
+
* session left in `RUNNING` shows a stop button for a turn that does not
|
|
144
|
+
* exist.
|
|
145
|
+
*
|
|
146
|
+
* Never rethrows: this IS the handling. The caller gets `crashed` and decides
|
|
147
|
+
* what to do with the message it was holding.
|
|
148
|
+
*/
|
|
149
|
+
private launchCrashed;
|
|
150
|
+
/** How long a freshly started agent may say nothing before we say so. */
|
|
151
|
+
private static readonly STARTUP_SILENCE_MS;
|
|
152
|
+
/**
|
|
153
|
+
* Watch for the first word out of a process we just started (ticket #225).
|
|
154
|
+
*
|
|
155
|
+
* «The adapter object exists» is not «the agent is running». A CLI that hangs
|
|
156
|
+
* before its first frame — a stuck hook, an MCP server that never answers, a
|
|
157
|
+
* transcript it cannot read — produces no events, no error and no exit, and
|
|
158
|
+
* the session sits in `RUNNING` forever. On a healthy launch the first event
|
|
159
|
+
* arrives in about two seconds, so a minute of silence is not a slow start,
|
|
160
|
+
* it is something worth saying out loud.
|
|
161
|
+
*
|
|
162
|
+
* Says it and stops there: no kill. A long conversation has the right to boot
|
|
163
|
+
* slowly, and killing it would cost the person the very turn they are waiting
|
|
164
|
+
* for.
|
|
165
|
+
*/
|
|
166
|
+
private watchForFirstSignOfLife;
|
|
167
|
+
/** The process spoke, or went away — either way the watch is over. */
|
|
168
|
+
private clearStartupWatch;
|
|
121
169
|
/**
|
|
122
170
|
* The project's own prompt file, read fresh for THIS agent process.
|
|
123
171
|
*
|