@vellumai/assistant 0.11.4-dev.202608201513.9b3b830 → 0.11.4-dev.202608201710.fcf0423
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/package.json +1 -1
- package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
- package/src/cli/commands/channel-verification-sessions.help.ts +9 -8
- package/src/cli/commands/channel-verification-sessions.ts +11 -21
- package/src/daemon/lifecycle.ts +4 -2
- package/src/daemon/startup-error.ts +16 -0
- package/src/ipc/socket-cleanup.ts +2 -11
- package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
- package/src/runtime/http-server.ts +20 -5
package/AGENTS.md
CHANGED
|
@@ -18,7 +18,7 @@ When you introduce a new env var that the assistant process needs to read at run
|
|
|
18
18
|
|
|
19
19
|
The daemon must **never** block startup due to **subsystem** failures (DB, Qdrant, plugins, feature flags, etc.). If an individual subsystem fails, log the error and continue in degraded mode so the process remains reachable for health checks and diagnostics.
|
|
20
20
|
|
|
21
|
-
**Exception
|
|
21
|
+
**Exception, occupied client-facing transports:** A **transport** is not a subsystem. If either client-facing transport's address is taken (the IPC socket in `ipc/assistant-server.ts`, the runtime HTTP port in `runtime/http-server.ts`), the daemon exits with an `EADDRINUSE`-coded error so `emitDaemonError` reports `PORT_IN_USE`. Half a daemon is worse than none: missing IPC makes it unmanageable while its background jobs still write the shared database, and missing HTTP makes it read healthy over IPC while the gateway proxies `/v1/*` to a foreign listener. Bind failures that are not address collisions (permission denied, fd exhaustion) stay non-fatal.
|
|
22
22
|
|
|
23
23
|
## DB migration readiness gating
|
|
24
24
|
|
package/package.json
CHANGED
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
* - `revoke` with no flags (channel: undefined)
|
|
14
14
|
* - `revoke --channel phone` sends channel param
|
|
15
15
|
* - `create --channel fax` → invalid channel, exits with code 1, IPC not called
|
|
16
|
+
* - the accepted `--channel` set is the canonical CHANNEL_IDS vocabulary
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
19
20
|
|
|
21
|
+
import { CHANNEL_IDS } from "@vellumai/service-contracts/channels";
|
|
20
22
|
import { Command } from "commander";
|
|
21
23
|
|
|
22
24
|
// ---------------------------------------------------------------------------
|
|
@@ -242,6 +244,35 @@ describe("channel-verification-sessions create", () => {
|
|
|
242
244
|
expect(lastIpcCall).toBeNull();
|
|
243
245
|
});
|
|
244
246
|
|
|
247
|
+
test("--channel discord reaches IPC (canonical vocabulary, not a local copy)", async () => {
|
|
248
|
+
const { exitCode } = await runCommand([
|
|
249
|
+
"channel-verification-sessions",
|
|
250
|
+
"create",
|
|
251
|
+
"--channel",
|
|
252
|
+
"discord",
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
expect(exitCode).toBe(0);
|
|
256
|
+
expect(lastIpcCall!.method).toBe("channel_verification_sessions_create");
|
|
257
|
+
expect(lastIpcCall!.params).toMatchObject({
|
|
258
|
+
body: { channel: "discord", purpose: "guardian" },
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("every canonical channel id is accepted", async () => {
|
|
263
|
+
for (const channel of CHANNEL_IDS) {
|
|
264
|
+
const { exitCode } = await runCommand([
|
|
265
|
+
"channel-verification-sessions",
|
|
266
|
+
"create",
|
|
267
|
+
"--channel",
|
|
268
|
+
channel,
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
expect(exitCode).toBe(0);
|
|
272
|
+
expect(lastIpcCall!.params).toMatchObject({ body: { channel } });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
245
276
|
test("IPC error results in exit code 1", async () => {
|
|
246
277
|
mockIpcResult = { ok: false, error: "Could not connect" };
|
|
247
278
|
|
|
@@ -10,15 +10,16 @@ export const channelVerificationSessionsHelp: CliCommandHelp = {
|
|
|
10
10
|
],
|
|
11
11
|
helpText: `
|
|
12
12
|
Verification sessions are used to verify guardian bindings and trusted
|
|
13
|
-
contacts across channels (telegram, phone, slack, email). Three
|
|
13
|
+
contacts across channels (telegram, phone, slack, discord, email). Three
|
|
14
|
+
flows exist:
|
|
14
15
|
|
|
15
16
|
1. Inbound challenge — the assistant generates a secret code and waits
|
|
16
17
|
for the guardian to send it back on the channel. Used when the
|
|
17
18
|
guardian can already message the assistant.
|
|
18
19
|
|
|
19
20
|
2. Outbound verification — the assistant sends a verification code to
|
|
20
|
-
a destination (Telegram handle, phone number, Slack
|
|
21
|
-
waits for confirmation. Used when bootstrapping a new channel.
|
|
21
|
+
a destination (Telegram handle, phone number, Slack or Discord user
|
|
22
|
+
ID) and waits for confirmation. Used when bootstrapping a new channel.
|
|
22
23
|
|
|
23
24
|
3. Trusted contact verification — verifies a contact channel that
|
|
24
25
|
already exists in the contact graph, sending a code to the channel
|
|
@@ -36,7 +37,7 @@ Examples:
|
|
|
36
37
|
options: [
|
|
37
38
|
{
|
|
38
39
|
flags: "--channel <channel>",
|
|
39
|
-
description: "Channel type (telegram, phone, slack, email)",
|
|
40
|
+
description: "Channel type (telegram, phone, slack, discord, email)",
|
|
40
41
|
},
|
|
41
42
|
{
|
|
42
43
|
flags: "--destination <destination>",
|
|
@@ -72,8 +73,8 @@ Routes between three creation modes based on the provided options:
|
|
|
72
73
|
|
|
73
74
|
2. Outbound: --channel <ch> --destination <dest>
|
|
74
75
|
Sends a verification code to the given destination. Supports telegram
|
|
75
|
-
(handle or chat ID), phone (E.164 number), slack (user ID),
|
|
76
|
-
Use --rebind to replace an existing guardian binding.
|
|
76
|
+
(handle or chat ID), phone (E.164 number), slack and discord (user ID),
|
|
77
|
+
and email. Use --rebind to replace an existing guardian binding.
|
|
77
78
|
|
|
78
79
|
3. Inbound: --channel <ch> (no --destination)
|
|
79
80
|
Generates a challenge secret for the guardian to send back on the
|
|
@@ -113,7 +114,7 @@ Examples:
|
|
|
113
114
|
options: [
|
|
114
115
|
{
|
|
115
116
|
flags: "--channel <channel>",
|
|
116
|
-
description: "Channel type (telegram, phone, slack, email)",
|
|
117
|
+
description: "Channel type (telegram, phone, slack, discord, email)",
|
|
117
118
|
required: true,
|
|
118
119
|
},
|
|
119
120
|
{
|
|
@@ -137,7 +138,7 @@ Examples:
|
|
|
137
138
|
options: [
|
|
138
139
|
{
|
|
139
140
|
flags: "--channel <channel>",
|
|
140
|
-
description: "Channel type (telegram, phone, slack, email)",
|
|
141
|
+
description: "Channel type (telegram, phone, slack, discord, email)",
|
|
141
142
|
required: true,
|
|
142
143
|
},
|
|
143
144
|
],
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
// `cli/no-daemon-internals` forbids hoisting `channels/types.js`, so the CLI
|
|
2
|
+
// reads the vocabulary straight from the contract package that file re-exports.
|
|
3
|
+
// Both sides then validate `--channel` against the same set.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
CHANNEL_IDS,
|
|
7
|
+
type ChannelId,
|
|
8
|
+
isChannelId,
|
|
9
|
+
} from "@vellumai/service-contracts/channels";
|
|
1
10
|
import type { Command } from "commander";
|
|
2
11
|
|
|
3
12
|
import { cliIpcCall, exitFromIpcResult } from "../../ipc/cli-client.js";
|
|
@@ -6,25 +15,6 @@ import { registerCommand } from "../lib/register-command.js";
|
|
|
6
15
|
import { writeOutput } from "../output.js";
|
|
7
16
|
import { channelVerificationSessionsHelp } from "./channel-verification-sessions.help.js";
|
|
8
17
|
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Local channel validation (replaces daemon-internal channels/types.js import)
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
const VALID_CHANNEL_IDS = [
|
|
14
|
-
"telegram",
|
|
15
|
-
"phone",
|
|
16
|
-
"vellum",
|
|
17
|
-
"whatsapp",
|
|
18
|
-
"slack",
|
|
19
|
-
"email",
|
|
20
|
-
"platform",
|
|
21
|
-
] as const;
|
|
22
|
-
type ChannelId = (typeof VALID_CHANNEL_IDS)[number];
|
|
23
|
-
|
|
24
|
-
function isChannelId(raw: string): raw is ChannelId {
|
|
25
|
-
return (VALID_CHANNEL_IDS as readonly string[]).includes(raw);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
18
|
/**
|
|
29
19
|
* Validate the --channel option. Returns the validated ChannelId or writes an
|
|
30
20
|
* error and returns `false`. When `required` is false an absent value is fine
|
|
@@ -49,7 +39,7 @@ function validateChannelOpt(
|
|
|
49
39
|
if (required) {
|
|
50
40
|
writeOutput(cmd, {
|
|
51
41
|
ok: false,
|
|
52
|
-
error: `The "channel" option is required. Valid values: ${
|
|
42
|
+
error: `The "channel" option is required. Valid values: ${CHANNEL_IDS.join(", ")}`,
|
|
53
43
|
});
|
|
54
44
|
process.exitCode = 1;
|
|
55
45
|
return false;
|
|
@@ -59,7 +49,7 @@ function validateChannelOpt(
|
|
|
59
49
|
if (!isChannelId(raw)) {
|
|
60
50
|
writeOutput(cmd, {
|
|
61
51
|
ok: false,
|
|
62
|
-
error: `Invalid channel "${raw}". Valid values: ${
|
|
52
|
+
error: `Invalid channel "${raw}". Valid values: ${CHANNEL_IDS.join(", ")}`,
|
|
63
53
|
});
|
|
64
54
|
process.exitCode = 1;
|
|
65
55
|
return false;
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -198,8 +198,10 @@ export async function runDaemon(): Promise<void> {
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
// Start the runtime HTTP server early so /healthz answers ASAP.
|
|
202
|
-
//
|
|
201
|
+
// Start the runtime HTTP server early so /healthz answers ASAP. Throws on
|
|
202
|
+
// EADDRINUSE to abort startup: another process holds the port every HTTP
|
|
203
|
+
// client (and the gateway's /v1/* proxy) targets, so an IPC-only daemon
|
|
204
|
+
// would look healthy while all HTTP traffic 502s.
|
|
203
205
|
await startRuntimeHttpServer();
|
|
204
206
|
|
|
205
207
|
// Warms the configured-probe cache (credential reads only, no DB). Fired
|
|
@@ -23,6 +23,22 @@ export interface DaemonStartupError {
|
|
|
23
23
|
|
|
24
24
|
const DAEMON_ERROR_PREFIX = "DAEMON_ERROR:";
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Build an `EADDRINUSE`-coded error so callers (and {@link categorizeDaemonError})
|
|
28
|
+
* can branch on `err.code` and surface the structured "already running"
|
|
29
|
+
* guidance instead of a generic UNKNOWN. Used by every transport that detects
|
|
30
|
+
* an occupied address itself rather than receiving a kernel-coded error.
|
|
31
|
+
*/
|
|
32
|
+
export function makeAddrInUseError(
|
|
33
|
+
message: string,
|
|
34
|
+
cause?: unknown,
|
|
35
|
+
): NodeJS.ErrnoException {
|
|
36
|
+
const err = new Error(message, cause === undefined ? undefined : { cause });
|
|
37
|
+
const errno = err as NodeJS.ErrnoException;
|
|
38
|
+
errno.code = "EADDRINUSE";
|
|
39
|
+
return errno;
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
/**
|
|
27
43
|
* Inspect an error and return a categorized {@link DaemonStartupError}.
|
|
28
44
|
*/
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
removeIpcEndpointFile,
|
|
16
16
|
} from "@vellumai/ipc-server-utils";
|
|
17
17
|
|
|
18
|
+
import { makeAddrInUseError } from "../daemon/startup-error.js";
|
|
19
|
+
|
|
18
20
|
/**
|
|
19
21
|
* Maximum time to wait for the probe `connect()` to settle before declaring
|
|
20
22
|
* the path occupied. Without a bound, a hung process holding the socket would
|
|
@@ -25,17 +27,6 @@ import {
|
|
|
25
27
|
*/
|
|
26
28
|
const PROBE_CONNECT_TIMEOUT_MS = 2000;
|
|
27
29
|
|
|
28
|
-
/**
|
|
29
|
-
* Build an `EADDRINUSE`-coded error so callers (and `categorizeDaemonError`)
|
|
30
|
-
* can branch on `err.code` and surface the structured "already running"
|
|
31
|
-
* guidance instead of a generic UNKNOWN.
|
|
32
|
-
*/
|
|
33
|
-
function makeAddrInUseError(message: string): NodeJS.ErrnoException {
|
|
34
|
-
const err = new Error(message) as NodeJS.ErrnoException;
|
|
35
|
-
err.code = "EADDRINUSE";
|
|
36
|
-
return err;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
30
|
/**
|
|
40
31
|
* Probe-connect to `socketPath`. Behavior:
|
|
41
32
|
* - Path doesn't exist → return.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { emitDaemonError } from "../../daemon/startup-error.js";
|
|
4
|
+
import {
|
|
5
|
+
startRuntimeHttpServer,
|
|
6
|
+
stopRuntimeHttpServer,
|
|
7
|
+
} from "../http-server.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The daemon binds two client-facing transports. An occupied runtime HTTP port
|
|
11
|
+
* must abort startup the same way an occupied IPC socket does, so the daemon
|
|
12
|
+
* can never answer IPC (reading healthy to `vellum ps` and platform status)
|
|
13
|
+
* while the gateway proxies /v1/* to a foreign listener.
|
|
14
|
+
*/
|
|
15
|
+
describe("runtime HTTP port collision", () => {
|
|
16
|
+
const originalPort = process.env.RUNTIME_HTTP_PORT;
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await stopRuntimeHttpServer();
|
|
20
|
+
if (originalPort === undefined) {
|
|
21
|
+
delete process.env.RUNTIME_HTTP_PORT;
|
|
22
|
+
} else {
|
|
23
|
+
process.env.RUNTIME_HTTP_PORT = originalPort;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("aborts startup when a foreign process holds the runtime HTTP port", async () => {
|
|
28
|
+
/**
|
|
29
|
+
* Tests that a runtime HTTP bind collision fails daemon startup instead of
|
|
30
|
+
* silently degrading to IPC-only operation.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// GIVEN a foreign process holding a port
|
|
34
|
+
const foreign = Bun.serve({
|
|
35
|
+
port: 0,
|
|
36
|
+
hostname: "127.0.0.1",
|
|
37
|
+
fetch: () => new Response("foreign"),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// AND the daemon configured to serve HTTP on that same port
|
|
41
|
+
process.env.RUNTIME_HTTP_PORT = String(foreign.port);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// WHEN the daemon starts its runtime HTTP server
|
|
45
|
+
const err = await startRuntimeHttpServer().then(
|
|
46
|
+
() => null,
|
|
47
|
+
(e: unknown) => e,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// THEN startup fails rather than continuing without HTTP
|
|
51
|
+
expect(err).toBeInstanceOf(Error);
|
|
52
|
+
|
|
53
|
+
// AND the failure carries the address-collision code the daemon's
|
|
54
|
+
// startup-error categorization branches on
|
|
55
|
+
expect((err as NodeJS.ErrnoException).code).toBe("EADDRINUSE");
|
|
56
|
+
|
|
57
|
+
// AND the message names the occupied port and the env var that moves it
|
|
58
|
+
expect((err as Error).message).toContain(String(foreign.port));
|
|
59
|
+
expect((err as Error).message).toContain("RUNTIME_HTTP_PORT");
|
|
60
|
+
} finally {
|
|
61
|
+
foreign.stop(true);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("reports a runtime HTTP port collision as PORT_IN_USE on stderr", async () => {
|
|
66
|
+
/**
|
|
67
|
+
* Tests that the bind failure reaches consumers that parse the daemon's
|
|
68
|
+
* structured startup-error line (e.g. the macOS app) as PORT_IN_USE.
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
// GIVEN a foreign process holding the daemon's runtime HTTP port
|
|
72
|
+
const foreign = Bun.serve({
|
|
73
|
+
port: 0,
|
|
74
|
+
hostname: "127.0.0.1",
|
|
75
|
+
fetch: () => new Response("foreign"),
|
|
76
|
+
});
|
|
77
|
+
process.env.RUNTIME_HTTP_PORT = String(foreign.port);
|
|
78
|
+
|
|
79
|
+
// AND stderr captured so the structured startup-error line is observable
|
|
80
|
+
const written: string[] = [];
|
|
81
|
+
const originalWrite = process.stderr.write.bind(process.stderr);
|
|
82
|
+
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
83
|
+
written.push(chunk.toString());
|
|
84
|
+
return true;
|
|
85
|
+
}) as typeof process.stderr.write;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// WHEN the daemon's startup error handler reports the bind failure
|
|
89
|
+
const err = await startRuntimeHttpServer().then(
|
|
90
|
+
() => null,
|
|
91
|
+
(e: unknown) => e,
|
|
92
|
+
);
|
|
93
|
+
emitDaemonError(err);
|
|
94
|
+
|
|
95
|
+
// THEN the structured line categorizes the failure as PORT_IN_USE
|
|
96
|
+
const line = written.find((entry) => entry.includes("DAEMON_ERROR:"));
|
|
97
|
+
expect(line).toBeDefined();
|
|
98
|
+
const structured = JSON.parse(
|
|
99
|
+
(line as string).replace("DAEMON_ERROR:", "").trim(),
|
|
100
|
+
) as Record<string, unknown>;
|
|
101
|
+
expect(structured.error).toBe("PORT_IN_USE");
|
|
102
|
+
} finally {
|
|
103
|
+
process.stderr.write = originalWrite;
|
|
104
|
+
foreign.stop(true);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("binds and leaves the singleton serving when the port is free", async () => {
|
|
109
|
+
/**
|
|
110
|
+
* Tests that the collision guard does not turn ordinary startup into a
|
|
111
|
+
* failure: a free port still yields a listening HTTP server.
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
// GIVEN a free port for the runtime HTTP server
|
|
115
|
+
const probe = Bun.serve({
|
|
116
|
+
port: 0,
|
|
117
|
+
hostname: "127.0.0.1",
|
|
118
|
+
fetch: () => new Response("probe"),
|
|
119
|
+
});
|
|
120
|
+
const freePort = probe.port;
|
|
121
|
+
probe.stop(true);
|
|
122
|
+
process.env.RUNTIME_HTTP_PORT = String(freePort);
|
|
123
|
+
|
|
124
|
+
// WHEN the daemon starts its runtime HTTP server
|
|
125
|
+
await startRuntimeHttpServer();
|
|
126
|
+
|
|
127
|
+
// THEN the server answers its liveness probe
|
|
128
|
+
const response = await fetch(`http://127.0.0.1:${freePort}/healthz`);
|
|
129
|
+
expect(response.status).toBe(200);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
isDbMigrationGateBypassed,
|
|
28
28
|
} from "../daemon/daemon-readiness.js";
|
|
29
29
|
import { processMessage } from "../daemon/process-message.js";
|
|
30
|
+
import { makeAddrInUseError } from "../daemon/startup-error.js";
|
|
30
31
|
import {
|
|
31
32
|
createLiveVoiceConnection,
|
|
32
33
|
type LiveVoiceConnection,
|
|
@@ -975,9 +976,14 @@ let instance: RuntimeHttpServer | null = null;
|
|
|
975
976
|
|
|
976
977
|
/**
|
|
977
978
|
* Start the runtime HTTP server singleton early in daemon startup so /healthz
|
|
978
|
-
* answers ASAP.
|
|
979
|
-
*
|
|
980
|
-
*
|
|
979
|
+
* answers ASAP.
|
|
980
|
+
*
|
|
981
|
+
* An occupied address (`EADDRINUSE`) aborts startup: whatever holds the port
|
|
982
|
+
* owns every HTTP client of this workspace, including the gateway proxy that
|
|
983
|
+
* fronts `/v1/*`. Continuing would leave a daemon that answers IPC (so `vellum
|
|
984
|
+
* ps` and platform status read healthy) while every proxied HTTP route 502s
|
|
985
|
+
* against a foreign listener. Any other bind failure (permission denied, fd
|
|
986
|
+
* exhaustion) is non-fatal, logged with the singleton left unset.
|
|
981
987
|
*/
|
|
982
988
|
export async function startRuntimeHttpServer(): Promise<void> {
|
|
983
989
|
const port = getRuntimeHttpPort();
|
|
@@ -992,11 +998,20 @@ export async function startRuntimeHttpServer(): Promise<void> {
|
|
|
992
998
|
"Daemon startup: runtime HTTP server listening",
|
|
993
999
|
);
|
|
994
1000
|
} catch (err) {
|
|
1001
|
+
if ((err as NodeJS.ErrnoException).code === "EADDRINUSE") {
|
|
1002
|
+
log.error(
|
|
1003
|
+
{ err, port, hostname },
|
|
1004
|
+
"Runtime HTTP port already in use, aborting startup to avoid an HTTP-blind daemon",
|
|
1005
|
+
);
|
|
1006
|
+
throw makeAddrInUseError(
|
|
1007
|
+
`Runtime HTTP port ${hostname}:${port} is already in use by another process. Stop it, or set RUNTIME_HTTP_PORT to a free port.`,
|
|
1008
|
+
err,
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
995
1011
|
log.warn(
|
|
996
1012
|
{ err, port },
|
|
997
1013
|
"Failed to start runtime HTTP server, continuing without it",
|
|
998
1014
|
);
|
|
999
|
-
instance = null;
|
|
1000
1015
|
}
|
|
1001
1016
|
}
|
|
1002
1017
|
|
|
@@ -1007,7 +1022,7 @@ export async function startRuntimeHttpServer(): Promise<void> {
|
|
|
1007
1022
|
* expiry, profile reaping) must still run; the retry sweep additionally skips
|
|
1008
1023
|
* its cycles while readiness is unready. Never called before migrations settle,
|
|
1009
1024
|
* so the sweeps can't race a schema mid-migration. No-op if the HTTP server
|
|
1010
|
-
*
|
|
1025
|
+
* isn't running or sweeps already started.
|
|
1011
1026
|
*/
|
|
1012
1027
|
export function startRuntimeHttpServerBackgroundSweeps(): void {
|
|
1013
1028
|
instance?.startBackgroundSweeps();
|