@vellumai/cli 0.11.7-staging.2 → 0.11.8-staging.1
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/AGENTS.md +1 -1
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
- package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/node_modules/@vellumai/local-mode/src/hatch.ts +17 -2
- package/node_modules/@vellumai/local-mode/src/index.ts +5 -0
- package/node_modules/@vellumai/local-mode/src/lifecycle-budgets.ts +47 -0
- package/node_modules/@vellumai/local-mode/src/retire.ts +9 -3
- package/node_modules/@vellumai/local-mode/src/sleep.ts +5 -5
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/package.json +4 -3
- package/src/__tests__/assistant-client-platform-auth.test.ts +219 -0
- package/src/__tests__/clean.test.ts +1 -0
- package/src/__tests__/nginx-ingress.test.ts +123 -8
- package/src/__tests__/retire-local.test.ts +32 -1
- package/src/__tests__/sleep.test.ts +5 -3
- package/src/__tests__/teleport.test.ts +1 -0
- package/src/__tests__/upgrade-local.test.ts +116 -0
- package/src/__tests__/windows-lifecycle.test.ts +77 -5
- package/src/adapters/openclaw-http-server.ts +5 -0
- package/src/commands/client.ts +35 -7
- package/src/commands/exec.ts +8 -2
- package/src/commands/logs.ts +11 -2
- package/src/commands/sleep.ts +6 -9
- package/src/commands/ssh.ts +6 -3
- package/src/commands/teleport.ts +6 -1
- package/src/commands/upgrade.ts +22 -12
- package/src/commands/voice.test.ts +81 -0
- package/src/commands/voice.ts +83 -6
- package/src/commands/wake.ts +7 -1
- package/src/lib/assistant-client.ts +145 -31
- package/src/lib/cloudflare-tunnel.ts +2 -1
- package/src/lib/detached-process.ts +1 -0
- package/src/lib/guardian-token.ts +4 -2
- package/src/lib/hatch-local.ts +5 -1
- package/src/lib/live-voice/client.ts +23 -2
- package/src/lib/live-voice/connection.ts +87 -10
- package/src/lib/live-voice/pcm-player.ts +16 -3
- package/src/lib/live-voice/velay.test.ts +99 -0
- package/src/lib/live-voice/velay.ts +199 -0
- package/src/lib/local-hatch-lock.ts +16 -3
- package/src/lib/local.ts +48 -13
- package/src/lib/nginx-ingress.ts +70 -10
- package/src/lib/ngrok.ts +2 -0
- package/src/lib/open-browser.ts +5 -1
- package/src/lib/process.ts +72 -10
- package/src/lib/provider-secrets.ts +5 -0
- package/src/lib/retire-local.ts +12 -5
- package/src/lib/step-runner.ts +3 -0
- package/src/lib/tailscale-tunnel.ts +6 -1
- package/src/lib/upgrade-lifecycle.ts +1 -0
package/src/lib/nginx-ingress.ts
CHANGED
|
@@ -180,7 +180,7 @@ function remoteWebIngressConfig(
|
|
|
180
180
|
* fingerprint matches, so this must change whenever the generated index or
|
|
181
181
|
* nginx template does.
|
|
182
182
|
*/
|
|
183
|
-
const EDGE_TEMPLATE_VERSION =
|
|
183
|
+
export const EDGE_TEMPLATE_VERSION = 7;
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
186
|
* Stable fingerprint of the SPA config injected into the served index and
|
|
@@ -201,19 +201,56 @@ function safeScriptJson(value: unknown): string {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
204
|
+
* How many modulepreload hints the served index may carry before they are
|
|
205
|
+
* stripped wholesale.
|
|
206
|
+
*
|
|
207
|
+
* The consolidated boot graph emits 4 hints (the entry chunk's static
|
|
208
|
+
* imports). Serving them lets a phone fetch the whole boot set in parallel;
|
|
209
|
+
* without them the browser cannot discover the other chunks until the entry
|
|
210
|
+
* has downloaded AND parsed, so ~515 kB gzip serializes behind ~623 kB plus
|
|
211
|
+
* a phone-CPU parse of ~2.2 MB of JavaScript.
|
|
212
|
+
*
|
|
213
|
+
* The strip exists for the pathological shape: a whole-chunk-graph index
|
|
214
|
+
* (~290 hints) floods the tunnel with requests, and one dropped *import*
|
|
215
|
+
* blanks the app before React can report it. A build that regresses toward
|
|
216
|
+
* that shape trips this threshold and gets the protective strip.
|
|
217
|
+
* All-or-nothing, because preserving an arbitrary prefix of a broken graph
|
|
218
|
+
* would be luck, not policy. 12 sits comfortably above the consolidated
|
|
219
|
+
* graph and within two rounds of the browser's six-per-origin HTTP/1.1
|
|
220
|
+
* connection pool, so kept hints never widen concurrency much beyond what
|
|
221
|
+
* the entry's own imports would open.
|
|
222
|
+
*/
|
|
223
|
+
export const MAX_PRESERVED_MODULE_PRELOADS = 12;
|
|
224
|
+
|
|
225
|
+
const MODULE_PRELOAD_RE = /<link[^>]+rel="modulepreload"[^>]*>\s*/g;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Failure semantics are engine-dependent while whatwg/html#10327 rolls out:
|
|
229
|
+
* older engines cache a failed module fetch in the module map (a later
|
|
230
|
+
* import returns the failure without refetching), newer ones evict it so a
|
|
231
|
+
* later import refetches. The invariant that matters holds in both models:
|
|
232
|
+
* the preserved hints name exactly the URLs the entry's static imports
|
|
233
|
+
* fetch anyway, through the same module map, so a dropped request for a
|
|
234
|
+
* BOOT chunk has the same outcome whether or not the chunk was hinted.
|
|
235
|
+
* Hints for the boot set add no failure surface; they only start the same
|
|
236
|
+
* fetches earlier. What the strip guards against is the whole-graph shape,
|
|
237
|
+
* where hundreds of speculative NON-boot fetches amplify the odds of any
|
|
238
|
+
* failure and (on old-model engines) a dropped lazy chunk poisons a later
|
|
239
|
+
* dynamic import.
|
|
207
240
|
*/
|
|
208
|
-
function
|
|
209
|
-
|
|
241
|
+
function stripExcessModulePreloads(html: string): string {
|
|
242
|
+
const count = html.match(MODULE_PRELOAD_RE)?.length ?? 0;
|
|
243
|
+
if (count <= MAX_PRESERVED_MODULE_PRELOADS) {
|
|
244
|
+
return html;
|
|
245
|
+
}
|
|
246
|
+
return html.replace(MODULE_PRELOAD_RE, "");
|
|
210
247
|
}
|
|
211
248
|
|
|
212
249
|
export function buildRemoteWebIndexHtml(
|
|
213
250
|
rawHtml: string,
|
|
214
251
|
config: Record<string, unknown>,
|
|
215
252
|
): string {
|
|
216
|
-
const html =
|
|
253
|
+
const html = stripExcessModulePreloads(rawHtml);
|
|
217
254
|
const script = `<script>window.__VELLUM_CONFIG__=${safeScriptJson(config)}</script>`;
|
|
218
255
|
if (html.includes("</head>")) {
|
|
219
256
|
return html.replace("</head>", `${script}</head>`);
|
|
@@ -274,6 +311,19 @@ http {
|
|
|
274
311
|
access_log off;
|
|
275
312
|
default_type application/octet-stream;
|
|
276
313
|
|
|
314
|
+
# Compression tuning only. It stays inert here because nginx ships with
|
|
315
|
+
# gzip off, and only the static SPA locations below turn it on: this server
|
|
316
|
+
# also proxies authenticated /v1 and /webhooks traffic, and compressing a
|
|
317
|
+
# response that carries both a secret and attacker-influenced content over
|
|
318
|
+
# TLS is the BREACH side channel. The SPA bytes are the whole win, so the
|
|
319
|
+
# boundary is drawn by opting locations in rather than by excluding proxies.
|
|
320
|
+
# text/html is always compressed once gzip is on, so it must not be
|
|
321
|
+
# repeated in gzip_types.
|
|
322
|
+
gzip_vary on;
|
|
323
|
+
gzip_comp_level 5;
|
|
324
|
+
gzip_min_length 1024;
|
|
325
|
+
gzip_types application/javascript application/json application/wasm image/svg+xml text/css text/plain;
|
|
326
|
+
|
|
277
327
|
types {
|
|
278
328
|
application/javascript js mjs;
|
|
279
329
|
application/json json map;
|
|
@@ -369,10 +419,16 @@ ${proxyBlock}
|
|
|
369
419
|
rewrite ^ /assistant/__remote-index.html last;
|
|
370
420
|
}
|
|
371
421
|
|
|
422
|
+
# The shell and the unhashed files beside it revalidate rather than
|
|
423
|
+
# refusing storage: they carry no credential, and nginx serves them from
|
|
424
|
+
# disk with a validator, so a repeat load costs a 304 instead of the whole
|
|
425
|
+
# document. __config is the exception below: it is returned inline, so
|
|
426
|
+
# nginx attaches no validator for a revalidation to match against.
|
|
372
427
|
location = /assistant/__remote-index.html {
|
|
373
428
|
internal;
|
|
429
|
+
gzip on;
|
|
374
430
|
alias ${nginxQuoted(indexHtmlPath, "remote web ingress index path")};
|
|
375
|
-
add_header Cache-Control "no-
|
|
431
|
+
add_header Cache-Control "no-cache";
|
|
376
432
|
}
|
|
377
433
|
|
|
378
434
|
location = /assistant/__config {
|
|
@@ -382,15 +438,17 @@ ${proxyBlock}
|
|
|
382
438
|
}
|
|
383
439
|
|
|
384
440
|
location ^~ /assistant/assets/ {
|
|
441
|
+
gzip on;
|
|
385
442
|
alias ${nginxQuoted(nginxDirPath(webAssetsDir), "web assets path")};
|
|
386
443
|
try_files $uri =404;
|
|
387
444
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
388
445
|
}
|
|
389
446
|
|
|
390
447
|
location ^~ /assistant/ {
|
|
448
|
+
gzip on;
|
|
391
449
|
alias ${nginxQuoted(webDistDir, "web dist path")};
|
|
392
450
|
try_files $uri $uri/ /assistant/__remote-index.html;
|
|
393
|
-
add_header Cache-Control "no-
|
|
451
|
+
add_header Cache-Control "no-cache";
|
|
394
452
|
}
|
|
395
453
|
|
|
396
454
|
location = / {
|
|
@@ -415,6 +473,7 @@ export function getNginxVersion(): string | null {
|
|
|
415
473
|
const result = spawnSync(nginxBin(), ["-v"], {
|
|
416
474
|
encoding: "utf-8",
|
|
417
475
|
timeout: 5_000,
|
|
476
|
+
windowsHide: true,
|
|
418
477
|
});
|
|
419
478
|
if (result.error || result.status !== 0) return null;
|
|
420
479
|
const output = `${result.stderr || ""}${result.stdout || ""}`.trim();
|
|
@@ -464,6 +523,7 @@ function isIngressNginxProcess(pid: number, paths: IngressPaths): boolean {
|
|
|
464
523
|
encoding: "utf-8",
|
|
465
524
|
timeout: 3000,
|
|
466
525
|
stdio: ["ignore", "pipe", "ignore"],
|
|
526
|
+
windowsHide: true,
|
|
467
527
|
},
|
|
468
528
|
).trim();
|
|
469
529
|
return (
|
|
@@ -604,7 +664,7 @@ export function startIngressNginx(opts: {
|
|
|
604
664
|
const child = spawn(
|
|
605
665
|
nginxBin(),
|
|
606
666
|
["-p", paths.dir, "-c", paths.confPath, "-g", "daemon off;"],
|
|
607
|
-
{ detached: true, stdio: ["ignore", fd, fd] },
|
|
667
|
+
{ detached: true, stdio: ["ignore", fd, fd], windowsHide: true },
|
|
608
668
|
);
|
|
609
669
|
closeSync(fd);
|
|
610
670
|
|
package/src/lib/ngrok.ts
CHANGED
|
@@ -36,6 +36,7 @@ export function getNgrokVersion(): string | null {
|
|
|
36
36
|
encoding: "utf-8",
|
|
37
37
|
timeout: 5_000,
|
|
38
38
|
stdio: ["ignore", "pipe", "ignore"],
|
|
39
|
+
windowsHide: true,
|
|
39
40
|
});
|
|
40
41
|
return output.trim();
|
|
41
42
|
} catch {
|
|
@@ -152,6 +153,7 @@ export function startNgrokProcess(
|
|
|
152
153
|
const child = spawn("ngrok", args, {
|
|
153
154
|
detached: true,
|
|
154
155
|
stdio,
|
|
156
|
+
windowsHide: true,
|
|
155
157
|
});
|
|
156
158
|
|
|
157
159
|
// The child process inherits a duplicate of the fd via dup2, so the
|
package/src/lib/open-browser.ts
CHANGED
|
@@ -12,7 +12,11 @@ export function openBrowser(url: string): void {
|
|
|
12
12
|
platform === "win32"
|
|
13
13
|
? ["/c", "start", '""', url.replace(/&/g, "^&")]
|
|
14
14
|
: [url];
|
|
15
|
-
const child = spawn(cmd, args, {
|
|
15
|
+
const child = spawn(cmd, args, {
|
|
16
|
+
stdio: "ignore",
|
|
17
|
+
detached: true,
|
|
18
|
+
windowsHide: true,
|
|
19
|
+
});
|
|
16
20
|
child.on("error", () => {
|
|
17
21
|
// Silently ignore — the user can still copy the URL from the console.
|
|
18
22
|
});
|
package/src/lib/process.ts
CHANGED
|
@@ -76,13 +76,22 @@ export function windowsCommandLineLookupArgs(pid: number): string[] {
|
|
|
76
76
|
];
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
/** Read a process command line using the platform's process inspection tool. */
|
|
80
|
+
export function readProcessCommandLine(
|
|
81
|
+
pid: number,
|
|
82
|
+
hostPlatform: NodeJS.Platform = platform(),
|
|
83
|
+
): string {
|
|
84
|
+
const command = hostPlatform === "win32" ? "powershell.exe" : "ps";
|
|
85
|
+
const args =
|
|
86
|
+
hostPlatform === "win32"
|
|
87
|
+
? windowsCommandLineLookupArgs(pid)
|
|
88
|
+
: ["-p", String(pid), "-o", "command="];
|
|
89
|
+
return execFileSync(command, args, {
|
|
81
90
|
windowsHide: true,
|
|
82
91
|
encoding: "utf8",
|
|
83
92
|
timeout: 3000,
|
|
84
93
|
stdio: ["ignore", "pipe", "ignore"],
|
|
85
|
-
});
|
|
94
|
+
}).trim();
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
export function isVellumWindowsProcess(
|
|
@@ -102,6 +111,21 @@ export function isVellumWindowsProcess(
|
|
|
102
111
|
return /^bun\.exe$/i.test(imageName) && isVellumCommandLine(commandLine);
|
|
103
112
|
}
|
|
104
113
|
|
|
114
|
+
/**
|
|
115
|
+
* The POSIX command line for `pid`, or null when it cannot be read.
|
|
116
|
+
*
|
|
117
|
+
* Used to prove a PID still names the same process across an awaited gap. The
|
|
118
|
+
* Windows path never needs it: `taskkill /T` already terminates the tree, so
|
|
119
|
+
* no group escalation happens there.
|
|
120
|
+
*/
|
|
121
|
+
export function readPosixCommandLine(pid: number): string | null {
|
|
122
|
+
try {
|
|
123
|
+
return readProcessCommandLine(pid, platform());
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
105
129
|
export function isVellumProcess(
|
|
106
130
|
pid: number,
|
|
107
131
|
hostPlatform: NodeJS.Platform = platform(),
|
|
@@ -110,15 +134,11 @@ export function isVellumProcess(
|
|
|
110
134
|
if (hostPlatform === "win32") {
|
|
111
135
|
const imageName = readWindowsProcesses(pid)[0]?.imageName ?? "";
|
|
112
136
|
const commandLine = /^(?:bun|qdrant)\.exe$/i.test(imageName)
|
|
113
|
-
?
|
|
137
|
+
? readProcessCommandLine(pid, hostPlatform)
|
|
114
138
|
: "";
|
|
115
139
|
return isVellumWindowsProcess(imageName, commandLine);
|
|
116
140
|
}
|
|
117
|
-
const output =
|
|
118
|
-
encoding: "utf-8",
|
|
119
|
-
timeout: 3000,
|
|
120
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
121
|
-
}).trim();
|
|
141
|
+
const output = readProcessCommandLine(pid, hostPlatform);
|
|
122
142
|
return isVellumCommandLine(output);
|
|
123
143
|
} catch {
|
|
124
144
|
return false;
|
|
@@ -277,6 +297,15 @@ export async function resolveProcessState(
|
|
|
277
297
|
return { status: "healthy", pid: result.pid };
|
|
278
298
|
}
|
|
279
299
|
|
|
300
|
+
/**
|
|
301
|
+
* SIGKILL ceiling for stopping the assistant daemon.
|
|
302
|
+
*
|
|
303
|
+
* Defined in `@vellumai/local-mode` because the host wrappers that spawn these
|
|
304
|
+
* commands derive their own timeouts from it: a wrapper that expires first
|
|
305
|
+
* kills the CLI before this ceiling is ever reached.
|
|
306
|
+
*/
|
|
307
|
+
export { DAEMON_STOP_TIMEOUT_MS } from "@vellumai/local-mode";
|
|
308
|
+
|
|
280
309
|
/**
|
|
281
310
|
* Stop a process by PID: SIGTERM, wait up to `timeoutMs`, then SIGKILL if still alive.
|
|
282
311
|
* Returns true if the process was stopped, false if it wasn't alive or
|
|
@@ -294,6 +323,7 @@ export async function stopProcess(
|
|
|
294
323
|
stdio: "ignore",
|
|
295
324
|
});
|
|
296
325
|
},
|
|
326
|
+
readIdentity: (pid: number) => string | null = readPosixCommandLine,
|
|
297
327
|
): Promise<boolean> {
|
|
298
328
|
try {
|
|
299
329
|
process.kill(pid, 0);
|
|
@@ -301,6 +331,11 @@ export async function stopProcess(
|
|
|
301
331
|
return false;
|
|
302
332
|
}
|
|
303
333
|
|
|
334
|
+
// Snapshot who this PID is before any waiting, so the group escalation below
|
|
335
|
+
// can prove it is still the same process rather than a recycled PID.
|
|
336
|
+
const identityBeforeWait =
|
|
337
|
+
hostPlatform === "win32" ? null : readIdentity(pid);
|
|
338
|
+
|
|
304
339
|
console.log(`Stopping ${label} (pid ${pid})...`);
|
|
305
340
|
let waitForGracefulExit = true;
|
|
306
341
|
if (hostPlatform === "win32") {
|
|
@@ -343,6 +378,32 @@ export async function stopProcess(
|
|
|
343
378
|
}
|
|
344
379
|
}
|
|
345
380
|
console.log(`${label} did not exit after SIGTERM, sending SIGKILL...`);
|
|
381
|
+
// Kill the process group, not just the process. Daemons are spawned
|
|
382
|
+
// `detached: true` (a session and group leader), and their worker
|
|
383
|
+
// subprocesses inherit that group, so a single-PID SIGKILL here is what
|
|
384
|
+
// orphans workers onto init running a stale runtime. Group-kill takes the
|
|
385
|
+
// whole tree at once, which is what the Windows branch above already does
|
|
386
|
+
// via `taskkill /T`. Tool subprocesses run in their own groups (see
|
|
387
|
+
// assistant orphan-reaper), so they are unaffected. SIGTERM above stays
|
|
388
|
+
// single-PID on purpose: graceful shutdown is the daemon's to orchestrate,
|
|
389
|
+
// and a group-wide SIGTERM would kill the route host mid-HTTP-drain.
|
|
390
|
+
//
|
|
391
|
+
// Widening a signal to a whole group demands more than liveness: the target
|
|
392
|
+
// may have exited during the wait and the OS may have handed its PID to an
|
|
393
|
+
// unrelated group leader. Escalate only while the command line still matches
|
|
394
|
+
// the one captured before the wait; otherwise fall through to the
|
|
395
|
+
// single-PID kill this function has always sent.
|
|
396
|
+
const identityHolds =
|
|
397
|
+
identityBeforeWait != null && readIdentity(pid) === identityBeforeWait;
|
|
398
|
+
if (identityHolds) {
|
|
399
|
+
try {
|
|
400
|
+
process.kill(-pid, "SIGKILL");
|
|
401
|
+
return true;
|
|
402
|
+
} catch {
|
|
403
|
+
// `pid` does not lead a group (or the group is already gone). Fall back
|
|
404
|
+
// to the single-PID kill.
|
|
405
|
+
}
|
|
406
|
+
}
|
|
346
407
|
try {
|
|
347
408
|
process.kill(pid, "SIGKILL");
|
|
348
409
|
return true;
|
|
@@ -428,7 +489,7 @@ export async function stopOrphanedDaemonProcesses(
|
|
|
428
489
|
(/^vellum-daemon\.exe$/i.test(imageName) ||
|
|
429
490
|
(/^bun\.exe$/i.test(imageName) &&
|
|
430
491
|
/vellum-daemon|[\\/]assistant[\\/]src[\\/](?:index|daemon[\\/]main)\.ts/i.test(
|
|
431
|
-
|
|
492
|
+
readProcessCommandLine(pid, hostPlatform),
|
|
432
493
|
))),
|
|
433
494
|
)
|
|
434
495
|
.map(({ pid }) =>
|
|
@@ -446,6 +507,7 @@ export async function stopOrphanedDaemonProcesses(
|
|
|
446
507
|
encoding: "utf-8",
|
|
447
508
|
timeout: 5000,
|
|
448
509
|
stdio: ["ignore", "pipe", "ignore"],
|
|
510
|
+
windowsHide: true,
|
|
449
511
|
});
|
|
450
512
|
} catch {
|
|
451
513
|
return false;
|
|
@@ -243,6 +243,11 @@ export function inferProviderFromModel(model: string): string | undefined {
|
|
|
243
243
|
if (model.startsWith("poolside/")) {
|
|
244
244
|
return "poolside";
|
|
245
245
|
}
|
|
246
|
+
if (model === "qwen/qwen3-8b") {
|
|
247
|
+
// The only qwen/* ID the Vellum hosted provider carries; the rest are
|
|
248
|
+
// OpenRouter's.
|
|
249
|
+
return "vellum";
|
|
250
|
+
}
|
|
246
251
|
if (model.includes("/")) {
|
|
247
252
|
return "openrouter";
|
|
248
253
|
}
|
package/src/lib/retire-local.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { AssistantEntry } from "./assistant-config.js";
|
|
|
12
12
|
import { stopIngressNginx } from "./nginx-ingress.js";
|
|
13
13
|
import { getKnownPidsFromAssistants } from "./orphan-detection.js";
|
|
14
14
|
import {
|
|
15
|
+
DAEMON_STOP_TIMEOUT_MS,
|
|
15
16
|
stopOrphanedDaemonProcesses,
|
|
16
17
|
stopProcessByPidFile,
|
|
17
18
|
} from "./process.js";
|
|
@@ -119,7 +120,12 @@ export async function retireLocal(
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
const daemonPidFile = getDaemonPidPath(resources);
|
|
122
|
-
const daemonStopped = await stopProcessByPidFile(
|
|
123
|
+
const daemonStopped = await stopProcessByPidFile(
|
|
124
|
+
daemonPidFile,
|
|
125
|
+
"daemon",
|
|
126
|
+
undefined,
|
|
127
|
+
DAEMON_STOP_TIMEOUT_MS,
|
|
128
|
+
);
|
|
123
129
|
|
|
124
130
|
// Stop gateway via PID file — use a longer timeout because the gateway has a
|
|
125
131
|
// drain window (5s) before it exits.
|
|
@@ -137,10 +143,10 @@ export async function retireLocal(
|
|
|
137
143
|
reporter.log("credential-executor stopped.");
|
|
138
144
|
}
|
|
139
145
|
|
|
140
|
-
// Stop Qdrant
|
|
141
|
-
// qdrantManager.stop(), but
|
|
142
|
-
//
|
|
143
|
-
// location and the legacy
|
|
146
|
+
// Stop Qdrant. The daemon's graceful shutdown tries to stop it via
|
|
147
|
+
// qdrantManager.stop(), but a daemon that never completes that shutdown
|
|
148
|
+
// (crash, OOM kill, or the SIGKILL ceiling above) leaves Qdrant running as
|
|
149
|
+
// an orphan. Check both the current PID file location and the legacy one.
|
|
144
150
|
const qdrantPidFile = join(
|
|
145
151
|
vellumDir,
|
|
146
152
|
"workspace",
|
|
@@ -211,6 +217,7 @@ export async function retireLocal(
|
|
|
211
217
|
const child = spawn(archiveCommand.command, archiveCommand.args, {
|
|
212
218
|
stdio: "ignore",
|
|
213
219
|
detached: true,
|
|
220
|
+
windowsHide: true,
|
|
214
221
|
...(archiveCommand.env
|
|
215
222
|
? { env: { ...process.env, ...archiveCommand.env } }
|
|
216
223
|
: {}),
|
package/src/lib/step-runner.ts
CHANGED
|
@@ -42,6 +42,7 @@ export function exec(
|
|
|
42
42
|
const child = spawn(command, args, {
|
|
43
43
|
cwd: options.cwd,
|
|
44
44
|
stdio: ["pipe", "pipe", "pipe"],
|
|
45
|
+
windowsHide: true,
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
let stdout = "";
|
|
@@ -82,6 +83,7 @@ export function execWithStdin(
|
|
|
82
83
|
const child = spawn(command, args, {
|
|
83
84
|
cwd: options.cwd,
|
|
84
85
|
stdio: ["pipe", "pipe", "pipe"],
|
|
86
|
+
windowsHide: true,
|
|
85
87
|
});
|
|
86
88
|
|
|
87
89
|
let stdout = "";
|
|
@@ -116,6 +118,7 @@ export function execOutput(
|
|
|
116
118
|
const child = spawn(command, args, {
|
|
117
119
|
cwd: options.cwd,
|
|
118
120
|
stdio: ["pipe", "pipe", "pipe"],
|
|
121
|
+
windowsHide: true,
|
|
119
122
|
});
|
|
120
123
|
|
|
121
124
|
let settled = false;
|
|
@@ -88,6 +88,7 @@ function realFindBinary(): string | null {
|
|
|
88
88
|
encoding: "utf-8",
|
|
89
89
|
timeout: 5_000,
|
|
90
90
|
stdio: ["ignore", "pipe", "ignore"],
|
|
91
|
+
windowsHide: true,
|
|
91
92
|
});
|
|
92
93
|
if (!res.error && res.status === 0) {
|
|
93
94
|
return candidate;
|
|
@@ -97,7 +98,11 @@ function realFindBinary(): string | null {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
function realRun(bin: string, args: string[]): TailscaleCommandResult {
|
|
100
|
-
const res = spawnSync(bin, args, {
|
|
101
|
+
const res = spawnSync(bin, args, {
|
|
102
|
+
encoding: "utf-8",
|
|
103
|
+
timeout: 15_000,
|
|
104
|
+
windowsHide: true,
|
|
105
|
+
});
|
|
101
106
|
if (res.error) {
|
|
102
107
|
throw res.error;
|
|
103
108
|
}
|