@agentconnect.md/daemon 1.0.0-rc.281 → 1.0.0-rc.283

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.
@@ -1 +1 @@
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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"}
1
+ {"version":3,"file":"login-eP4k_Gf_.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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"}
@@ -0,0 +1,2 @@
1
+ import { t as runForeground } from "./run-foreground-Cennaxiw.js";
2
+ export { runForeground };
@@ -3,9 +3,9 @@ import { n as require_ms, t as require_src$3 } from "./src-DDtlVutj.js";
3
3
  import { n as init_esm_shims, t as __dirname } from "./esm-shims-DxGcm4Hw.js";
4
4
  import { t as DAEMON_VERSION } from "./version-DGaRXjBK.js";
5
5
  import { C as _coercedNumber, T as NEVER, a as array, d as literal, f as looseObject, i as any, m as object, o as boolean, p as number$1, t as ZodNumber, v as string, x as url$4 } from "./schemas-CQkO9hC5.js";
6
- import { C as ZodIssueCode, S as RESERVED_MCP_SERVER_NAME, _ as buildEnvelope, a as agentChildEnv, b as MAX_FRAME_BYTES, c as persistRelays, d as normalizeGitUrl, f as RELAY_DAEMON_SUBPROTOCOL, g as SHARED_CONFIG_ACTION_ID, h as decodeRelayDaemonFrame, i as selectAgent, m as buildRelayDaemonFrame, n as findAgentFiles, o as loadConfig, p as RELAY_DAEMON_WS_PATH, r as loadAgents, s as persistDaemonId, v as decodeEnvelope, y as encode } from "./load-agents-Amic2B8b.js";
6
+ import { C as ZodIssueCode, S as RESERVED_MCP_SERVER_NAME, _ as buildEnvelope, a as agentChildEnv, b as MAX_FRAME_BYTES, c as persistRelays, d as normalizeGitUrl, f as RELAY_DAEMON_SUBPROTOCOL, g as SHARED_CONFIG_ACTION_ID, h as decodeRelayDaemonFrame, i as selectAgent, m as buildRelayDaemonFrame, n as findAgentFiles, o as loadConfig, p as RELAY_DAEMON_WS_PATH, r as loadAgents, s as persistDaemonId, v as decodeEnvelope, y as encode } from "./load-agents-DzCnR0cj.js";
7
7
  import { l as resolveRoot, o as mcpSocketPath, u as statePath } from "./paths-DEcUO8lC.js";
8
- import { S as resolveRuntimes, _ as writeMemoryFile, a as initGitInjection, b as cachedRuntimeNames, c as simpleGit, d as memoryProviderFor, f as MemoryConflictError, g as resolveInMemoryDir, h as listMemory, i as prepareWorkspace, l as createMemoryProvider, m as MemoryTooLargeError, n as installedRuntimes, o as probeGitVersion, p as MemoryPathError, r as prefetchWorkspace, s as sessionGitEnv, t as AcpHost, u as memoryKindOf, v as MEMORY_TOOL_NAMES, x as cachedRuntimeVersions, y as toolsForIntegrations } from "./acp-host-CrjtbS3A.js";
8
+ import { S as resolveRuntimes, _ as writeMemoryFile, a as initGitInjection, b as cachedRuntimeNames, c as simpleGit, d as memoryProviderFor, f as MemoryConflictError, g as resolveInMemoryDir, h as listMemory, i as prepareWorkspace, l as createMemoryProvider, m as MemoryTooLargeError, n as installedRuntimes, o as probeGitVersion, p as MemoryPathError, r as prefetchWorkspace, s as sessionGitEnv, t as AcpHost, u as memoryKindOf, v as MEMORY_TOOL_NAMES, x as cachedRuntimeVersions, y as toolsForIntegrations } from "./acp-host-C-4zFhF8.js";
9
9
  import { i as GitCredentialCache, n as gitcredSocketPath, r as writeGitcredShim, t as GitCredServer } from "./gitcred-server-CMC80O_1.js";
10
10
  import { B as ToolListChangedNotificationSchema, D as ListPromptsResultSchema, E as ListChangedOptionsBaseSchema, F as PromptListChangedNotificationSchema, G as isZ4Schema, H as isJSONRPCRequest, I as ReadResourceResultSchema, J as encodeFrame, K as safeParse, L as ResourceListChangedNotificationSchema, M as ListToolsResultSchema, O as ListResourceTemplatesResultSchema, P as McpError, R as SUPPORTED_PROTOCOL_VERSIONS, S as InitializeResultSchema, T as LATEST_PROTOCOL_VERSION, U as isJSONRPCResultResponse, V as isInitializedNotification, W as getObjectShape, _ as ElicitResultSchema, a as AjvJsonSchemaValidator, b as GetPromptResultSchema, c as mergeCapabilities, d as CompleteResultSchema, f as CreateMessageRequestSchema, g as ElicitRequestSchema, h as CreateTaskResultSchema, i as assertToolsCallTaskCapability, k as ListResourcesResultSchema, m as CreateMessageResultWithToolsSchema, n as serializeMessage, o as require_fast_deep_equal, p as CreateMessageResultSchema, q as decodeFrames, r as assertClientRequestTaskCapability, s as Protocol, t as ReadBuffer, u as CallToolResultSchema, v as EmptyResultSchema, w as JSONRPCMessageSchema, y as ErrorCode } from "./stdio-DnS8WcXi.js";
