@agentconnect.md/daemon 1.0.0-rc.280 → 1.0.0-rc.281
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/index.js +2 -2
- package/dist/{login-CzDsJwym.js → login-BPKCyqR_.js} +2 -2
- package/dist/{login-CzDsJwym.js.map → login-BPKCyqR_.js.map} +1 -1
- package/dist/{run-foreground-1y1IF3Dc.js → run-foreground-BBBkQ93M.js} +191 -52
- package/dist/{run-foreground-1y1IF3Dc.js.map → run-foreground-BBBkQ93M.js.map} +1 -1
- package/dist/run-foreground-DA_KUM1q.js +2 -0
- package/package.json +1 -1
- package/dist/run-foreground-CfLjjwW7.js +0 -2
package/dist/index.js
CHANGED
|
@@ -68493,8 +68493,8 @@ const [{ Command }, { fileURLToPath }, { runChat }, { runLogin }, { runForegroun
|
|
|
68493
68493
|
import("./commander-B5QVEFzH.js"),
|
|
68494
68494
|
import("node:url"),
|
|
68495
68495
|
import("./chat-4Is9zl7L.js"),
|
|
68496
|
-
import("./login-
|
|
68497
|
-
import("./run-foreground-
|
|
68496
|
+
import("./login-BPKCyqR_.js"),
|
|
68497
|
+
import("./run-foreground-DA_KUM1q.js"),
|
|
68498
68498
|
import("./service-6w0WKhdx.js"),
|
|
68499
68499
|
import("./lock-CA9Bm8_s.js"),
|
|
68500
68500
|
import("./paths-CcRUjFjy.js")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as DAEMON_VERSION } from "./version-DGaRXjBK.js";
|
|
2
2
|
import { _ as buildEnvelope, l as ConfigSchema, u as isFrame, v as decodeEnvelope, y as encode } from "./load-agents-Amic2B8b.js";
|
|
3
|
-
import { i as ClientTransport, t as runForeground } from "./run-foreground-
|
|
3
|
+
import { i as ClientTransport, t as runForeground } from "./run-foreground-BBBkQ93M.js";
|
|
4
4
|
import { l as resolveRoot, t as configPath } from "./paths-DEcUO8lC.js";
|
|
5
5
|
import { n as resolveController } from "./service-BQVw7wbz.js";
|
|
6
6
|
import { dirname } from "node:path";
|
|
@@ -228,4 +228,4 @@ async function runLogin(opts, partial = {}) {
|
|
|
228
228
|
//#endregion
|
|
229
229
|
export { buildInstallOpts, persistCredentials, runLogin };
|
|
230
230
|
|
|
231
|
-
//# sourceMappingURL=login-
|
|
231
|
+
//# sourceMappingURL=login-BPKCyqR_.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login-CzDsJwym.js","names":["realRunForeground"],"sources":["../src/cp/auth-probe.ts","../src/cli/login.ts"],"sourcesContent":["/**\n * `probeAuth` — a one-shot CP auth check used by `agentconnect login`. It dials\n * the CP, sends ONLY the `auth` frame (no `register`), and resolves with the\n * authoritative daemonId from `auth/ok`, or a human-readable failure reason on\n * a `4401` close, a correlated `error`, a dial failure, or a timeout. It always\n * closes the socket and never leaves a connection open.\n */\nimport { buildEnvelope, decodeEnvelope, encode, isFrame } from '@agentconnect.md/protocol'\nimport { ClientTransport, type Transport } from '@agentconnect.md/connection'\nimport { CP_SUBPROTOCOL, CP_WS_PATH } from './client.js'\n\nexport interface ProbeResult {\n ok: boolean\n daemonId?: string\n reason?: string\n}\n\nexport interface ProbeOpts {\n url: string\n token: string\n daemonId?: string\n agentVersion?: string\n timeoutMs?: number\n /** Injectable for tests; defaults to dialing the real WebSocket. */\n connect?: () => Promise<Transport>\n}\n\nexport async function probeAuth(opts: ProbeOpts): Promise<ProbeResult> {\n const timeoutMs = opts.timeoutMs ?? 10_000\n const dial = opts.connect ?? (() => ClientTransport.dial(opts.url, { subprotocol: CP_SUBPROTOCOL, path: CP_WS_PATH }))\n\n let transport: Transport\n try {\n transport = await dial()\n } catch (err) {\n return { ok: false, reason: `cannot reach ${opts.url}: ${(err as Error).message}` }\n }\n\n const authPayload: Record<string, unknown> = {\n apiKey: opts.token,\n agentVersion: opts.agentVersion ?? 'probe'\n }\n if (opts.daemonId) authPayload.daemonId = opts.daemonId\n const frame = buildEnvelope('auth', authPayload)\n\n return await new Promise<ProbeResult>((resolve) => {\n let settled = false\n const done = (r: ProbeResult): void => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n try {\n transport.close(1000, 'probe done')\n } catch {\n // socket may already be closed\n }\n resolve(r)\n }\n\n const timer = setTimeout(\n () => done({ ok: false, reason: `timed out after ${timeoutMs}ms waiting for auth/ok` }),\n timeoutMs\n )\n\n transport.onMessage((text) => {\n const decoded = decodeEnvelope(text)\n if (!decoded.ok) return\n const f = decoded.frame\n if (f.corr !== frame.id) return\n if (f.type === 'error') {\n const e = f.payload as { code: string; message: string }\n done({ ok: false, reason: `${e.code}: ${e.message}` })\n return\n }\n if (isFrame('auth/ok')(f)) {\n const ok = f.payload as { daemonId: string }\n done({ ok: true, daemonId: ok.daemonId })\n }\n })\n\n transport.onClose((code) => {\n if (code === 4401) done({ ok: false, reason: 'authentication failed (4401) — check the daemon token' })\n else done({ ok: false, reason: `connection closed (${code}) before auth/ok` })\n })\n\n transport.send(encode(frame))\n })\n}\n","/**\n * `agentconnect login` — interactive onboarding. It tests the Control Plane auth\n * flow, persists credentials only on success, then offers to install the OS\n * service. If installed it starts the service (`up`) and returns; otherwise it\n * runs the daemon in the foreground (the same path as `agentconnect run`).\n *\n * `persistCredentials` is the pure config-merge half (token-only onboarding:\n * `daemonId` is omitted unless given — the first connect adopts it from the\n * token's `sub`). `runLogin` is the orchestrator, with injectable deps for tests.\n */\nimport { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { createInterface } from 'node:readline'\nimport { fileURLToPath } from 'node:url'\nimport { ConfigSchema } from '../config/config-schema.js'\nimport { resolveRoot, configPath } from '../paths.js'\nimport { DAEMON_VERSION } from '../version.js'\nimport { probeAuth, type ProbeResult } from '../cp/auth-probe.js'\nimport { resolveController, type InstallOpts } from '../service/index.js'\nimport { runForeground as realRunForeground } from './run-foreground.js'\n\nexport interface PersistCredsOpts {\n cpUrl: string\n cpKey: string\n daemonId?: string\n root?: string\n configPath?: string\n}\n\n/** Merge url/token into config.json (validated), returning the written path. */\nexport function persistCredentials(opts: PersistCredsOpts): string {\n const root = resolveRoot(opts.root)\n const file = opts.configPath ?? configPath(root)\n const raw: Record<string, unknown> = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : { version: 1 }\n\n const cp = (raw.controlPlane as Record<string, unknown> | undefined) ?? {}\n raw.controlPlane = { ...cp, url: opts.cpUrl, key: opts.cpKey, enabled: true }\n if (opts.daemonId) raw.daemonId = opts.daemonId\n\n ConfigSchema.parse(raw) // fail before writing if the merge is invalid\n\n mkdirSync(dirname(file), { recursive: true })\n writeFileSync(file, JSON.stringify(raw, null, 2) + '\\n')\n return file\n}\n\nexport interface RunLoginOpts {\n cpUrl?: string\n cpKey?: string\n daemonId?: string\n root?: string\n configPath?: string\n /** The resolved CLI entry path. Must be supplied by the CLI entry point\n * (index.ts) so the correct bundle path is used rather than inferring it\n * from login.ts's own import.meta.url (which is the same bundle but the\n * `../` path calculation would be wrong). Falls back to import.meta.url\n * for the pure test path where login.ts is loaded standalone. */\n cliEntry?: string\n}\n\n/**\n * Build the InstallOpts for the OS service installer.\n *\n * Exported as a pure helper so it can be unit-tested independently of the\n * full `runLogin` orchestration. The caller (index.ts) passes `cliEntry` so\n * the correct bundle path always flows through; if omitted we fall back to\n * `import.meta.url` (correct only when login.ts is the entry, e.g. in tests).\n */\nexport function buildInstallOpts(opts: { root?: string; cliEntry?: string }): InstallOpts {\n return {\n execPath: process.execPath,\n cliEntry: opts.cliEntry ?? fileURLToPath(import.meta.url),\n includeRootEnv: resolveRoot(opts.root) !== resolveRoot(undefined)\n }\n}\n\nexport interface LoginDeps {\n probe: (o: { url: string; token: string }) => Promise<ProbeResult>\n installService: () => Promise<void>\n runForeground: () => Promise<void>\n out: NodeJS.WritableStream\n input: NodeJS.ReadableStream\n isTTY: boolean\n}\n\nfunction realDeps(opts: RunLoginOpts): LoginDeps {\n return {\n probe: (o) => probeAuth({ url: o.url, token: o.token, agentVersion: DAEMON_VERSION }),\n installService: async () => {\n const controller = resolveController({ root: opts.root })\n await controller.install(buildInstallOpts(opts))\n await controller.up()\n },\n runForeground: () =>\n realRunForeground({\n root: opts.root,\n overrides: { cpUrl: opts.cpUrl, cpKey: opts.cpKey, daemonId: opts.daemonId }\n }),\n out: process.stdout,\n input: process.stdin,\n isTTY: Boolean(process.stdin.isTTY)\n }\n}\n\n/** Read the next line from a readline interface via async iterator. */\nasync function nextLine(\n iter: AsyncIterableIterator<string>,\n prompt: string,\n out: NodeJS.WritableStream\n): Promise<string> {\n out.write(prompt)\n const { value, done } = await iter.next()\n if (done) throw new Error(`unexpected end of input waiting for: ${prompt}`)\n return (value as string).trim()\n}\n\nexport async function runLogin(opts: RunLoginOpts, partial: Partial<LoginDeps> = {}): Promise<void> {\n const deps: LoginDeps = { ...realDeps(opts), ...partial }\n const { out } = deps\n\n // ── Non-interactive: flags required; probe; persist. No prompts, no handoff. ──\n if (!deps.isTTY) {\n if (!opts.cpUrl) throw new Error('login requires --cp-url <url>')\n if (!opts.cpKey) throw new Error('login requires --cp-key <key>')\n const r = await deps.probe({ url: opts.cpUrl, token: opts.cpKey })\n if (!r.ok) throw new Error(`authentication failed: ${r.reason}`)\n persistCredentials({ ...opts, cpUrl: opts.cpUrl, cpKey: opts.cpKey })\n out.write(`✓ authenticated${r.daemonId ? ` (daemon ${r.daemonId})` : ''} — credentials saved\\n`)\n return\n }\n\n // Both interactive handoffs read `<root>/config.json`: the installed service\n // runs `… run` (no `--config`), and the foreground `Daemon` loads by root. A\n // custom `--config` would persist credentials the daemon never reads, so the\n // install/run branch would look successful but start without them. Reject it;\n // `--root` is the knob that keeps persist + service + foreground in sync.\n if (opts.configPath) {\n throw new Error(\n 'interactive login does not support --config — use --root <dir> so the daemon and the installed service read the same config'\n )\n }\n\n // ── Interactive ──\n const rl = createInterface({ input: deps.input, output: out })\n const iter = rl[Symbol.asyncIterator]()\n let url = opts.cpUrl\n let token = opts.cpKey\n try {\n if (!url) url = await nextLine(iter, 'Control Plane URL: ', out)\n\n for (let attempt = 0; ; attempt++) {\n if (!token) token = await nextLine(iter, 'Daemon API key: ', out)\n out.write(`Testing connection to ${url}…\\n`)\n const r = await deps.probe({ url: url!, token: token! })\n if (r.ok) {\n out.write(`✓ authenticated${r.daemonId ? ` (daemon ${r.daemonId})` : ''}\\n`)\n break\n }\n out.write(`✗ ${r.reason}\\n`)\n if (attempt >= 1) throw new Error('authentication failed')\n token = undefined // re-prompt on retry\n }\n\n persistCredentials({ ...opts, cpUrl: url!, cpKey: token! })\n\n const ans = (await nextLine(iter, 'Install AgentConnect as a background service? (y/N) ', out)).toLowerCase()\n if (ans === 'y' || ans === 'yes') {\n await deps.installService()\n out.write('Service installed and started. Manage it with `agentconnect up` / `down` / `status`.\\n')\n return\n }\n out.write('Starting in the foreground (Ctrl-C to stop)…\\n')\n } finally {\n rl.close()\n }\n\n await deps.runForeground()\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"login-BPKCyqR_.js","names":["realRunForeground"],"sources":["../src/cp/auth-probe.ts","../src/cli/login.ts"],"sourcesContent":["/**\n * `probeAuth` — a one-shot CP auth check used by `agentconnect login`. It dials\n * the CP, sends ONLY the `auth` frame (no `register`), and resolves with the\n * authoritative daemonId from `auth/ok`, or a human-readable failure reason on\n * a `4401` close, a correlated `error`, a dial failure, or a timeout. It always\n * closes the socket and never leaves a connection open.\n */\nimport { buildEnvelope, decodeEnvelope, encode, isFrame } from '@agentconnect.md/protocol'\nimport { ClientTransport, type Transport } from '@agentconnect.md/connection'\nimport { CP_SUBPROTOCOL, CP_WS_PATH } from './client.js'\n\nexport interface ProbeResult {\n ok: boolean\n daemonId?: string\n reason?: string\n}\n\nexport interface ProbeOpts {\n url: string\n token: string\n daemonId?: string\n agentVersion?: string\n timeoutMs?: number\n /** Injectable for tests; defaults to dialing the real WebSocket. */\n connect?: () => Promise<Transport>\n}\n\nexport async function probeAuth(opts: ProbeOpts): Promise<ProbeResult> {\n const timeoutMs = opts.timeoutMs ?? 10_000\n const dial = opts.connect ?? (() => ClientTransport.dial(opts.url, { subprotocol: CP_SUBPROTOCOL, path: CP_WS_PATH }))\n\n let transport: Transport\n try {\n transport = await dial()\n } catch (err) {\n return { ok: false, reason: `cannot reach ${opts.url}: ${(err as Error).message}` }\n }\n\n const authPayload: Record<string, unknown> = {\n apiKey: opts.token,\n agentVersion: opts.agentVersion ?? 'probe'\n }\n if (opts.daemonId) authPayload.daemonId = opts.daemonId\n const frame = buildEnvelope('auth', authPayload)\n\n return await new Promise<ProbeResult>((resolve) => {\n let settled = false\n const done = (r: ProbeResult): void => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n try {\n transport.close(1000, 'probe done')\n } catch {\n // socket may already be closed\n }\n resolve(r)\n }\n\n const timer = setTimeout(\n () => done({ ok: false, reason: `timed out after ${timeoutMs}ms waiting for auth/ok` }),\n timeoutMs\n )\n\n transport.onMessage((text) => {\n const decoded = decodeEnvelope(text)\n if (!decoded.ok) return\n const f = decoded.frame\n if (f.corr !== frame.id) return\n if (f.type === 'error') {\n const e = f.payload as { code: string; message: string }\n done({ ok: false, reason: `${e.code}: ${e.message}` })\n return\n }\n if (isFrame('auth/ok')(f)) {\n const ok = f.payload as { daemonId: string }\n done({ ok: true, daemonId: ok.daemonId })\n }\n })\n\n transport.onClose((code) => {\n if (code === 4401) done({ ok: false, reason: 'authentication failed (4401) — check the daemon token' })\n else done({ ok: false, reason: `connection closed (${code}) before auth/ok` })\n })\n\n transport.send(encode(frame))\n })\n}\n","/**\n * `agentconnect login` — interactive onboarding. It tests the Control Plane auth\n * flow, persists credentials only on success, then offers to install the OS\n * service. If installed it starts the service (`up`) and returns; otherwise it\n * runs the daemon in the foreground (the same path as `agentconnect run`).\n *\n * `persistCredentials` is the pure config-merge half (token-only onboarding:\n * `daemonId` is omitted unless given — the first connect adopts it from the\n * token's `sub`). `runLogin` is the orchestrator, with injectable deps for tests.\n */\nimport { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { createInterface } from 'node:readline'\nimport { fileURLToPath } from 'node:url'\nimport { ConfigSchema } from '../config/config-schema.js'\nimport { resolveRoot, configPath } from '../paths.js'\nimport { DAEMON_VERSION } from '../version.js'\nimport { probeAuth, type ProbeResult } from '../cp/auth-probe.js'\nimport { resolveController, type InstallOpts } from '../service/index.js'\nimport { runForeground as realRunForeground } from './run-foreground.js'\n\nexport interface PersistCredsOpts {\n cpUrl: string\n cpKey: string\n daemonId?: string\n root?: string\n configPath?: string\n}\n\n/** Merge url/token into config.json (validated), returning the written path. */\nexport function persistCredentials(opts: PersistCredsOpts): string {\n const root = resolveRoot(opts.root)\n const file = opts.configPath ?? configPath(root)\n const raw: Record<string, unknown> = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : { version: 1 }\n\n const cp = (raw.controlPlane as Record<string, unknown> | undefined) ?? {}\n raw.controlPlane = { ...cp, url: opts.cpUrl, key: opts.cpKey, enabled: true }\n if (opts.daemonId) raw.daemonId = opts.daemonId\n\n ConfigSchema.parse(raw) // fail before writing if the merge is invalid\n\n mkdirSync(dirname(file), { recursive: true })\n writeFileSync(file, JSON.stringify(raw, null, 2) + '\\n')\n return file\n}\n\nexport interface RunLoginOpts {\n cpUrl?: string\n cpKey?: string\n daemonId?: string\n root?: string\n configPath?: string\n /** The resolved CLI entry path. Must be supplied by the CLI entry point\n * (index.ts) so the correct bundle path is used rather than inferring it\n * from login.ts's own import.meta.url (which is the same bundle but the\n * `../` path calculation would be wrong). Falls back to import.meta.url\n * for the pure test path where login.ts is loaded standalone. */\n cliEntry?: string\n}\n\n/**\n * Build the InstallOpts for the OS service installer.\n *\n * Exported as a pure helper so it can be unit-tested independently of the\n * full `runLogin` orchestration. The caller (index.ts) passes `cliEntry` so\n * the correct bundle path always flows through; if omitted we fall back to\n * `import.meta.url` (correct only when login.ts is the entry, e.g. in tests).\n */\nexport function buildInstallOpts(opts: { root?: string; cliEntry?: string }): InstallOpts {\n return {\n execPath: process.execPath,\n cliEntry: opts.cliEntry ?? fileURLToPath(import.meta.url),\n includeRootEnv: resolveRoot(opts.root) !== resolveRoot(undefined)\n }\n}\n\nexport interface LoginDeps {\n probe: (o: { url: string; token: string }) => Promise<ProbeResult>\n installService: () => Promise<void>\n runForeground: () => Promise<void>\n out: NodeJS.WritableStream\n input: NodeJS.ReadableStream\n isTTY: boolean\n}\n\nfunction realDeps(opts: RunLoginOpts): LoginDeps {\n return {\n probe: (o) => probeAuth({ url: o.url, token: o.token, agentVersion: DAEMON_VERSION }),\n installService: async () => {\n const controller = resolveController({ root: opts.root })\n await controller.install(buildInstallOpts(opts))\n await controller.up()\n },\n runForeground: () =>\n realRunForeground({\n root: opts.root,\n overrides: { cpUrl: opts.cpUrl, cpKey: opts.cpKey, daemonId: opts.daemonId }\n }),\n out: process.stdout,\n input: process.stdin,\n isTTY: Boolean(process.stdin.isTTY)\n }\n}\n\n/** Read the next line from a readline interface via async iterator. */\nasync function nextLine(\n iter: AsyncIterableIterator<string>,\n prompt: string,\n out: NodeJS.WritableStream\n): Promise<string> {\n out.write(prompt)\n const { value, done } = await iter.next()\n if (done) throw new Error(`unexpected end of input waiting for: ${prompt}`)\n return (value as string).trim()\n}\n\nexport async function runLogin(opts: RunLoginOpts, partial: Partial<LoginDeps> = {}): Promise<void> {\n const deps: LoginDeps = { ...realDeps(opts), ...partial }\n const { out } = deps\n\n // ── Non-interactive: flags required; probe; persist. No prompts, no handoff. ──\n if (!deps.isTTY) {\n if (!opts.cpUrl) throw new Error('login requires --cp-url <url>')\n if (!opts.cpKey) throw new Error('login requires --cp-key <key>')\n const r = await deps.probe({ url: opts.cpUrl, token: opts.cpKey })\n if (!r.ok) throw new Error(`authentication failed: ${r.reason}`)\n persistCredentials({ ...opts, cpUrl: opts.cpUrl, cpKey: opts.cpKey })\n out.write(`✓ authenticated${r.daemonId ? ` (daemon ${r.daemonId})` : ''} — credentials saved\\n`)\n return\n }\n\n // Both interactive handoffs read `<root>/config.json`: the installed service\n // runs `… run` (no `--config`), and the foreground `Daemon` loads by root. A\n // custom `--config` would persist credentials the daemon never reads, so the\n // install/run branch would look successful but start without them. Reject it;\n // `--root` is the knob that keeps persist + service + foreground in sync.\n if (opts.configPath) {\n throw new Error(\n 'interactive login does not support --config — use --root <dir> so the daemon and the installed service read the same config'\n )\n }\n\n // ── Interactive ──\n const rl = createInterface({ input: deps.input, output: out })\n const iter = rl[Symbol.asyncIterator]()\n let url = opts.cpUrl\n let token = opts.cpKey\n try {\n if (!url) url = await nextLine(iter, 'Control Plane URL: ', out)\n\n for (let attempt = 0; ; attempt++) {\n if (!token) token = await nextLine(iter, 'Daemon API key: ', out)\n out.write(`Testing connection to ${url}…\\n`)\n const r = await deps.probe({ url: url!, token: token! })\n if (r.ok) {\n out.write(`✓ authenticated${r.daemonId ? ` (daemon ${r.daemonId})` : ''}\\n`)\n break\n }\n out.write(`✗ ${r.reason}\\n`)\n if (attempt >= 1) throw new Error('authentication failed')\n token = undefined // re-prompt on retry\n }\n\n persistCredentials({ ...opts, cpUrl: url!, cpKey: token! })\n\n const ans = (await nextLine(iter, 'Install AgentConnect as a background service? (y/N) ', out)).toLowerCase()\n if (ans === 'y' || ans === 'yes') {\n await deps.installService()\n out.write('Service installed and started. Manage it with `agentconnect up` / `down` / `status`.\\n')\n return\n }\n out.write('Starting in the foreground (Ctrl-C to stop)…\\n')\n } finally {\n rl.close()\n }\n\n await deps.runForeground()\n}\n"],"mappings":"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"}
|
|
@@ -213861,6 +213861,26 @@ function formatErr(err) {
|
|
|
213861
213861
|
return e?.stack ?? String(err);
|
|
213862
213862
|
}
|
|
213863
213863
|
const MAX_QUEUED_PER_SESSION = 10;
|
|
213864
|
+
/** Thrown to a `dispatch()` caller when the per-session admission queue is at its depth
|
|
213865
|
+
* cap (§4.4 backpressure): the message is fast-failed, not buffered. Carries a stable
|
|
213866
|
+
* `reason` so an agent-call source (P1/P2) can surface a typed `queue_full`. */
|
|
213867
|
+
var QueueFullError = class extends Error {
|
|
213868
|
+
reason = "queue_full";
|
|
213869
|
+
constructor(sessionKey) {
|
|
213870
|
+
super(`admission queue full for session ${sessionKey}`);
|
|
213871
|
+
this.name = "QueueFullError";
|
|
213872
|
+
}
|
|
213873
|
+
};
|
|
213874
|
+
/** Thrown to the `dispatch()` callers of the messages still queued behind a turn that
|
|
213875
|
+
* FAILED (§6.9 #378 fail-stop): the daemon does not auto-run buffered work onto a broken
|
|
213876
|
+
* session, so each follow-up is rejected rather than silently dropped or force-run. */
|
|
213877
|
+
var FailStopError = class extends Error {
|
|
213878
|
+
reason = "fail_stop";
|
|
213879
|
+
constructor(sessionKey) {
|
|
213880
|
+
super(`turn failed for session ${sessionKey}; queued messages were not auto-run`);
|
|
213881
|
+
this.name = "FailStopError";
|
|
213882
|
+
}
|
|
213883
|
+
};
|
|
213864
213884
|
const SELECT_KIND_CODE = {
|
|
213865
213885
|
model: "m",
|
|
213866
213886
|
effort: "e",
|
|
@@ -214871,8 +214891,10 @@ var Daemon = class Daemon {
|
|
|
214871
214891
|
* NO mute) shared by the Slack status-bar Cancel button. No-op if nothing is running. */
|
|
214872
214892
|
cancelSessionByKey(key) {
|
|
214873
214893
|
const rec = this.store.getSession(key);
|
|
214874
|
-
if (!
|
|
214875
|
-
|
|
214894
|
+
if (!this.inflight.has(key)) return;
|
|
214895
|
+
const agentId = rec?.agentId ?? this.serialQueue.get(key)?.[0]?.agentId;
|
|
214896
|
+
if (!agentId) return;
|
|
214897
|
+
this.interruptTurn(agentId, key, "cancel", rec?.acpSessionId ?? void 0);
|
|
214876
214898
|
}
|
|
214877
214899
|
/** Switch a session's reasoning effort by its local key — the effort counterpart of
|
|
214878
214900
|
* {@link setModelByKey}. Records the sticky override and, if the ACP session is warm,
|
|
@@ -214944,9 +214966,16 @@ var Daemon = class Daemon {
|
|
|
214944
214966
|
* Resolved by conversationId (the op carries no agentId). No-op when idle. */
|
|
214945
214967
|
handleWebchatCancel(conversationId) {
|
|
214946
214968
|
for (const [acpSessionId, p] of this.pending) if (p.webchat?.conversationId === conversationId) {
|
|
214947
|
-
this.interruptTurn(p.agentId,
|
|
214969
|
+
this.interruptTurn(p.agentId, p.sessionKey, "cancel", acpSessionId);
|
|
214948
214970
|
return;
|
|
214949
214971
|
}
|
|
214972
|
+
for (const [key, entries] of this.serialQueue) {
|
|
214973
|
+
const hit = entries.find((e) => e.webchat?.conversationId === conversationId);
|
|
214974
|
+
if (hit) {
|
|
214975
|
+
this.interruptTurn(hit.agentId, key, "cancel");
|
|
214976
|
+
return;
|
|
214977
|
+
}
|
|
214978
|
+
}
|
|
214950
214979
|
this.log.debug(`webchat cancel: no in-flight turn for conversation ${conversationId}`);
|
|
214951
214980
|
}
|
|
214952
214981
|
relayMsgAcks = /* @__PURE__ */ new Map();
|
|
@@ -215308,7 +215337,8 @@ var Daemon = class Daemon {
|
|
|
215308
215337
|
};
|
|
215309
215338
|
});
|
|
215310
215339
|
}
|
|
215311
|
-
|
|
215340
|
+
inflight = /* @__PURE__ */ new Set();
|
|
215341
|
+
serialQueue = /* @__PURE__ */ new Map();
|
|
215312
215342
|
/**
|
|
215313
215343
|
* Resolve a command's target from the channel's latest session when the routing ladder
|
|
215314
215344
|
* couldn't (no mention entity / thread / dm rule matched — e.g. a group `/status@bot`).
|
|
@@ -215357,7 +215387,7 @@ var Daemon = class Daemon {
|
|
|
215357
215387
|
}
|
|
215358
215388
|
}
|
|
215359
215389
|
const acpSessionId = rec?.acpSessionId;
|
|
215360
|
-
const inflight =
|
|
215390
|
+
const inflight = this.inflight.has(key);
|
|
215361
215391
|
const tgReplyTo = this.telegramReplyTarget(msg);
|
|
215362
215392
|
const reply = (text) => {
|
|
215363
215393
|
if (!conn) return;
|
|
@@ -215371,7 +215401,7 @@ var Daemon = class Daemon {
|
|
|
215371
215401
|
reply(rec ? `🔇 Nothing is running. ${muteNote}` : "Nothing is running to stop.");
|
|
215372
215402
|
return;
|
|
215373
215403
|
}
|
|
215374
|
-
this.interruptTurn(target.agentId,
|
|
215404
|
+
this.interruptTurn(target.agentId, key, "stop", acpSessionId ?? void 0);
|
|
215375
215405
|
reply(`🛑 Stopped. ${muteNote}`);
|
|
215376
215406
|
return;
|
|
215377
215407
|
}
|
|
@@ -215380,7 +215410,7 @@ var Daemon = class Daemon {
|
|
|
215380
215410
|
reply("Nothing is running to cancel.");
|
|
215381
215411
|
return;
|
|
215382
215412
|
}
|
|
215383
|
-
this.interruptTurn(target.agentId,
|
|
215413
|
+
this.interruptTurn(target.agentId, key, "cancel", acpSessionId ?? void 0);
|
|
215384
215414
|
reply("🛑 Cancelled.");
|
|
215385
215415
|
return;
|
|
215386
215416
|
}
|
|
@@ -215446,21 +215476,23 @@ var Daemon = class Daemon {
|
|
|
215446
215476
|
text: command.text,
|
|
215447
215477
|
thread
|
|
215448
215478
|
};
|
|
215449
|
-
if (
|
|
215450
|
-
this.log.
|
|
215451
|
-
this.dispatch(target.agentId, payload, target.integrationId).catch((err) => this.log.error(`queued dispatch failed for agent "${target.agentId}": ${err.stack ?? err}`));
|
|
215452
|
-
return;
|
|
215453
|
-
}
|
|
215454
|
-
const q = this.queued.get(acpSessionId) ?? [];
|
|
215455
|
-
if (q.length >= MAX_QUEUED_PER_SESSION) {
|
|
215456
|
-
this.log.warn(`command: queue → agent "${target.agentId}" session ${acpSessionId} full (${q.length}), rejected`);
|
|
215479
|
+
if (inflight && (this.serialQueue.get(key)?.length ?? 0) >= MAX_QUEUED_PER_SESSION) {
|
|
215480
|
+
this.log.warn(`command: queue → agent "${target.agentId}" session ${key} full, rejected`);
|
|
215457
215481
|
reply(`Queue is full (${MAX_QUEUED_PER_SESSION} pending) — wait for the current turn to finish.`);
|
|
215458
215482
|
return;
|
|
215459
215483
|
}
|
|
215460
|
-
|
|
215461
|
-
|
|
215462
|
-
|
|
215463
|
-
|
|
215484
|
+
this.dispatch(target.agentId, payload, target.integrationId, void 0, void 0, { isQueueCmd: true }).catch((err) => {
|
|
215485
|
+
if (err instanceof QueueFullError) return;
|
|
215486
|
+
this.log.error(`queued dispatch failed for agent "${target.agentId}": ${err.stack ?? err}`);
|
|
215487
|
+
});
|
|
215488
|
+
if (!inflight) {
|
|
215489
|
+
this.log.info(`command: queue → agent "${target.agentId}" idle, dispatching now`);
|
|
215490
|
+
reply(`▶️ Running now — the session was idle.`);
|
|
215491
|
+
} else {
|
|
215492
|
+
const depth = this.serialQueue.get(key)?.length ?? 0;
|
|
215493
|
+
this.log.info(`command: queue → agent "${target.agentId}" session ${key} (depth ${depth})`);
|
|
215494
|
+
reply(`📥 Queued (#${depth}) — will run when the current turn finishes.`);
|
|
215495
|
+
}
|
|
215464
215496
|
}
|
|
215465
215497
|
selectLabel(kind) {
|
|
215466
215498
|
return kind === "model" ? "Model" : kind === "effort" ? "Reasoning effort" : "Permission mode";
|
|
@@ -215591,15 +215623,6 @@ var Daemon = class Daemon {
|
|
|
215591
215623
|
acpSessionId: latest.acpSessionId ?? void 0
|
|
215592
215624
|
};
|
|
215593
215625
|
}
|
|
215594
|
-
/** Drain one buffered message for a session whose turn just finished (FIFO). */
|
|
215595
|
-
flushQueued(agentId, sessionId, integrationId) {
|
|
215596
|
-
const q = this.queued.get(sessionId);
|
|
215597
|
-
if (!q || q.length === 0) return;
|
|
215598
|
-
const next = q.shift();
|
|
215599
|
-
if (q.length === 0) this.queued.delete(sessionId);
|
|
215600
|
-
this.log.info(`queue: dispatching buffered message to agent "${agentId}" session ${sessionId} (${q.length} left)`);
|
|
215601
|
-
this.dispatch(agentId, next, integrationId).catch((err) => this.log.error(`queued dispatch failed for agent "${agentId}": ${err.stack ?? err}`));
|
|
215602
|
-
}
|
|
215603
215626
|
/** Local layer (agent.json) ∪ resolved CP layer; unservable CP rules are dropped + warn-logged. */
|
|
215604
215627
|
mergedRules() {
|
|
215605
215628
|
const local = [...this.agents.values()].flatMap((a) => rulesFromAgent(a, this.botUserIds));
|
|
@@ -215646,21 +215669,116 @@ var Daemon = class Daemon {
|
|
|
215646
215669
|
ctx.replyConn.postMessage(ctx.channel, `⚠️ Agent failed to respond: ${reason}`, ctx.thread);
|
|
215647
215670
|
}
|
|
215648
215671
|
}
|
|
215649
|
-
|
|
215650
|
-
|
|
215651
|
-
|
|
215652
|
-
|
|
215672
|
+
/**
|
|
215673
|
+
* Terminate a QUEUED entry's webchat sink when it is gate-dropped/rejected without ever
|
|
215674
|
+
* entering dispatchOne (drain/pause gate-drop, fail-stop of the queued rest, `!cancel`/`!stop`
|
|
215675
|
+
* queue drop, shutdown). A webchat turn signals its client via the WebchatSink terminal
|
|
215676
|
+
* `done` frame, NOT the dispatch() promise — so settling the promise alone leaves the browser
|
|
215677
|
+
* UI spinning forever. Mirrors surfaceTurnFailure's webchat branch. No-op for non-webchat
|
|
215678
|
+
* entries. Pass an `error` for reject/cancel/shutdown; omit it for a clean gate-drop.
|
|
215679
|
+
*/
|
|
215680
|
+
terminateQueuedSink(entry, error) {
|
|
215681
|
+
if (!entry.webchat) return;
|
|
215682
|
+
entry.webchat.sink.done({
|
|
215683
|
+
conversationId: entry.webchat.conversationId,
|
|
215684
|
+
turnId: entry.webchat.turnId,
|
|
215685
|
+
...error ? { error } : {}
|
|
215686
|
+
});
|
|
215687
|
+
}
|
|
215688
|
+
dispatch(agentId, msg, integrationId, webchat, callMeta, opts) {
|
|
215689
|
+
return new Promise((resolve, reject) => {
|
|
215690
|
+
if (this.draining || this.drainingAgents.has(agentId)) {
|
|
215691
|
+
this.log.debug(`dispatch: skipped for agent "${agentId}" (draining)`);
|
|
215692
|
+
resolve(null);
|
|
215693
|
+
return;
|
|
215694
|
+
}
|
|
215695
|
+
if (this.agents.get(agentId).pause) {
|
|
215696
|
+
this.log.info(`dispatch: skipped for agent "${agentId}" (paused)`);
|
|
215697
|
+
resolve(null);
|
|
215698
|
+
return;
|
|
215699
|
+
}
|
|
215700
|
+
const key = sessionKey(msg.platform, msg.channel, msg.thread ?? msg.msgId, agentId);
|
|
215701
|
+
const entry = {
|
|
215702
|
+
agentId,
|
|
215703
|
+
msg,
|
|
215704
|
+
...integrationId !== void 0 ? { integrationId } : {},
|
|
215705
|
+
...webchat ? { webchat } : {},
|
|
215706
|
+
...callMeta ? { callMeta } : {},
|
|
215707
|
+
...opts?.isQueueCmd ? { isQueueCmd: true } : {},
|
|
215708
|
+
resolve,
|
|
215709
|
+
reject
|
|
215710
|
+
};
|
|
215711
|
+
if (this.inflight.has(key)) {
|
|
215712
|
+
const q = this.serialQueue.get(key) ?? [];
|
|
215713
|
+
if (q.length >= MAX_QUEUED_PER_SESSION) {
|
|
215714
|
+
this.log.warn(`dispatch: queue full for session ${key} (${q.length}) — rejecting (queue_full)`);
|
|
215715
|
+
reject(new QueueFullError(key));
|
|
215716
|
+
return;
|
|
215717
|
+
}
|
|
215718
|
+
q.push(entry);
|
|
215719
|
+
this.serialQueue.set(key, q);
|
|
215720
|
+
this.log.debug(`dispatch: queued behind in-flight turn for session ${key} (depth ${q.length})`);
|
|
215721
|
+
return;
|
|
215722
|
+
}
|
|
215723
|
+
this.inflight.add(key);
|
|
215724
|
+
this.runLoop(key, entry);
|
|
215725
|
+
});
|
|
215726
|
+
}
|
|
215727
|
+
/**
|
|
215728
|
+
* Own a sessionKey's serial gate: run its head turn to completion, settle that entry's
|
|
215729
|
+
* own promise, then drain the queue in FIFO order WITHOUT releasing ownership between
|
|
215730
|
+
* turns (§6.9 #390 — deleting `inflight` then dequeuing would reopen the FIFO race).
|
|
215731
|
+
* Ownership is released only once the queue is confirmed empty in the same synchronous
|
|
215732
|
+
* tick. Fail-stop is the default (§6.9 #378): if a turn throws, the rest of the queue is
|
|
215733
|
+
* NOT auto-run — each remaining entry is rejected with a notice and the queue cleared,
|
|
215734
|
+
* so buffered work never chains onto a broken session.
|
|
215735
|
+
*/
|
|
215736
|
+
paused(agentId) {
|
|
215737
|
+
return this.agents.get(agentId)?.pause === true;
|
|
215738
|
+
}
|
|
215739
|
+
async runLoop(key, firstEntry) {
|
|
215740
|
+
let entry = firstEntry;
|
|
215741
|
+
while (entry) {
|
|
215742
|
+
if (entry !== firstEntry && (this.draining || this.drainingAgents.has(entry.agentId) || this.paused(entry.agentId))) {
|
|
215743
|
+
this.terminateQueuedSink(entry);
|
|
215744
|
+
entry.resolve(null);
|
|
215745
|
+
const rest = this.serialQueue.get(key) ?? [];
|
|
215746
|
+
this.serialQueue.delete(key);
|
|
215747
|
+
for (const e of rest) {
|
|
215748
|
+
this.terminateQueuedSink(e);
|
|
215749
|
+
e.resolve(null);
|
|
215750
|
+
}
|
|
215751
|
+
this.inflight.delete(key);
|
|
215752
|
+
return;
|
|
215753
|
+
}
|
|
215754
|
+
try {
|
|
215755
|
+
const sessionId = await this.dispatchOne(entry, key);
|
|
215756
|
+
entry.resolve(sessionId);
|
|
215757
|
+
} catch (err) {
|
|
215758
|
+
entry.reject(err);
|
|
215759
|
+
const rest = this.serialQueue.get(key) ?? [];
|
|
215760
|
+
this.serialQueue.delete(key);
|
|
215761
|
+
for (const e of rest) {
|
|
215762
|
+
const failStop = new FailStopError(key);
|
|
215763
|
+
this.terminateQueuedSink(e, failStop.message);
|
|
215764
|
+
e.reject(failStop);
|
|
215765
|
+
}
|
|
215766
|
+
this.inflight.delete(key);
|
|
215767
|
+
return;
|
|
215768
|
+
}
|
|
215769
|
+
const q = this.serialQueue.get(key);
|
|
215770
|
+
entry = q?.shift();
|
|
215771
|
+
if (q && q.length === 0) this.serialQueue.delete(key);
|
|
215653
215772
|
}
|
|
215773
|
+
this.inflight.delete(key);
|
|
215774
|
+
}
|
|
215775
|
+
async dispatchOne(entry, key) {
|
|
215776
|
+
const { agentId, msg, integrationId, webchat, callMeta } = entry;
|
|
215654
215777
|
const agent = this.agents.get(agentId);
|
|
215655
|
-
if (agent.pause) {
|
|
215656
|
-
this.log.info(`dispatch: skipped for agent "${agentId}" (paused)`);
|
|
215657
|
-
return null;
|
|
215658
|
-
}
|
|
215659
215778
|
const rec = new TranscriptRecorder();
|
|
215660
215779
|
const replyConn = msg.headless || webchat ? void 0 : this.replyConnFor(agentId, integrationId);
|
|
215661
215780
|
const wasRunning = this.hostStarts.has(agentId);
|
|
215662
215781
|
const statusThread = msg.thread ?? msg.msgId;
|
|
215663
|
-
const key = sessionKey(msg.platform, msg.channel, statusThread, agentId);
|
|
215664
215782
|
const mode = this.store.getOutputModeOverride(key) ?? agent.output.mode;
|
|
215665
215783
|
const conv = msg.platform === "telegram" ? new TelegramConverger(mode) : msg.platform === "discord" ? new DiscordConverger(mode) : new OutputConverger(mode);
|
|
215666
215784
|
this.showActivity(replyConn, msg.channel, statusThread, wasRunning ? "is thinking…" : "is starting up…");
|
|
@@ -215799,18 +215917,28 @@ var Daemon = class Daemon {
|
|
|
215799
215917
|
this.clearCancelBackstop(sessionId);
|
|
215800
215918
|
p.resolveDone();
|
|
215801
215919
|
}
|
|
215802
|
-
this.flushQueued(agentId, sessionId, integrationId);
|
|
215803
215920
|
return sessionId;
|
|
215804
215921
|
}
|
|
215805
|
-
/** Interrupt
|
|
215806
|
-
* drop anything queued behind it
|
|
215807
|
-
*
|
|
215808
|
-
*
|
|
215809
|
-
|
|
215810
|
-
|
|
215922
|
+
/** Interrupt a session by its LOGICAL sessionKey (shared by `!stop`, `!cancel`, and
|
|
215923
|
+
* webchat cancel): reject+drop anything queued behind it in the serial gate, then — if
|
|
215924
|
+
* a turn is actually in flight (ACP session live) — release any unanswered cards, mark
|
|
215925
|
+
* it `cancelling`, send ACP `session/cancel`, and arm the force backstop. Works for a
|
|
215926
|
+
* queued-but-not-yet-started or cold session too (§6.9 #390): draining the queue is
|
|
215927
|
+
* enough, no ACP id required. Muting is the CALLER's concern — `!stop` mutes,
|
|
215928
|
+
* `!cancel`/webchat-cancel do not. */
|
|
215929
|
+
interruptTurn(agentId, key, reason, acpSessionId) {
|
|
215930
|
+
const queued = this.serialQueue.get(key);
|
|
215931
|
+
if (queued && queued.length > 0) {
|
|
215932
|
+
this.serialQueue.delete(key);
|
|
215933
|
+
for (const e of queued) {
|
|
215934
|
+
this.terminateQueuedSink(e, reason);
|
|
215935
|
+
e.reject(new FailStopError(key));
|
|
215936
|
+
}
|
|
215937
|
+
}
|
|
215938
|
+
this.log.info(`command: ${reason} → agent "${agentId}" session ${key}${acpSessionId ? ` (${acpSessionId})` : ""}`);
|
|
215939
|
+
if (!acpSessionId || !this.pending.has(acpSessionId)) return;
|
|
215811
215940
|
this.releasePermissions(acpSessionId);
|
|
215812
215941
|
this.releaseElicits(acpSessionId);
|
|
215813
|
-
this.log.info(`command: ${reason} → agent "${agentId}" session ${acpSessionId}`);
|
|
215814
215942
|
this.store.setSessionState(key, "cancelling", this.clock.now());
|
|
215815
215943
|
this.hosts.get(agentId)?.cancel(acpSessionId).catch((err) => this.log.error(`command ${reason}: cancel failed: ${err.message}`));
|
|
215816
215944
|
this.armCancelBackstop(agentId, acpSessionId, key);
|
|
@@ -216723,13 +216851,24 @@ var Daemon = class Daemon {
|
|
|
216723
216851
|
async drainForShutdown() {
|
|
216724
216852
|
this.draining = true;
|
|
216725
216853
|
const live = [...this.pending.values()];
|
|
216726
|
-
if (live.length
|
|
216727
|
-
|
|
216728
|
-
|
|
216729
|
-
|
|
216730
|
-
|
|
216731
|
-
|
|
216854
|
+
if (live.length > 0) {
|
|
216855
|
+
const deadlineMs = this.cfg.limits.shutdownDrainMs;
|
|
216856
|
+
this.log.info(`shutdown: draining ${live.length} in-flight turn(s) (deadline ${Math.round(deadlineMs / 1e3)}s)`);
|
|
216857
|
+
if (await this.raceDeadline(Promise.all(live.map((p) => p.done)), deadlineMs) === "timeout") {
|
|
216858
|
+
this.log.warn(`shutdown: deadline hit with ${this.pending.size} turn(s) still in flight — cancelling`);
|
|
216859
|
+
for (const [sid, p] of this.pending) await this.hosts.get(p.agentId)?.cancel(sid).catch(() => {});
|
|
216860
|
+
}
|
|
216861
|
+
}
|
|
216862
|
+
this.settleQueuedForShutdown();
|
|
216863
|
+
}
|
|
216864
|
+
/** Reject and clear every entry still in the serial gate's queues (shutdown/teardown),
|
|
216865
|
+
* so no admitted-but-unrun message leaves an unsettled dispatch() promise. */
|
|
216866
|
+
settleQueuedForShutdown() {
|
|
216867
|
+
for (const [key, entries] of this.serialQueue) for (const e of entries) {
|
|
216868
|
+
this.terminateQueuedSink(e, "daemon shutting down");
|
|
216869
|
+
e.reject(new FailStopError(key));
|
|
216732
216870
|
}
|
|
216871
|
+
this.serialQueue.clear();
|
|
216733
216872
|
}
|
|
216734
216873
|
/** daemon/restart + daemon/upgrade (§8.3): ack now, then drain + stop + exit so
|
|
216735
216874
|
* the supervisor relaunches (the new binary, for upgrade). */
|
|
@@ -217133,4 +217272,4 @@ async function runForeground(opts, deps = {}) {
|
|
|
217133
217272
|
//#endregion
|
|
217134
217273
|
export { ClientTransport as i, CP_SUBPROTOCOL as n, CP_WS_PATH as r, runForeground as t };
|
|
217135
217274
|
|
|
217136
|
-
//# sourceMappingURL=run-foreground-
|
|
217275
|
+
//# sourceMappingURL=run-foreground-BBBkQ93M.js.map
|