@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 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-CzDsJwym.js"),
68497
- import("./run-foreground-CfLjjwW7.js"),
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-1y1IF3Dc.js";
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-CzDsJwym.js.map
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 (!rec?.acpSessionId || !this.pending.has(rec.acpSessionId)) return;
214875
- this.interruptTurn(rec.agentId, rec.acpSessionId, key, "cancel");
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, acpSessionId, p.sessionKey, "cancel");
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
- queued = /* @__PURE__ */ new Map();
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 = !!(acpSessionId && this.pending.has(acpSessionId));
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, acpSessionId, key, "stop");
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, acpSessionId, key, "cancel");
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 (!inflight) {
215450
- this.log.info(`command: queue → agent "${target.agentId}" idle, dispatching now`);
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
- q.push(payload);
215461
- this.queued.set(acpSessionId, q);
215462
- this.log.info(`command: queue → agent "${target.agentId}" session ${acpSessionId} (depth ${q.length})`);
215463
- reply(`📥 Queued (#${q.length}) — will run when the current turn finishes.`);
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
- async dispatch(agentId, msg, integrationId, webchat, callMeta) {
215650
- if (this.draining || this.drainingAgents.has(agentId)) {
215651
- this.log.debug(`dispatch: skipped for agent "${agentId}" (draining)`);
215652
- return null;
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 an in-flight turn (shared by `!stop`, `!cancel`, and webchat cancel):
215806
- * drop anything queued behind it, mark the session `cancelling`, send ACP
215807
- * `session/cancel`, and arm the force backstop. Muting is the CALLER's concern —
215808
- * `!stop` mutes, `!cancel`/webchat-cancel do not. `reason` is just for the log line. */
215809
- interruptTurn(agentId, acpSessionId, key, reason) {
215810
- this.queued.delete(acpSessionId);
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 === 0) return;
216727
- const deadlineMs = this.cfg.limits.shutdownDrainMs;
216728
- this.log.info(`shutdown: draining ${live.length} in-flight turn(s) (deadline ${Math.round(deadlineMs / 1e3)}s)`);
216729
- if (await this.raceDeadline(Promise.all(live.map((p) => p.done)), deadlineMs) === "timeout") {
216730
- this.log.warn(`shutdown: deadline hit with ${this.pending.size} turn(s) still in flight — cancelling`);
216731
- for (const [sid, p] of this.pending) await this.hosts.get(p.agentId)?.cancel(sid).catch(() => {});
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-1y1IF3Dc.js.map
217275
+ //# sourceMappingURL=run-foreground-BBBkQ93M.js.map