@vellumai/cli 0.10.5-dev.202607031338.eae4811 → 0.10.5-dev.202607031543.075941f
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/package.json +1 -1
- package/src/__tests__/sleep.test.ts +10 -2
- package/src/commands/recover.ts +9 -3
- package/src/commands/sleep.ts +12 -0
- package/src/commands/upgrade.ts +11 -3
- package/src/commands/wake.ts +18 -2
- package/src/lib/hatch-local.ts +12 -4
- package/src/lib/local.ts +201 -0
- package/src/lib/process.ts +1 -1
package/package.json
CHANGED
|
@@ -139,7 +139,8 @@ describe("sleep command", () => {
|
|
|
139
139
|
consoleLog.mockRestore();
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
// assistant + gateway + CES sibling
|
|
143
|
+
expect(stopProcessByPidFileMock).toHaveBeenCalledTimes(3);
|
|
143
144
|
});
|
|
144
145
|
|
|
145
146
|
test("force stops the assistant even when an active call lease exists", async () => {
|
|
@@ -154,7 +155,7 @@ describe("sleep command", () => {
|
|
|
154
155
|
consoleLog.mockRestore();
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
expect(stopProcessByPidFileMock).toHaveBeenCalledTimes(
|
|
158
|
+
expect(stopProcessByPidFileMock).toHaveBeenCalledTimes(3);
|
|
158
159
|
// The assistant stop passes a generous 120s grace so the daemon's WAL
|
|
159
160
|
// checkpoint completes before any SIGKILL (default 2s would truncate it).
|
|
160
161
|
expect(stopProcessByPidFileMock).toHaveBeenNthCalledWith(
|
|
@@ -171,5 +172,12 @@ describe("sleep command", () => {
|
|
|
171
172
|
undefined,
|
|
172
173
|
7000,
|
|
173
174
|
);
|
|
175
|
+
// The CES sibling (CES_STANDALONE opt-in) is stopped by its PID file; a
|
|
176
|
+
// no-op when absent on the default topology.
|
|
177
|
+
expect(stopProcessByPidFileMock).toHaveBeenNthCalledWith(
|
|
178
|
+
3,
|
|
179
|
+
join(assistantRootDir, "ces.pid"),
|
|
180
|
+
"credential-executor",
|
|
181
|
+
);
|
|
174
182
|
});
|
|
175
183
|
});
|
package/src/commands/recover.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { saveAssistantEntry } from "../lib/assistant-config";
|
|
|
12
12
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
13
13
|
import {
|
|
14
14
|
generateLocalSigningKey,
|
|
15
|
+
startCes,
|
|
15
16
|
startLocalDaemon,
|
|
16
17
|
startGateway,
|
|
17
18
|
} from "../lib/local";
|
|
@@ -117,9 +118,14 @@ export async function recover(): Promise<void> {
|
|
|
117
118
|
entry.guardianBootstrapSecret = bootstrapSecret;
|
|
118
119
|
saveAssistantEntry(entry);
|
|
119
120
|
|
|
120
|
-
// 8. Start daemon + gateway
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
// 8. Start CES sibling (opt-in) + daemon + gateway in parallel, the way the
|
|
122
|
+
// Docker topology brings its sibling processes up together. startCes is a
|
|
123
|
+
// no-op unless CES_STANDALONE is set.
|
|
124
|
+
await Promise.all([
|
|
125
|
+
startCes(false, entry.resources),
|
|
126
|
+
startLocalDaemon(false, entry.resources, { signingKey }),
|
|
127
|
+
startGateway(false, entry.resources, { signingKey, bootstrapSecret }),
|
|
128
|
+
]);
|
|
123
129
|
|
|
124
130
|
console.log(`✅ Recovered assistant '${name}'.`);
|
|
125
131
|
}
|
package/src/commands/sleep.ts
CHANGED
|
@@ -168,6 +168,18 @@ export async function sleep(): Promise<void> {
|
|
|
168
168
|
console.log("Gateway stopped.");
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
// Stop the CES sibling if one was launched (CES_STANDALONE). No-op when the
|
|
172
|
+
// PID file is absent — on the default topology the assistant owns CES as an
|
|
173
|
+
// stdio child and it exits with the daemon.
|
|
174
|
+
const cesPidFile = join(vellumDir, "ces.pid");
|
|
175
|
+
const cesStopped = await stopProcessByPidFile(
|
|
176
|
+
cesPidFile,
|
|
177
|
+
"credential-executor",
|
|
178
|
+
);
|
|
179
|
+
if (cesStopped) {
|
|
180
|
+
console.log("credential-executor stopped.");
|
|
181
|
+
}
|
|
182
|
+
|
|
171
183
|
// Stop the nginx ingress if one is fronting this gateway — otherwise it
|
|
172
184
|
// keeps running against a dead upstream and serves 502s.
|
|
173
185
|
const ingressStopped = await stopIngressNginx(join(vellumDir, "workspace"));
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -66,6 +66,7 @@ import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
|
66
66
|
import {
|
|
67
67
|
generateLocalSigningKey,
|
|
68
68
|
ensureLocalRuntime,
|
|
69
|
+
startCes,
|
|
69
70
|
startGateway,
|
|
70
71
|
startLocalDaemon,
|
|
71
72
|
stopLocalProcesses,
|
|
@@ -434,7 +435,8 @@ async function upgradeDocker(
|
|
|
434
435
|
}
|
|
435
436
|
|
|
436
437
|
// Recover the assistant host port from the entry, fall back to default.
|
|
437
|
-
const assistantPort =
|
|
438
|
+
const assistantPort =
|
|
439
|
+
entry.containerInfo?.assistantPort ?? ASSISTANT_INTERNAL_PORT;
|
|
438
440
|
|
|
439
441
|
// Create pre-upgrade backup (best-effort, daemon must be running)
|
|
440
442
|
await broadcastUpgradeEvent(
|
|
@@ -984,8 +986,14 @@ async function upgradeLocal(
|
|
|
984
986
|
const previousAppVersion = process.env.APP_VERSION;
|
|
985
987
|
process.env.APP_VERSION = stripVersionPrefix(targetVersion);
|
|
986
988
|
try {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
+
// Bring CES, daemon, and gateway up in parallel, the way the Docker
|
|
990
|
+
// topology starts its sibling processes together. startCes is a no-op
|
|
991
|
+
// unless CES_STANDALONE is set.
|
|
992
|
+
await Promise.all([
|
|
993
|
+
startCes(false, entry.resources),
|
|
994
|
+
startLocalDaemon(false, entry.resources, { signingKey }),
|
|
995
|
+
startGateway(false, entry.resources, { signingKey, bootstrapSecret }),
|
|
996
|
+
]);
|
|
989
997
|
} finally {
|
|
990
998
|
if (previousAppVersion === undefined) {
|
|
991
999
|
delete process.env.APP_VERSION;
|
package/src/commands/wake.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
generateLocalSigningKey,
|
|
18
18
|
isAssistantWatchModeAvailable,
|
|
19
19
|
isGatewayWatchModeAvailable,
|
|
20
|
+
startCes,
|
|
20
21
|
startLocalDaemon,
|
|
21
22
|
startGateway,
|
|
22
23
|
} from "../lib/local";
|
|
@@ -176,7 +177,18 @@ export async function wake(): Promise<void> {
|
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
if (!daemonRunning) {
|
|
179
|
-
|
|
180
|
+
// Spin up CES and the daemon in parallel, the way the Docker topology
|
|
181
|
+
// brings its sibling containers up together — the assistant polls for the
|
|
182
|
+
// CES socket during startup (discoverCesWithRetry), so it tolerates CES
|
|
183
|
+
// still binding. CES's lifecycle tracks the daemon (its only consumer):
|
|
184
|
+
// restarting it under a live daemon would sever the daemon's open
|
|
185
|
+
// connection, so it is only (re)started alongside the daemon. startCes is a
|
|
186
|
+
// no-op unless CES_STANDALONE is set, in which case the assistant spawns
|
|
187
|
+
// CES itself as today.
|
|
188
|
+
await Promise.all([
|
|
189
|
+
startCes(watch, resources),
|
|
190
|
+
startLocalDaemon(watch, resources, { foreground, signingKey }),
|
|
191
|
+
]);
|
|
180
192
|
}
|
|
181
193
|
|
|
182
194
|
// Start gateway
|
|
@@ -256,7 +268,11 @@ export async function wake(): Promise<void> {
|
|
|
256
268
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
257
269
|
try {
|
|
258
270
|
await resetGuardianBootstrap(loopbackUrl, bootstrapSecret);
|
|
259
|
-
await leaseGuardianToken(
|
|
271
|
+
await leaseGuardianToken(
|
|
272
|
+
loopbackUrl,
|
|
273
|
+
entry.assistantId,
|
|
274
|
+
bootstrapSecret,
|
|
275
|
+
);
|
|
260
276
|
console.log(" Re-provisioned guardian token.");
|
|
261
277
|
break;
|
|
262
278
|
} catch (err) {
|
package/src/lib/hatch-local.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type { Species } from "./constants.js";
|
|
|
25
25
|
import { buildHatchConfigValues, writeInitialConfig } from "./config-utils.js";
|
|
26
26
|
import {
|
|
27
27
|
generateLocalSigningKey,
|
|
28
|
+
startCes,
|
|
28
29
|
startLocalDaemon,
|
|
29
30
|
startGateway,
|
|
30
31
|
stopLocalProcesses,
|
|
@@ -225,10 +226,17 @@ export async function hatchLocal(
|
|
|
225
226
|
reporter.progress(3, 6, "Starting assistant...");
|
|
226
227
|
const signingKey = generateLocalSigningKey();
|
|
227
228
|
const bootstrapSecret = generateLocalSigningKey();
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
// Launch the CES sibling alongside the daemon, in parallel — matching the
|
|
230
|
+
// Docker topology. Under the opt-in the assistant does not spawn its own CES,
|
|
231
|
+
// so a freshly hatched instance would otherwise come up with CES unavailable.
|
|
232
|
+
// startCes is a no-op unless CES_STANDALONE is set.
|
|
233
|
+
await Promise.all([
|
|
234
|
+
startCes(watch, resources),
|
|
235
|
+
startLocalDaemon(watch, resources, {
|
|
236
|
+
defaultWorkspaceConfigPath,
|
|
237
|
+
signingKey,
|
|
238
|
+
}),
|
|
239
|
+
]);
|
|
232
240
|
|
|
233
241
|
reporter.progress(4, 6, "Starting gateway...");
|
|
234
242
|
let runtimeUrl = `http://127.0.0.1:${resources.gatewayPort}`;
|
package/src/lib/local.ts
CHANGED
|
@@ -145,6 +145,19 @@ function localRuntimeGatewayDir(
|
|
|
145
145
|
return isGatewaySourceDir(candidate) ? candidate : undefined;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
function localRuntimeCesDir(
|
|
149
|
+
resources: LocalInstanceResources | undefined,
|
|
150
|
+
): string | undefined {
|
|
151
|
+
const installDir = resources?.runtimeInstallDir;
|
|
152
|
+
if (!installDir) return undefined;
|
|
153
|
+
const candidate = packagePath(
|
|
154
|
+
installDir,
|
|
155
|
+
"@vellumai/credential-executor",
|
|
156
|
+
"",
|
|
157
|
+
);
|
|
158
|
+
return isCesSourceDir(candidate) ? candidate : undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
148
161
|
export function ensureLocalRuntime(
|
|
149
162
|
resources: LocalInstanceResources,
|
|
150
163
|
version: string,
|
|
@@ -347,6 +360,18 @@ function isGatewaySourceDir(dir: string): boolean {
|
|
|
347
360
|
}
|
|
348
361
|
}
|
|
349
362
|
|
|
363
|
+
function isCesSourceDir(dir: string): boolean {
|
|
364
|
+
const pkgPath = join(dir, "package.json");
|
|
365
|
+
if (!existsSync(pkgPath) || !existsSync(join(dir, "src", "main.ts")))
|
|
366
|
+
return false;
|
|
367
|
+
try {
|
|
368
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
369
|
+
return pkg.name === "@vellumai/credential-executor";
|
|
370
|
+
} catch {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
350
375
|
function findGatewaySourceFromCwd(): string | undefined {
|
|
351
376
|
let current = process.cwd();
|
|
352
377
|
while (true) {
|
|
@@ -540,6 +565,14 @@ function applyDaemonEnvOverrides(
|
|
|
540
565
|
env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH =
|
|
541
566
|
options.defaultWorkspaceConfigPath;
|
|
542
567
|
}
|
|
568
|
+
// When the CLI launches CES as a sibling (CES_STANDALONE), pin the daemon to
|
|
569
|
+
// the exact socket the sibling binds so the two agree regardless of any stale
|
|
570
|
+
// CES_LOCAL_SOCKET inherited from the parent environment. The assistant then
|
|
571
|
+
// connects to the sibling instead of spawning its own CES.
|
|
572
|
+
if (isCesSiblingOptIn()) {
|
|
573
|
+
env.CES_STANDALONE = "1";
|
|
574
|
+
env.CES_LOCAL_SOCKET = resolveCesSocketPath(resources);
|
|
575
|
+
}
|
|
543
576
|
applyIpcSocketDirOverride(env);
|
|
544
577
|
}
|
|
545
578
|
|
|
@@ -731,6 +764,168 @@ function resolveGatewayDir(resources?: LocalInstanceResources): string {
|
|
|
731
764
|
}
|
|
732
765
|
}
|
|
733
766
|
|
|
767
|
+
function resolveCesDir(resources?: LocalInstanceResources): string {
|
|
768
|
+
const runtimeCesDir = localRuntimeCesDir(resources);
|
|
769
|
+
if (runtimeCesDir) return runtimeCesDir;
|
|
770
|
+
|
|
771
|
+
// Source tree / npm sibling: cli/src/lib/ → ../../.. → credential-executor/
|
|
772
|
+
const sourceDir = join(
|
|
773
|
+
import.meta.dir,
|
|
774
|
+
"..",
|
|
775
|
+
"..",
|
|
776
|
+
"..",
|
|
777
|
+
"credential-executor",
|
|
778
|
+
);
|
|
779
|
+
if (isCesSourceDir(sourceDir)) {
|
|
780
|
+
return sourceDir;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// npm-installed elsewhere on disk: resolve via the package entry.
|
|
784
|
+
try {
|
|
785
|
+
const pkgPath = _require.resolve(
|
|
786
|
+
"@vellumai/credential-executor/package.json",
|
|
787
|
+
);
|
|
788
|
+
return dirname(pkgPath);
|
|
789
|
+
} catch {
|
|
790
|
+
throw new Error(
|
|
791
|
+
"credential-executor not found. Ensure @vellumai/credential-executor is installed or run from the source tree.",
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Resolve the Unix socket path the CLI-launched CES sibling binds and the
|
|
798
|
+
* daemon connects to. Both sides read `CES_LOCAL_SOCKET`, which the CLI sets to
|
|
799
|
+
* this exact path so they agree. On macOS, long workspace paths are relocated
|
|
800
|
+
* to a short tmpdir override (the same one the IPC sockets use) to stay under
|
|
801
|
+
* the AF_UNIX path limit.
|
|
802
|
+
*/
|
|
803
|
+
function resolveCesSocketPath(resources?: LocalInstanceResources): string {
|
|
804
|
+
const workspaceDir = resources
|
|
805
|
+
? join(resources.instanceDir, ".vellum", "workspace")
|
|
806
|
+
: join(homedir(), ".vellum", "workspace");
|
|
807
|
+
const override = computeIpcSocketDirOverride(workspaceDir);
|
|
808
|
+
const socketDir = override ?? workspaceDir;
|
|
809
|
+
mkdirSync(socketDir, { recursive: true });
|
|
810
|
+
return join(socketDir, "ces.sock");
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Whether the CLI should launch CES as an independent sibling process instead
|
|
815
|
+
* of leaving the assistant to spawn it as an stdio child. Temporary opt-in
|
|
816
|
+
* (`CES_STANDALONE=1`) while local CES converges onto the sibling model that
|
|
817
|
+
* containerized homes already use.
|
|
818
|
+
*/
|
|
819
|
+
function isCesSiblingOptIn(): boolean {
|
|
820
|
+
return process.env.CES_STANDALONE === "1";
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Launch the local CES sibling over a Unix socket (opted into via
|
|
825
|
+
* `CES_STANDALONE=1`). No-op unless the opt-in is set, in which case the
|
|
826
|
+
* assistant continues to spawn CES itself as today.
|
|
827
|
+
*
|
|
828
|
+
* The sibling runs with `CES_STANDALONE=1` so its lifecycle is anchored to
|
|
829
|
+
* SIGTERM rather than stdin EOF, mirroring the gateway: a CLI-owned process
|
|
830
|
+
* with a PID file under `.vellum/ces.pid`, started by `wake` and stopped by
|
|
831
|
+
* `sleep`.
|
|
832
|
+
*/
|
|
833
|
+
export async function startCes(
|
|
834
|
+
watch: boolean = false,
|
|
835
|
+
resources?: LocalInstanceResources,
|
|
836
|
+
): Promise<void> {
|
|
837
|
+
if (!isCesSiblingOptIn()) return;
|
|
838
|
+
|
|
839
|
+
const vellumDir = resources
|
|
840
|
+
? join(resources.instanceDir, ".vellum")
|
|
841
|
+
: join(homedir(), ".vellum");
|
|
842
|
+
const cesPidFile = join(vellumDir, "ces.pid");
|
|
843
|
+
|
|
844
|
+
// Kill any existing sibling first — a stale CES holds the socket and would
|
|
845
|
+
// corrupt the shared credential store if a second copy also bound it.
|
|
846
|
+
await stopProcessByPidFile(cesPidFile, "credential-executor");
|
|
847
|
+
|
|
848
|
+
console.log("🔐 Starting credential-executor sibling...");
|
|
849
|
+
|
|
850
|
+
const socketPath = resolveCesSocketPath(resources);
|
|
851
|
+
// A stale socket file from an unclean shutdown blocks re-bind; CES unlinks it
|
|
852
|
+
// on startup, but remove it here too so a leftover never masks a launch bug.
|
|
853
|
+
try {
|
|
854
|
+
unlinkSync(socketPath);
|
|
855
|
+
} catch {
|
|
856
|
+
/* no stale socket — fine */
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const securityDir = resources
|
|
860
|
+
? join(resources.instanceDir, ".vellum", "protected")
|
|
861
|
+
: join(homedir(), ".vellum", "protected");
|
|
862
|
+
const workspaceDir = resources
|
|
863
|
+
? join(resources.instanceDir, ".vellum", "workspace")
|
|
864
|
+
: join(homedir(), ".vellum", "workspace");
|
|
865
|
+
mkdirSync(securityDir, { recursive: true });
|
|
866
|
+
|
|
867
|
+
const cesEnv: Record<string, string | undefined> = {
|
|
868
|
+
...process.env,
|
|
869
|
+
CES_STANDALONE: "1",
|
|
870
|
+
CES_LOCAL_SOCKET: socketPath,
|
|
871
|
+
CREDENTIAL_SECURITY_DIR: securityDir,
|
|
872
|
+
VELLUM_WORKSPACE_DIR: workspaceDir,
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
let ces;
|
|
876
|
+
const runtimeCesDir = !watch ? localRuntimeCesDir(resources) : undefined;
|
|
877
|
+
const cesBinary = join(dirname(process.execPath), "credential-executor");
|
|
878
|
+
if (!runtimeCesDir && existsSync(cesBinary) && !watch) {
|
|
879
|
+
// Compiled binary alongside the CLI (desktop app / compiled CLI).
|
|
880
|
+
const cesLogFd = openLogFile("hatch.log");
|
|
881
|
+
ces = spawn(cesBinary, [], {
|
|
882
|
+
detached: true,
|
|
883
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
884
|
+
env: cesEnv,
|
|
885
|
+
});
|
|
886
|
+
pipeToLogFile(ces, cesLogFd, "credential-executor");
|
|
887
|
+
} else {
|
|
888
|
+
// Source tree / bunx: run the CES entry point via bun.
|
|
889
|
+
const cesDir = runtimeCesDir ?? resolveCesDir(resources);
|
|
890
|
+
const bunArgs = watch
|
|
891
|
+
? ["--watch", "run", "src/main.ts"]
|
|
892
|
+
: ["run", "src/main.ts"];
|
|
893
|
+
const cesLogFd = openLogFile("hatch.log");
|
|
894
|
+
ces = spawn(resolveBunExecutable(), bunArgs, {
|
|
895
|
+
cwd: cesDir,
|
|
896
|
+
detached: true,
|
|
897
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
898
|
+
env: envWithBunPath(cesEnv),
|
|
899
|
+
});
|
|
900
|
+
pipeToLogFile(ces, cesLogFd, "credential-executor");
|
|
901
|
+
if (watch) {
|
|
902
|
+
console.log(" credential-executor started in watch mode (bun --watch)");
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
ces.unref();
|
|
907
|
+
|
|
908
|
+
if (ces.pid) {
|
|
909
|
+
mkdirSync(vellumDir, { recursive: true });
|
|
910
|
+
writeFileSync(cesPidFile, String(ces.pid), "utf-8");
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Wait for the socket to appear so the daemon's discovery finds it on the
|
|
914
|
+
// first probe rather than burning its retry budget.
|
|
915
|
+
const deadline = Date.now() + 10_000;
|
|
916
|
+
while (Date.now() < deadline) {
|
|
917
|
+
if (existsSync(socketPath)) break;
|
|
918
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
919
|
+
}
|
|
920
|
+
if (!existsSync(socketPath)) {
|
|
921
|
+
console.warn(
|
|
922
|
+
"⚠ credential-executor started but its socket did not appear within 10s",
|
|
923
|
+
);
|
|
924
|
+
} else {
|
|
925
|
+
console.log("✅ credential-executor started\n");
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
734
929
|
/**
|
|
735
930
|
* Check if the daemon is responsive by hitting its HTTP `/healthz` endpoint.
|
|
736
931
|
* This replaces the socket-based `isSocketResponsive()` check.
|
|
@@ -1421,6 +1616,12 @@ export async function stopLocalProcesses(
|
|
|
1421
1616
|
const gatewayPidFile = join(vellumDir, "gateway.pid");
|
|
1422
1617
|
await stopProcessByPidFile(gatewayPidFile, "gateway", undefined, 7000);
|
|
1423
1618
|
|
|
1619
|
+
// Stop the CES sibling if one was launched (CES_STANDALONE). No-op when the
|
|
1620
|
+
// PID file is absent, so this is safe on the default topology where the
|
|
1621
|
+
// assistant owns CES as an stdio child.
|
|
1622
|
+
const cesPidFile = join(vellumDir, "ces.pid");
|
|
1623
|
+
await stopProcessByPidFile(cesPidFile, "credential-executor");
|
|
1624
|
+
|
|
1424
1625
|
// Kill ngrok directly by PID rather than using stopProcessByPidFile, because
|
|
1425
1626
|
// isVellumProcess() won't match the ngrok binary — resulting in a no-op that
|
|
1426
1627
|
// leaves ngrok running. Verify the PID still belongs to ngrok before killing
|
package/src/lib/process.ts
CHANGED
|
@@ -15,7 +15,7 @@ export function isVellumProcess(pid: number): boolean {
|
|
|
15
15
|
timeout: 3000,
|
|
16
16
|
stdio: ["ignore", "pipe", "ignore"],
|
|
17
17
|
}).trim();
|
|
18
|
-
return /vellum-daemon|vellum-cli|vellum-gateway|@vellumai|\/\.?vellum\/|\/daemon\/main|\/\.vellum\/.*qdrant\/bin\/qdrant/.test(
|
|
18
|
+
return /vellum-daemon|vellum-cli|vellum-gateway|credential-executor|@vellumai|\/\.?vellum\/|\/daemon\/main|\/\.vellum\/.*qdrant\/bin\/qdrant/.test(
|
|
19
19
|
output,
|
|
20
20
|
);
|
|
21
21
|
} catch {
|