11
11
  import { URL as URL$1 } from "url";
@@ -4516,6 +4516,9 @@ var CpClient = class {
4516
4516
  case "relay/roster":
4517
4517
  this.deps.configApply.applyRelayRoster(frame.payload.relays);
4518
4518
  return;
4519
+ case "collaboration/routes":
4520
+ this.deps.configApply.applyCollabRoutes(frame.payload);
4521
+ return;
4519
4522
  case "agent/upsert": {
4520
4523
  const u = frame.payload;
4521
4524
  this.deps.configApply.applyAgentUpsert(u.agentId, u.spec);
@@ -6370,6 +6373,26 @@ var LocalStore = class {
6370
6373
  CREATE TABLE IF NOT EXISTS cron_runs (
6371
6374
  key TEXT PRIMARY KEY, lastRunAt INTEGER NOT NULL
6372
6375
  );
6376
+ -- §6.9 #353 durable inbox: an ADMITTED-but-QUEUED message persisted BEFORE the
6377
+ -- admission ACK, so a hard kill / agent move can't lose a message the caller was
6378
+ -- already told delivered:true. Replayed FIFO-by-sessionKey on startup and removed
6379
+ -- on every terminal path (success / reject / cancel / gate-drop). The id column is the
6380
+ -- message's STABLE deliveryId/msgId (§6.3) so re-append and replay are idempotent
6381
+ -- against the existing admission-idempotency maps. Webchat turns are NOT persisted
6382
+ -- here (their live sink can't be restored — see §6.9 #367). enqueuedAt is a
6383
+ -- monotonic decimal string (fixed-width ⇒ string order == numeric order) driving
6384
+ -- FIFO within a sessionKey.
6385
+ CREATE TABLE IF NOT EXISTS inbox (
6386
+ id TEXT PRIMARY KEY,
6387
+ sessionKey TEXT NOT NULL,
6388
+ agentId TEXT NOT NULL,
6389
+ msg TEXT NOT NULL,
6390
+ integrationId TEXT,
6391
+ callMeta TEXT,
6392
+ isQueueCmd INTEGER,
6393
+ enqueuedAt TEXT NOT NULL
6394
+ );
6395
+ CREATE INDEX IF NOT EXISTS inbox_fifo ON inbox (sessionKey, enqueuedAt);
6373
6396
  `);
6374
6397
  this.migrateSessionUsage();
6375
6398
  this.migrateTranscriptToolBody();
@@ -6724,6 +6747,32 @@ var LocalStore = class {
6724
6747
  getCronLastRun(key) {
6725
6748
  return this.db.prepare("SELECT lastRunAt FROM cron_runs WHERE key = ?").get(key)?.lastRunAt;
6726
6749
  }
6750
+ /** §6.9 #353 durable inbox: persist an admitted message BEFORE its admission ACK. Keyed
6751
+ * by the stable id (deliveryId/msgId) with INSERT OR IGNORE, so a re-append of the same
6752
+ * delivery (P2 retry / replay) is a no-op — idempotent against the admission maps. */
6753
+ appendInbox(row) {
6754
+ this.db.prepare(`INSERT OR IGNORE INTO inbox (id, sessionKey, agentId, msg, integrationId, callMeta, isQueueCmd, enqueuedAt)
6755
+ VALUES (@id, @sessionKey, @agentId, @msg, @integrationId, @callMeta, @isQueueCmd, @enqueuedAt)`).run({
6756
+ id: row.id,
6757
+ sessionKey: row.sessionKey,
6758
+ agentId: row.agentId,
6759
+ msg: row.msg,
6760
+ integrationId: row.integrationId ?? null,
6761
+ callMeta: row.callMeta ?? null,
6762
+ isQueueCmd: row.isQueueCmd ?? null,
6763
+ enqueuedAt: row.enqueuedAt
6764
+ });
6765
+ }
6766
+ /** Remove an inbox row once its turn reaches a terminal state (success / reject / cancel /
6767
+ * gate-drop) so it is never replayed. No-op on an unknown id. */
6768
+ removeInbox(id) {
6769
+ this.db.prepare("DELETE FROM inbox WHERE id = ?").run(id);
6770
+ }
6771
+ /** All pending inbox rows, ordered FIFO-by-sessionKey (sessionKey, then enqueuedAt) for
6772
+ * startup replay (§6.9 #353). Order within a sessionKey is preserved by `enqueuedAt`. */
6773
+ listInboxBySessionKeyFifo() {
6774
+ return this.db.prepare("SELECT * FROM inbox ORDER BY sessionKey ASC, enqueuedAt ASC").all();
6775
+ }
6727
6776
  close() {
6728
6777
  this.db.close();
6729
6778
  }
@@ -197213,6 +197262,18 @@ var RelayClient = class {
197213
197262
  isReady() {
197214
197263
  return this.state === "READY";
197215
197264
  }
197265
+ /**
197266
+ * Send a cross-daemon agent-call to the relay (`rd/agentmsg` REQ, agent-collaboration
197267
+ * §2.3/§6.4) and resolve with the relay's admission verdict. `claimedFromAgentId` is
197268
+ * this daemon's trusted caller — the relay re-validates it against the socket's
197269
+ * authenticated daemonId. Rejects (retryable) if the socket isn't READY.
197270
+ */
197271
+ async sendAgentMsg(payload) {
197272
+ if (this.state !== "READY") throw new WireError("INTERNAL", `rd link not ready (${this.state})`, true);
197273
+ const rep = await this.sendRequest(buildRelayDaemonFrame("rd/agentmsg", payload));
197274
+ if (rep.type !== "rd/agentmsg/ack") throw new WireError("INTERNAL", `expected rd/agentmsg/ack, got ${rep.type}`, false);
197275
+ return rep.payload;
197276
+ }
197216
197277
  async attemptConnect() {
197217
197278
  if (this.stopped || this.fatal) return;
197218
197279
  const daemonId = this.deps.daemonId();
@@ -197264,6 +197325,10 @@ var RelayClient = class {
197264
197325
  this.handleMsg(frame.id, frame.payload);
197265
197326
  return;
197266
197327
  }
197328
+ if (frame.type === "rd/agentmsg/fwd") {
197329
+ this.handleAgentMsgFwd(frame.id, frame.payload);
197330
+ return;
197331
+ }
197267
197332
  this.deps.log.debug(`relay(${this.relayId}): ignoring ${frame.type}`);
197268
197333
  }
197269
197334
  /** An inbound rd/msg: dispatch it, reply `rd/ack`; only a webchat op streams
@@ -197274,7 +197339,22 @@ var RelayClient = class {
197274
197339
  const ack = this.deps.onRelayMsg(msg, chat);
197275
197340
  this.transport?.send(JSON.stringify(buildRelayDaemonFrame("rd/ack", ack, { corr: reqId })));
197276
197341
  }
197277
- /** Send one reply chunk back over THIS relay's socket, stamping the per-chat `seq`. */
197342
+ /** A forwarded cross-daemon agent-call: terminal-verify + dispatch, reply the typed
197343
+ * admission verdict (`rd/agentmsg/ack`, corr = the relay's fwd id). */
197344
+ async handleAgentMsgFwd(reqId, msg) {
197345
+ let ack;
197346
+ try {
197347
+ ack = await this.deps.onRelayAgentMsg(msg);
197348
+ } catch (err) {
197349
+ this.deps.log.error(`relay(${this.relayId}): rd/agentmsg/fwd handler threw: ${err.message}`);
197350
+ ack = {
197351
+ deliveryId: msg.deliveryId,
197352
+ delivered: false,
197353
+ reason: "offline"
197354
+ };
197355
+ }
197356
+ this.transport?.send(JSON.stringify(buildRelayDaemonFrame("rd/agentmsg/ack", ack, { corr: reqId })));
197357
+ }
197278
197358
  sendChat(chatId, event) {
197279
197359
  const seq = (this.seqByChat.get(chatId) ?? 0) + 1;
197280
197360
  this.seqByChat.set(chatId, seq);
@@ -197346,6 +197426,45 @@ var RelayManager = class {
197346
197426
  size() {
197347
197427
  return this.clients.size;
197348
197428
  }
197429
+ /**
197430
+ * Send a cross-daemon agent-call over ANY READY relay (agent-collaboration §2.3/§6.4).
197431
+ * The all-to-all roster means any relay this daemon holds can route the call to the
197432
+ * target's owning daemon via the CP's collaboration snapshot — so we pick the first
197433
+ * READY client. Throws if none is READY (the caller reports the call undeliverable).
197434
+ */
197435
+ async sendAgentMsg(payload) {
197436
+ for (const client of this.clients.values()) if (client.isReady()) return client.sendAgentMsg(payload);
197437
+ throw new Error("no READY relay to route agent-call");
197438
+ }
197439
+ };
197440
+ //#endregion
197441
+ //#region src/cp/cp-collab-routes.ts
197442
+ var CpCollabRoutes = class {
197443
+ generation = -1;
197444
+ channels = /* @__PURE__ */ new Map();
197445
+ key(orgId, platform, channelId) {
197446
+ return orgId + "\0" + platform + "\0" + channelId;
197447
+ }
197448
+ /** FULL-REPLACE from a CP snapshot (converge-don't-diff); ignore an older generation.
197449
+ * Tolerates an absent snapshot (a pre-collab CP / hand-built test fixture) as a no-op. */
197450
+ replace(snap) {
197451
+ if (!snap) return;
197452
+ if (snap.generation < this.generation) return;
197453
+ this.generation = snap.generation;
197454
+ this.channels.clear();
197455
+ for (const ch of snap.channels) {
197456
+ const byAgent = /* @__PURE__ */ new Map();
197457
+ for (const a of ch.agents) byAgent.set(a.agentId, {
197458
+ ...a,
197459
+ orgId: ch.orgId
197460
+ });
197461
+ this.channels.set(this.key(ch.orgId, ch.platform, ch.channelId), byAgent);
197462
+ }
197463
+ }
197464
+ /** The placement of `agentId` in `(orgId,platform,channel)`, or undefined. */
197465
+ resolve(orgId, platform, channelId, agentId) {
197466
+ return this.channels.get(this.key(orgId, platform, channelId))?.get(agentId);
197467
+ }
197349
197468
  };
197350
197469
  //#endregion
197351
197470
  //#region src/slack/permalink.ts
@@ -213917,6 +214036,10 @@ const IDLE_FLUSH_MS = 2e3;
213917
214036
  /** Public Web App console origin used for session deep links when neither a local
213918
214037
  * `webAppUrl` config nor a CP-provided one is set — so "View session" always resolves. */
213919
214038
  const DEFAULT_WEB_APP_URL = "https://app.agentconnect.md";
214039
+ /** Per-in-flight-turn rendering state, keyed by ACP sessionId in `this.pending`. */
214040
+ /** Cap on agent→agent hop depth (design §2.4/§4.5) — reject a `messageAgent` that
214041
+ * would push the outgoing hopCount past this, so an A↔B wake loop can't run away. */
214042
+ const MAX_AGENT_CALL_HOPS = 8;
213920
214043
  /** Build the wire SessionKey (protocol §5) for a pending turn — what `drain/done`
213921
214044
  * reports as released so the CP may reassign it. Uses the real `thread` (absent for
213922
214045
  * a channel-root message), NOT `statusThread` (which falls back to msgId): the CP
@@ -213992,6 +214115,7 @@ var Daemon = class Daemon {
213992
214115
  cpIntegrations;
213993
214116
  botUserIds = {};
213994
214117
  cpRouting;
214118
+ cpCollab = new CpCollabRoutes();
213995
214119
  clock;
213996
214120
  requestExit;
213997
214121
  draining = false;
@@ -214217,6 +214341,7 @@ var Daemon = class Daemon {
214217
214341
  };
214218
214342
  this.watcher.on("add", debounced).on("change", debounced).on("unlink", debounced);
214219
214343
  this.log.info(`watching ${this.agentsDir} for agent changes`);
214344
+ this.replayInbox();
214220
214345
  this.startCpClient(root);
214221
214346
  this.armIdleSweep();
214222
214347
  this.log.info("daemon ready");
@@ -215027,6 +215152,74 @@ var Daemon = class Daemon {
215027
215152
  accepted: true
215028
215153
  };
215029
215154
  }
215155
+ relayAgentMsgAcks = /* @__PURE__ */ new Map();
215156
+ /**
215157
+ * TARGET side of a cross-daemon `messageAgent` (agent-collaboration §2.3/§6.2, P2) —
215158
+ * the mirror of {@link handleRelayIm} for a relay-forwarded `rd/agentmsg/fwd`. The
215159
+ * relay already validated the caller and minted a TRUSTED claim
215160
+ * (`trustedFromAgentId` + org/channel assertion), but we TERMINAL-VERIFY it against
215161
+ * our OWN local snapshot/spec (defense in depth, §2.5 #4) before dispatching — never
215162
+ * blindly trusting the relay. The ACK is returned on durable ADMISSION (the P4-gate
215163
+ * enqueue), NOT after the model turn (§6.4).
215164
+ */
215165
+ async handleRelayAgentMsg(msg) {
215166
+ const nak = (reason) => ({
215167
+ deliveryId: msg.deliveryId,
215168
+ delivered: false,
215169
+ ...reason ? { reason } : {}
215170
+ });
215171
+ const dedupKey = `${msg.trustedFromAgentId}:${msg.deliveryId}`;
215172
+ const prior = this.relayAgentMsgAcks.get(dedupKey);
215173
+ if (prior) return prior;
215174
+ const record = (ack) => {
215175
+ if (this.relayAgentMsgAcks.size >= 2e3) this.relayAgentMsgAcks.clear();
215176
+ this.relayAgentMsgAcks.set(dedupKey, ack);
215177
+ return ack;
215178
+ };
215179
+ if (msg.hopCount > MAX_AGENT_CALL_HOPS) return record(nak("hop_limit"));
215180
+ const { platform, channel, thread } = msg.coords;
215181
+ if (!this.agents.get(msg.toAgentId)) return record(nak("not_found"));
215182
+ const target = this.cpCollab.resolve(msg.orgId, platform, channel, msg.toAgentId);
215183
+ const caller = this.cpCollab.resolve(msg.orgId, platform, channel, msg.trustedFromAgentId);
215184
+ if (!target || !caller) {
215185
+ this.log.warn(`relay: rd/agentmsg/fwd terminal-verify failed (no placement) for ${msg.toAgentId} — fail closed`);
215186
+ return record(nak("not_allowed"));
215187
+ }
215188
+ if (target.callPolicy === "selected" && !target.allowedCallerAgentIds.includes(msg.trustedFromAgentId)) {
215189
+ this.log.info(`relay: rd/agentmsg/fwd not_allowed — ${msg.trustedFromAgentId} → ${msg.toAgentId} (selected)`);
215190
+ return record(nak("not_allowed"));
215191
+ }
215192
+ const callMeta = {
215193
+ callFrom: msg.trustedFromAgentId,
215194
+ ...msg.correlationId !== void 0 ? { correlationId: msg.correlationId } : {},
215195
+ hopCount: msg.hopCount,
215196
+ deliveryId: msg.deliveryId
215197
+ };
215198
+ const narrowed = this.narrowPlatform(platform);
215199
+ const resolved = this.resolveCpAgent(msg.toAgentId);
215200
+ const integrationId = msg.integrationId ?? resolved?.integrationId;
215201
+ const normalized = {
215202
+ msgId: `agentcall:${channel}:${msg.deliveryId}`,
215203
+ traceId: msg.deliveryId,
215204
+ source: "agent",
215205
+ platform: narrowed,
215206
+ channel,
215207
+ ...thread !== void 0 ? { thread } : {},
215208
+ sender: {
215209
+ id: msg.trustedFromAgentId,
215210
+ isBot: true
215211
+ },
215212
+ text: `@${msg.trustedFromAgentId} asked:\n${msg.text}`,
215213
+ mentionedBots: resolved?.botUserId ? [resolved.botUserId] : [],
215214
+ isDm: false
215215
+ };
215216
+ this.dispatch(msg.toAgentId, normalized, integrationId, void 0, callMeta).catch((err) => this.log.error(`relay agentmsg dispatch failed for agent "${msg.toAgentId}": ${formatErr(err)}`));
215217
+ this.log.info(`relay: rd/agentmsg/fwd ${msg.trustedFromAgentId} → ${msg.toAgentId} delivery=${msg.deliveryId}`);
215218
+ return record({
215219
+ deliveryId: msg.deliveryId,
215220
+ delivered: true
215221
+ });
215222
+ }
215030
215223
  /**
215031
215224
  * Same-daemon agent→agent delivery — the impl behind the `messageAgent` MCP tool
215032
215225
  * (design §2.2/§2.3, P1). Wakes `toAgentId` by constructing a NormalizedMessage at
@@ -215061,11 +215254,16 @@ var Daemon = class Daemon {
215061
215254
  return result;
215062
215255
  };
215063
215256
  const target = this.agents.get(req.toAgentId);
215064
- if (!target) return record({
215065
- delivered: false,
215066
- targetSession,
215067
- reason: "not_local"
215068
- });
215257
+ if (!target) {
215258
+ const coordPlatform = platform === "hook" ? "slack" : platform;
215259
+ return record(await this.routeAgentMsgCrossDaemon(req, {
215260
+ platform: coordPlatform,
215261
+ channel,
215262
+ thread,
215263
+ deliveryId,
215264
+ targetSession
215265
+ }));
215266
+ }
215069
215267
  if (target.callPolicy === "selected" && !target.allowedCallerAgentIds.includes(req.callerAgentId)) {
215070
215268
  this.log.info(`messageAgent: ${req.callerAgentId} not allowed to call ${req.toAgentId} (policy=selected)`);
215071
215269
  return record({
@@ -215106,6 +215304,53 @@ var Daemon = class Daemon {
215106
215304
  targetSession
215107
215305
  });
215108
215306
  }
215307
+ /**
215308
+ * Route a cross-daemon `messageAgent` over the relay data plane (agent-collaboration
215309
+ * §2.3/§6.2/§6.4, P2) and map the relay's typed admission verdict to a
215310
+ * {@link MessageAgentResult}. The `claimedFromAgentId` is our trusted caller — the
215311
+ * relay re-validates it against our authenticated daemonId (a forged claim is
215312
+ * rejected there). `hopCount` is the source turn's depth (0 in P1's minimal
215313
+ * cross-turn model); the relay increments it (+1, cap 8). The body never touches
215314
+ * the CP — only the relay.
215315
+ */
215316
+ async routeAgentMsgCrossDaemon(req, ctx) {
215317
+ if (!this.relays) return {
215318
+ delivered: false,
215319
+ targetSession: ctx.targetSession,
215320
+ reason: "not_local"
215321
+ };
215322
+ try {
215323
+ const ack = await this.relays.sendAgentMsg({
215324
+ claimedFromAgentId: req.callerAgentId,
215325
+ toAgentId: req.toAgentId,
215326
+ text: req.text,
215327
+ coords: {
215328
+ platform: ctx.platform,
215329
+ channel: ctx.channel,
215330
+ ...ctx.thread !== void 0 ? { thread: ctx.thread } : {}
215331
+ },
215332
+ ...req.correlationId !== void 0 ? { correlationId: req.correlationId } : {},
215333
+ hopCount: 0,
215334
+ deliveryId: ctx.deliveryId
215335
+ });
215336
+ if (ack.delivered) return {
215337
+ delivered: true,
215338
+ targetSession: ctx.targetSession
215339
+ };
215340
+ return {
215341
+ delivered: false,
215342
+ targetSession: ctx.targetSession,
215343
+ ...ack.reason ? { reason: ack.reason } : {}
215344
+ };
215345
+ } catch (err) {
215346
+ this.log.warn(`messageAgent: cross-daemon route failed for ${req.toAgentId}: ${formatErr(err)}`);
215347
+ return {
215348
+ delivered: false,
215349
+ targetSession: ctx.targetSession,
215350
+ reason: "offline"
215351
+ };
215352
+ }
215353
+ }
215109
215354
  /** Narrow the trusted session-context platform string to the NormalizedMessage union;
215110
215355
  * falls back to 'slack' for an unrecognized value (coords still resolve — the union is
215111
215356
  * a routing/key detail, not the trust basis). */
@@ -215339,6 +215584,10 @@ var Daemon = class Daemon {
215339
215584
  }
215340
215585
  inflight = /* @__PURE__ */ new Set();
215341
215586
  serialQueue = /* @__PURE__ */ new Map();
215587
+ /** §6.9 #353: stable ids of messages currently backed by a durable inbox row (admitted,
215588
+ * not yet terminal). Guards startup replay from re-admitting a row whose entry is already
215589
+ * live in the gate (idempotency — a duplicate/in-flight id is not double-processed). */
215590
+ liveInboxIds = /* @__PURE__ */ new Set();
215342
215591
  /**
215343
215592
  * Resolve a command's target from the channel's latest session when the routing ladder
215344
215593
  * couldn't (no mention entity / thread / dm rule matched — e.g. a group `/status@bot`).
@@ -215670,6 +215919,49 @@ var Daemon = class Daemon {
215670
215919
  }
215671
215920
  }
215672
215921
  /**
215922
+ * §6.9 #353 durable inbox: persist an ADMITTED entry BEFORE its admission ACK/return, so a
215923
+ * hard kill / agent move can't lose a message the caller was already told delivered:true.
215924
+ * Called ONLY on the two genuine admission branches in dispatch() (claim + enqueue), never
215925
+ * on a rejected/gate-dropped admission — a queue-full or paused/draining drop persists
215926
+ * nothing. WEBCHAT turns are skipped (§6.9 #367): their `sink` is a live in-memory transport
215927
+ * that can't be restored across a restart and a dead browser socket can't be resumed, so a
215928
+ * durable row would be un-replayable. Sets `entry.inboxId` so every terminal path can delete
215929
+ * the row. The row id is the message's stable deliveryId/msgId (§6.3) — idempotent re-append.
215930
+ */
215931
+ persistInbox(entry, key) {
215932
+ if (entry.webchat) return;
215933
+ const id = entry.callMeta?.deliveryId ?? entry.msg.msgId;
215934
+ try {
215935
+ this.store.appendInbox({
215936
+ id,
215937
+ sessionKey: key,
215938
+ agentId: entry.agentId,
215939
+ msg: JSON.stringify(entry.msg),
215940
+ integrationId: entry.integrationId ?? null,
215941
+ callMeta: entry.callMeta ? JSON.stringify(entry.callMeta) : null,
215942
+ isQueueCmd: entry.isQueueCmd ? 1 : null,
215943
+ enqueuedAt: monotonicTs()
215944
+ });
215945
+ entry.inboxId = id;
215946
+ this.liveInboxIds.add(id);
215947
+ } catch (err) {
215948
+ this.log.warn(`durable inbox: append failed for ${id}: ${err.message}`);
215949
+ }
215950
+ }
215951
+ /** Remove an entry's durable inbox row once its turn reaches ANY terminal state (success,
215952
+ * reject/fail-stop, cancel, gate-drop) so it is not replayed on the next startup. No-op
215953
+ * for a non-persisted entry (webchat / never-admitted). NOT called on graceful-shutdown
215954
+ * settle: those admitted-but-unrun rows are intentionally LEFT for startup replay. */
215955
+ removeInbox(entry) {
215956
+ if (!entry.inboxId) return;
215957
+ this.liveInboxIds.delete(entry.inboxId);
215958
+ try {
215959
+ this.store.removeInbox(entry.inboxId);
215960
+ } catch (err) {
215961
+ this.log.warn(`durable inbox: remove failed for ${entry.inboxId}: ${err.message}`);
215962
+ }
215963
+ }
215964
+ /**
215673
215965
  * Terminate a QUEUED entry's webchat sink when it is gate-dropped/rejected without ever
215674
215966
  * entering dispatchOne (drain/pause gate-drop, fail-stop of the queued rest, `!cancel`/`!stop`
215675
215967
  * queue drop, shutdown). A webchat turn signals its client via the WebchatSink terminal
@@ -215717,9 +216009,11 @@ var Daemon = class Daemon {
215717
216009
  }
215718
216010
  q.push(entry);
215719
216011
  this.serialQueue.set(key, q);
216012
+ this.persistInbox(entry, key);
215720
216013
  this.log.debug(`dispatch: queued behind in-flight turn for session ${key} (depth ${q.length})`);
215721
216014
  return;
215722
216015
  }
216016
+ this.persistInbox(entry, key);
215723
216017
  this.inflight.add(key);
215724
216018
  this.runLoop(key, entry);
215725
216019
  });
@@ -215741,11 +216035,13 @@ var Daemon = class Daemon {
215741
216035
  while (entry) {
215742
216036
  if (entry !== firstEntry && (this.draining || this.drainingAgents.has(entry.agentId) || this.paused(entry.agentId))) {
215743
216037
  this.terminateQueuedSink(entry);
216038
+ if (!this.draining) this.removeInbox(entry);
215744
216039
  entry.resolve(null);
215745
216040
  const rest = this.serialQueue.get(key) ?? [];
215746
216041
  this.serialQueue.delete(key);
215747
216042
  for (const e of rest) {
215748
216043
  this.terminateQueuedSink(e);
216044
+ if (!this.draining) this.removeInbox(e);
215749
216045
  e.resolve(null);
215750
216046
  }
215751
216047
  this.inflight.delete(key);
@@ -215753,14 +216049,17 @@ var Daemon = class Daemon {
215753
216049
  }
215754
216050
  try {
215755
216051
  const sessionId = await this.dispatchOne(entry, key);
216052
+ this.removeInbox(entry);
215756
216053
  entry.resolve(sessionId);
215757
216054
  } catch (err) {
216055
+ if (!this.draining) this.removeInbox(entry);
215758
216056
  entry.reject(err);
215759
216057
  const rest = this.serialQueue.get(key) ?? [];
215760
216058
  this.serialQueue.delete(key);
215761
216059
  for (const e of rest) {
215762
216060
  const failStop = new FailStopError(key);
215763
216061
  this.terminateQueuedSink(e, failStop.message);
216062
+ if (!this.draining) this.removeInbox(e);
215764
216063
  e.reject(failStop);
215765
216064
  }
215766
216065
  this.inflight.delete(key);
@@ -215932,6 +216231,7 @@ var Daemon = class Daemon {
215932
216231
  this.serialQueue.delete(key);
215933
216232
  for (const e of queued) {
215934
216233
  this.terminateQueuedSink(e, reason);
216234
+ this.removeInbox(e);
215935
216235
  e.reject(new FailStopError(key));
215936
216236
  }
215937
216237
  }
@@ -216870,6 +217170,49 @@ var Daemon = class Daemon {
216870
217170
  }
216871
217171
  this.serialQueue.clear();
216872
217172
  }
217173
+ /**
217174
+ * §6.9 #353 startup replay: re-admit durably-persisted admitted-but-not-completed inbox rows
217175
+ * through the SAME serial gate (`dispatch`), FIFO-by-sessionKey (rows come back ordered by
217176
+ * (sessionKey, enqueuedAt), so order within a sessionKey is preserved and the gate keeps them
217177
+ * serial). Runs once on start() after the store is open, agents are loaded into `this.agents`,
217178
+ * and platform connections/crons are up (so a re-admitted turn has its reply transport), as
217179
+ * normal inbound begins. Idempotency: a row whose id is already live in the gate (`liveInboxIds`)
217180
+ * is skipped — dispatch re-appends the same id with INSERT OR IGNORE so no double row is written,
217181
+ * and the re-admitted entry adopts the existing row for later removal. Rows for an agent that no
217182
+ * longer exists on this daemon are SKIPPED + logged (the simplest choice — another owner/daemon
217183
+ * may hold that agent; we neither drop the row nor block on it). Webchat turns were never
217184
+ * persisted, so none appear here.
217185
+ */
217186
+ replayInbox() {
217187
+ let rows;
217188
+ try {
217189
+ rows = this.store.listInboxBySessionKeyFifo();
217190
+ } catch (err) {
217191
+ this.log.warn(`durable inbox: replay read failed: ${err.message}`);
217192
+ return;
217193
+ }
217194
+ if (rows.length === 0) return;
217195
+ let replayed = 0;
217196
+ for (const row of rows) {
217197
+ if (this.liveInboxIds.has(row.id)) continue;
217198
+ if (!this.agents.has(row.agentId)) {
217199
+ this.log.warn(`durable inbox: skipping replay of ${row.id} — unknown agent "${row.agentId}"`);
217200
+ continue;
217201
+ }
217202
+ let msg;
217203
+ let callMeta;
217204
+ try {
217205
+ msg = JSON.parse(row.msg);
217206
+ callMeta = row.callMeta ? JSON.parse(row.callMeta) : void 0;
217207
+ } catch (err) {
217208
+ this.log.warn(`durable inbox: skipping corrupt row ${row.id}: ${err.message}`);
217209
+ continue;
217210
+ }
217211
+ this.dispatch(row.agentId, msg, row.integrationId ?? void 0, void 0, callMeta, row.isQueueCmd ? { isQueueCmd: true } : void 0).catch(() => {});
217212
+ replayed++;
217213
+ }
217214
+ if (replayed) this.log.info(`durable inbox: replayed ${replayed} admitted message(s) through the serial gate`);
217215
+ }
216873
217216
  /** daemon/restart + daemon/upgrade (§8.3): ack now, then drain + stop + exit so
216874
217217
  * the supervisor relaunches (the new binary, for upgrade). */
216875
217218
  scheduleFleetExit(kind, targetVersion) {
@@ -216901,6 +217244,7 @@ var Daemon = class Daemon {
216901
217244
  this.cpAgents?.converge(snap.agents);
216902
217245
  this.cpIntegrations?.converge(snap.integrations);
216903
217246
  this.convergeRelays(snap.relays);
217247
+ this.cpCollab.replace(snap.collabRoutes);
216904
217248
  for (const id of snap.drop.crons) this.cpCrons?.remove(id);
216905
217249
  this.cpCrons?.converge(snap.crons);
216906
217250
  this.cpRouting?.converge({
@@ -216929,6 +217273,7 @@ var Daemon = class Daemon {
216929
217273
  applyRouteAssign: (a) => this.cpRouting?.upsertAssign(a),
216930
217274
  applyRouteUpdate: (u) => this.cpRouting?.applyUpdate(u),
216931
217275
  applyRelayRoster: (relays) => this.convergeRelays(relays),
217276
+ applyCollabRoutes: (snap) => this.cpCollab.replace(snap),
216932
217277
  applyAgentLaunch: async (launch) => {
216933
217278
  const agent = this.agents.get(launch.agentId);
216934
217279
  if (!agent) throw new Error(`agent/launch: unknown agent ${launch.agentId}`);
@@ -217065,7 +217410,8 @@ var Daemon = class Daemon {
217065
217410
  path: RELAY_DAEMON_WS_PATH
217066
217411
  }),
217067
217412
  log: this.log,
217068
- onRelayMsg: (msg, chat) => this.handleRelayMsg(msg, chat)
217413
+ onRelayMsg: (msg, chat) => this.handleRelayMsg(msg, chat),
217414
+ onRelayAgentMsg: (msg) => this.handleRelayAgentMsg(msg)
217069
217415
  });
217070
217416
  if (this.cfg.relays.length) this.relays.converge(this.cfg.relays);
217071
217417
  this.cpClient = new CpClient({
@@ -217272,4 +217618,4 @@ async function runForeground(opts, deps = {}) {
217272
217618
  //#endregion
217273
217619
  export { ClientTransport as i, CP_SUBPROTOCOL as n, CP_WS_PATH as r, runForeground as t };
217274
217620
 
217275
- //# sourceMappingURL=run-foreground-BBBkQ93M.js.map
217621
+ //# sourceMappingURL=run-foreground-Cennaxiw.js.map