@agentchatme/agent-core 0.0.131 → 0.0.1312
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/README.md +5 -0
- package/dist/{chunk-XLRPNQ7G.js → chunk-AGDJ4A6R.js} +58 -1
- package/dist/chunk-AGDJ4A6R.js.map +1 -0
- package/dist/daemon-entry.d.ts +39 -4
- package/dist/daemon-entry.js +305 -65
- package/dist/daemon-entry.js.map +1 -1
- package/dist/index.d.ts +28 -4
- package/dist/index.js +203 -54
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-XLRPNQ7G.js.map +0 -1
package/dist/daemon-entry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/daemon/ws-client.ts","../src/daemon/frames.ts","../src/daemon/coord.ts","../src/daemon/format.ts","../src/daemon/run.ts","../src/daemon/config.ts","../src/daemon/loop.ts"],"sourcesContent":["import { WebSocket } from 'ws'\nimport { EventEmitter } from 'node:events'\nimport { log } from '../util/log.js'\nimport { parseInbound, type SyncRow } from './frames.js'\n\n// ─── Agent WebSocket client ─────────────────────────────────────────────────\n//\n// Connects to /v1/ws as the agent (Bearer auth). The server drains undelivered\n// as `message.new` frames on connect AND pushes them in real time. The `ws`\n// library auto-pongs the server's heartbeat pings, which keeps presence alive.\n// We add reconnect with exponential backoff + jitter, a liveness watchdog, and\n// a terminal state for auth failure so a bad key doesn't reconnect forever.\n\ntype State = 'connecting' | 'ready' | 'reconnecting' | 'terminal' | 'closed'\n\nconst BASE_BACKOFF_MS = 1_000\nconst MAX_BACKOFF_MS = 60_000\n// If no frame/ping arrives for this long, treat the socket as dead. The server\n// pings every 45s, so ~2 missed cycles.\nconst LIVENESS_MS = 100_000\n\nexport interface WsClientEvents {\n inbound: (row: SyncRow) => void\n ready: () => void\n terminal: (reason: string) => void\n}\n\nexport class AgentWsClient extends EventEmitter {\n private ws: WebSocket | null = null\n private state: State = 'closed'\n private attempt = 0\n private reconnectTimer: NodeJS.Timeout | null = null\n private livenessTimer: NodeJS.Timeout | null = null\n private stopped = false\n private ackMode = false\n\n constructor(\n private readonly url: string,\n private readonly apiKey: string,\n ) {\n super()\n }\n\n /** True only while the socket is live and ready. The heartbeat writer keys\n * off this, so a reconnecting/terminal daemon lets its heartbeat go stale\n * and the next session detects that always-on is actually down. */\n get connected(): boolean {\n return this.state === 'ready'\n }\n\n start(): void {\n this.stopped = false\n this.open()\n }\n\n stop(): void {\n this.stopped = true\n this.state = 'closed'\n this.clearTimers()\n if (this.ws) {\n try {\n this.ws.close(1000, 'daemon shutdown')\n } catch {\n /* already closed */\n }\n this.ws = null\n }\n }\n\n getState(): State {\n return this.state\n }\n\n /**\n * Confirm a message as handled: `{\"type\":\"ack\",\"message_id\":\"msg_...\"}`.\n * Fire-and-forget by design — a dropped ack is loss-free (the delivery\n * stays 'stored' and re-drains on the next reconnect, where dedup absorbs\n * the replay). Acking by message id (not delivery id) is what lets a\n * real-time push — which carries no delivery_id — be acked at all.\n */\n ack(messageId: string): void {\n if (this.state !== 'ready' || !this.ws) return\n try {\n this.ws.send(JSON.stringify({ type: 'ack', message_id: messageId }))\n } catch (err) {\n log.debug(`ack send failed for ${messageId} (will re-drain): ${String(err)}`)\n }\n }\n\n private open(): void {\n if (this.stopped) return\n this.state = this.attempt === 0 ? 'connecting' : 'reconnecting'\n log.info(`ws ${this.state} (attempt ${this.attempt + 1}) → ${this.url}`)\n\n const ws = new WebSocket(this.url, {\n headers: {\n authorization: `Bearer ${this.apiKey}`,\n // Opt into the delivery-ack protocol: the server then leaves each\n // delivery 'stored' until we ack it (by message id) instead of\n // marking it delivered the instant it hits the socket. A crash\n // mid-turn therefore re-drains on reconnect — at-least-once.\n 'x-agentchat-capabilities': 'ack',\n },\n })\n this.ws = ws\n\n ws.on('open', () => {\n this.attempt = 0\n this.state = 'ready'\n this.armLiveness()\n log.info('ws ready — draining + listening')\n this.emit('ready')\n })\n\n ws.on('message', (data) => {\n this.armLiveness()\n let frame: unknown\n try {\n frame = JSON.parse(data.toString())\n } catch {\n return // non-JSON frame — ignore\n }\n const f = frame as { type?: string; payload?: unknown; capabilities?: unknown }\n if (f?.type === 'message.new') {\n const row = parseInbound(f.payload)\n if (row) this.emit('inbound', row)\n else log.warn(`message.new payload failed to parse: ${JSON.stringify(f.payload).slice(0, 300)}`)\n } else if (f?.type === 'hello.ok') {\n const caps = Array.isArray(f.capabilities) ? (f.capabilities as string[]) : []\n this.ackMode = caps.includes('ack')\n log.info(`ws hello.ok — ack-mode ${this.ackMode ? 'ON' : 'OFF (legacy)'}`)\n } else {\n log.debug(`ws frame: ${f?.type}`)\n }\n // presence.update, typing.* etc. — not acted on here.\n })\n\n ws.on('ping', () => this.armLiveness()) // ws auto-pongs; just refresh liveness\n\n ws.on('unexpected-response', (_req, res) => {\n if (res.statusCode === 401 || res.statusCode === 403) {\n this.state = 'terminal'\n this.clearTimers()\n const reason = `auth rejected (${res.statusCode}) — check the agent's API key`\n log.error(`ws ${reason}`)\n this.emit('terminal', reason)\n return\n }\n log.warn(`ws unexpected response ${res.statusCode} — will reconnect`)\n })\n\n ws.on('error', (err) => {\n log.warn(`ws error: ${String(err)}`)\n // 'close' fires after 'error'; reconnect is scheduled there.\n })\n\n ws.on('close', (code) => {\n if (this.state === 'terminal' || this.stopped) return\n log.warn(`ws closed (${code}) — scheduling reconnect`)\n this.scheduleReconnect()\n })\n }\n\n private scheduleReconnect(): void {\n if (this.stopped || this.state === 'terminal') return\n this.state = 'reconnecting'\n this.clearTimers()\n const backoff = Math.min(BASE_BACKOFF_MS * 2 ** this.attempt, MAX_BACKOFF_MS)\n const jitter = backoff * (0.5 + Math.random() * 0.5) // 50–100% of backoff\n this.attempt++\n this.reconnectTimer = setTimeout(() => this.open(), jitter)\n }\n\n private armLiveness(): void {\n if (this.livenessTimer) clearTimeout(this.livenessTimer)\n this.livenessTimer = setTimeout(() => {\n log.warn('ws liveness timeout — forcing reconnect')\n try {\n this.ws?.terminate()\n } catch {\n /* ignore */\n }\n this.scheduleReconnect()\n }, LIVENESS_MS)\n }\n\n private clearTimers(): void {\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer)\n this.reconnectTimer = null\n }\n if (this.livenessTimer) {\n clearTimeout(this.livenessTimer)\n this.livenessTimer = null\n }\n }\n}\n","import { z } from 'zod'\nimport { log } from '../util/log.js'\n\n// ─── Wire shapes + HTTP fallback drain ──────────────────────────────────────\n//\n// The socket is ACK-CAPABLE (opted in via the `x-agentchat-capabilities: ack`\n// request header): the server leaves deliveries 'stored' until we ack, so a\n// crash mid-processing re-drains on reconnect (at-least-once). We ack over the\n// WS by MESSAGE id (`{\"type\":\"ack\",\"message_id\":\"msg_...\"}`) — the one field\n// present on BOTH real-time pushes and reconnect-drain frames. Real-time frames\n// carry NO delivery_id (that's a REST /sync concept), so the schema treats it\n// as optional. syncPeek/syncAck below are the belt-and-suspenders REST fallback\n// (that path always has delivery_id). Same bare-array / string-cursor wire the\n// coding-agents CLI uses (SDK still mis-types this path).\n\nexport interface WireConfig {\n apiKey: string\n apiBase: string\n timeoutMs?: number\n}\n\nconst SyncRowSchema = z\n .object({\n id: z.string(),\n conversation_id: z.string(),\n // Present on REST /sync + reconnect-drain rows; ABSENT on real-time pushes.\n delivery_id: z.string().nullish(),\n sender: z.string().optional(),\n sender_handle: z.string().optional(),\n type: z.string().optional(),\n content: z.record(z.unknown()).optional(),\n created_at: z.string().optional(),\n })\n .passthrough()\n\nexport type SyncRow = z.infer<typeof SyncRowSchema>\n\nasync function request(cfg: WireConfig, method: 'GET' | 'POST', pathname: string, body?: unknown): Promise<unknown> {\n const url = cfg.apiBase.replace(/\\/+$/, '') + pathname\n const res = await fetch(url, {\n method,\n headers: {\n authorization: `Bearer ${cfg.apiKey}`,\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n signal: AbortSignal.timeout(cfg.timeoutMs ?? 6000),\n })\n if (!res.ok) throw new Error(`AgentChat API ${res.status}: ${(await res.text().catch(() => '')).slice(0, 200)}`)\n return res.json()\n}\n\nexport function parseInbound(payload: unknown): SyncRow | null {\n const parsed = SyncRowSchema.safeParse(payload)\n return parsed.success ? parsed.data : null\n}\n\nexport function senderOf(row: SyncRow): string {\n return row.sender ?? row.sender_handle ?? 'unknown'\n}\n\n/** Platform-authored trusted context (server `message.context`) — resolved\n * sender identity, the conversation descriptor, and the parsed mention list.\n * Read defensively off the passthrough row; a message predating the server\n * enrichment yields all-null/empty and the caller degrades to bare handles. */\nexport interface MessageContext {\n senderDisplayName: string | null\n senderKind: 'agent' | 'system'\n groupName: string | null\n memberCount: number | null\n mentions: string[]\n}\n\nexport function contextOf(row: SyncRow): MessageContext {\n const raw = (row as { context?: unknown }).context\n const c = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n const sender = (c.sender && typeof c.sender === 'object' ? c.sender : {}) as Record<\n string,\n unknown\n >\n const conv = (c.conversation && typeof c.conversation === 'object'\n ? c.conversation\n : {}) as Record<string, unknown>\n return {\n senderDisplayName: typeof sender.display_name === 'string' ? sender.display_name : null,\n senderKind: sender.kind === 'system' ? 'system' : 'agent',\n groupName: typeof conv.group_name === 'string' ? conv.group_name : null,\n memberCount: typeof conv.member_count === 'number' ? conv.member_count : null,\n mentions: Array.isArray(c.mentions)\n ? c.mentions.filter((m): m is string => typeof m === 'string').map((m) => m.toLowerCase())\n : [],\n }\n}\n\n/** Commit deliveries at-or-before the cursor. Injection/handling = delivered. */\nexport async function syncAck(cfg: WireConfig, lastDeliveryId: string): Promise<number> {\n const data = await request(cfg, 'POST', '/v1/messages/sync/ack', { last_delivery_id: lastDeliveryId })\n const parsed = z.object({ acked: z.number() }).safeParse(data)\n return parsed.success ? parsed.data.acked : 0\n}\n\n/** Non-destructive peek — a fallback drain if the WS ever misses (belt-and-\n * suspenders; the WS already drains on connect). */\nexport async function syncPeek(cfg: WireConfig, after?: string): Promise<SyncRow[]> {\n const qs = after ? `?after=${encodeURIComponent(after)}&limit=200` : '?limit=200'\n const data = await request(cfg, 'GET', `/v1/messages/sync${qs}`)\n if (!Array.isArray(data)) {\n log.warn(`sync returned non-array (${typeof data})`)\n return []\n }\n const rows: SyncRow[] = []\n for (const item of data) {\n const p = SyncRowSchema.safeParse(item)\n if (p.success) rows.push(p.data)\n else break // never ack past an unparseable row\n }\n return rows\n}\n","import { log } from '../util/log.js'\n\n// ─── Reply-coordination client (/v1/reply) ───────────────────────────────────\n//\n// Lets this daemon agree with the agent's live coding session on ONE replier\n// per message, so a message is never answered twice when both are present.\n//\n// Design rule: EVERY call fails OPEN toward replying. A coordination outage\n// (Redis/API blip) must never make the daemon go silent — a missed reply is\n// worse than a rare double. So `claim` fails to TRUE (reply anyway) and\n// `isSessionActive` fails to FALSE (don't yield to a session we can't see).\n\nexport interface CoordConfig {\n apiKey: string\n apiBase: string\n /** Stable, replier-unique token, e.g. \"daemon:<host>\". Same token across a\n * restart on the same host so the daemon re-claims its own in-flight work. */\n holder: string\n timeoutMs?: number\n}\n\nexport class ReplyCoord {\n constructor(private readonly cfg: CoordConfig) {}\n\n private async req(method: 'GET' | 'POST', pathname: string, body?: unknown): Promise<unknown> {\n const url = this.cfg.apiBase.replace(/\\/+$/, '') + pathname\n const res = await fetch(url, {\n method,\n headers: {\n authorization: `Bearer ${this.cfg.apiKey}`,\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n signal: AbortSignal.timeout(this.cfg.timeoutMs ?? 5_000),\n })\n if (!res.ok) throw new Error(`reply-coord ${res.status}`)\n return res.json()\n }\n\n /** Is the agent's live coding session actively working? Fail-open → FALSE. */\n async isSessionActive(): Promise<boolean> {\n try {\n const d = (await this.req('GET', '/v1/reply/active')) as { active?: boolean }\n return d?.active === true\n } catch (err) {\n log.debug(`coord isSessionActive failed (assuming inactive): ${String(err)}`)\n return false\n }\n }\n\n /**\n * Claim the sole right to reply to a message. Returns true if THIS daemon is\n * the designated replier, false if a live session already owns it. Fail-open\n * → TRUE (reply anyway rather than drop).\n */\n async claim(messageId: string): Promise<boolean> {\n try {\n const d = (await this.req('POST', '/v1/reply/claim', {\n message_id: messageId,\n holder: this.cfg.holder,\n })) as { claimed?: boolean }\n return d?.claimed !== false\n } catch (err) {\n log.debug(`coord claim failed (proceeding): ${String(err)}`)\n return true\n }\n }\n}\n","import type { TurnContext } from './adapter-types.js'\n\n// Shared first-touch orientation fragments for the daemon adapters (claude +\n// codex render identical framing). Group labels keep the conversation id so the\n// agent can pass it straight to agentchat_get_conversation.\n\n/** \"the group \\\"Ops\\\" (grp_x)\", or a bare \"the group conversation grp_x\" when\n * the server supplied no name, or \"the direct conversation conv_x\". */\nexport function describeConversation(ctx: TurnContext): string {\n if (!ctx.conversationId.startsWith('grp_')) {\n return `the direct conversation ${ctx.conversationId}`\n }\n return ctx.groupName\n ? `the group \"${ctx.groupName}\" (${ctx.conversationId})`\n : `the group conversation ${ctx.conversationId}`\n}\n\n/** Resolved sender identity: \"Display Name (@handle)\" or \"@handle\", flagging a\n * system agent so the model weights its words as platform-authored. */\nexport function describeSender(ctx: TurnContext): string {\n const named = ctx.senderDisplayName\n ? `${ctx.senderDisplayName} (@${ctx.sender})`\n : `@${ctx.sender}`\n return ctx.senderKind === 'system' ? `${named}, a system agent` : named\n}\n","import * as path from 'node:path'\nimport * as fs from 'node:fs'\nimport { log } from '../util/log.js'\nimport { acquireLeaderLock } from './leader-lock.js'\nimport { resolveIdentity, credentialsPath } from '../identity/credentials.js'\nimport { resolveDaemonConfig } from './config.js'\nimport { Daemon } from './loop.js'\nimport { idle } from './health.js'\nimport type { RuntimeAdapter } from './adapter-types.js'\n\n// ─── The always-on supervisor ───────────────────────────────────────────────\n//\n// This process is RESIDENT. It is registered as a service when the integration\n// is installed, and from then on it simply exists — whether or not anyone has\n// signed in.\n//\n// That separation is the whole design, and getting it wrong was a real defect:\n// the service used to be created by `daemon install`, which refuses without\n// credentials. So the daemon's EXISTENCE was tied to the user's LOGIN STATE,\n// and three things followed. Installing the product did not give you always-on.\n// `logout` deleted the credentials but left the service, so the daemon threw\n// \"no identity\", exited 1, and KeepAlive restarted it — forever. And signing\n// back in restored nothing, because nothing re-created the service.\n//\n// Installation and authentication are different lifecycles. This is the shape\n// every comparable daemon uses (tailscaled is installed and running before\n// `tailscale up`; logging out idles it rather than uninstalling it).\n//\n// So:\n// no credentials → idle. No socket, no retries, no CPU. Just watch.\n// credentials → connect and serve.\n// credentials change (sign out, sign in, swap agents) → follow them.\n//\n// The only thing that removes this process is an explicit `daemon disable`.\n\n/** How often to re-read the identity. A `stat`; the watcher usually beats it. */\nconst POLL_MS = 5_000\n/** How often the watcher flag is consulted while waiting out a poll. */\nconst TICK_MS = 250\n/** Ceiling for retry backoff when the runtime simply is not usable yet. */\nconst MAX_BACKOFF_MS = 5 * 60_000\n\nexport interface RunDaemonOpts {\n /** THE identity home for the agent this daemon serves. */\n home: string\n /** How to spawn one headless turn of this integration's coding agent. */\n adapter: RuntimeAdapter\n /** Scratch dir override; defaults to `<home>/daemon-workdir`. */\n workdir?: string\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms))\n\n/**\n * Identity fingerprint: changes when the user signs out, signs in, or swaps to\n * a different agent. Comparing it is how the supervisor notices without caring\n * why.\n */\nfunction fingerprint(home: string): string | null {\n const id = resolveIdentity(home)\n return id === null ? null : `${id.apiKey}:${id.handle ?? ''}`\n}\n\n/**\n * Run the always-on daemon. Returns only on a condition that makes running\n * pointless — namely another daemon already holding this home's lock.\n */\nexport async function runDaemon(opts: RunDaemonOpts): Promise<number> {\n const home = path.resolve(opts.home)\n const workdir = opts.workdir ?? path.join(home, 'daemon-workdir')\n\n // Hooks default to `warn` because they run on every session start and must\n // stay silent. A resident service is the opposite case: its output goes to a\n // log file nobody sees until something is wrong, and an empty log is useless\n // for answering \"is always-on actually working?\". Still overridable.\n if (process.env['AGENTCHAT_LOG_LEVEL'] === undefined) process.env['AGENTCHAT_LOG_LEVEL'] = 'info'\n\n // Taken for the PROCESS, not for a connection: one resident daemon per\n // identity home, signed in or not.\n const lock = acquireLeaderLock(home)\n if (lock === null) return 1\n\n let live: Daemon | null = null\n let liveFingerprint: string | null = null\n /** A credential the server refused. Sit out until it CHANGES. */\n let refused: string | null = null\n /** Consecutive connect failures, for backoff. Reset on success or on a new\n * credential — a fresh sign-in always deserves a fast first attempt. */\n let failures = 0\n /** Last failure message, so a persistent condition is logged once. */\n let lastFailure: string | null = null\n let shuttingDown = false\n\n const disconnect = (why: string): void => {\n if (live === null) return\n log.info(`${why} — disconnecting, staying resident`)\n live.stop()\n live = null\n liveFingerprint = null\n idle(home)\n }\n\n const shutdown = (sig: string): void => {\n if (shuttingDown) return\n shuttingDown = true\n log.info(`${sig} — shutting down`)\n live?.stop()\n idle(home)\n lock.release()\n process.exit(0)\n }\n process.on('SIGINT', () => shutdown('SIGINT'))\n process.on('SIGTERM', () => shutdown('SIGTERM'))\n\n // Best-effort accelerator so signing in connects in about a second instead of\n // waiting out a poll. fs.watch is unreliable on some filesystems and network\n // mounts, so the poll below is the real guarantee and this is pure upside.\n let nudged = false\n try {\n fs.mkdirSync(home, { recursive: true })\n const watcher = fs.watch(home, (_event, filename) => {\n if (filename === null || String(filename).startsWith('credentials')) nudged = true\n })\n watcher.unref()\n } catch {\n /* polling covers it */\n }\n\n log.info(`always-on resident for ${home} (${credentialsPath(home)})`)\n idle(home)\n\n for (;;) {\n if (shuttingDown) break\n const fp = fingerprint(home)\n\n if (fp === null) {\n // Signed out, or never signed in. Idle — and forget any refusal, since\n // the next credential to appear deserves a fresh attempt.\n disconnect('signed out')\n if (refused !== null) refused = null\n } else if (fp !== liveFingerprint) {\n // A credential appeared, or changed underneath us. Any backoff from a\n // previous credential is irrelevant to this one.\n disconnect('identity changed')\n failures = 0\n if (fp === refused) {\n // Same key the server already rejected; wait for a different one\n // rather than hammering the endpoint.\n } else {\n try {\n const cfg = await resolveDaemonConfig({ home, workdir })\n const candidate = new Daemon(cfg, opts.adapter, undefined, (reason) => {\n // Auth refused: stop trying THIS credential, keep the process.\n log.warn(`credential refused (${reason}) — idling until it changes`)\n refused = fp\n live = null\n liveFingerprint = null\n idle(home)\n })\n await candidate.start()\n live = candidate\n liveFingerprint = fp\n failures = 0\n lastFailure = null\n } catch (err) {\n // Runtime not ready (host CLI missing or not logged in), network\n // down, whatever. Stay resident and try again later — exiting would\n // just make the service manager restart us in a loop.\n //\n // Backed off and de-duplicated: \"codex CLI not found on PATH\" is a\n // condition that can last days, and retrying every 5s would write\n // ~17k identical lines a day into a log meant to be readable.\n const msg = String(err instanceof Error ? err.message : err)\n if (msg !== lastFailure) {\n log.warn(`not connecting yet: ${msg}`)\n lastFailure = msg\n }\n failures += 1\n live = null\n liveFingerprint = null\n idle(home)\n }\n }\n }\n\n // Wait out the poll interval, but wake as soon as the watcher fires so a\n // sign-in connects in well under a second instead of up to POLL_MS. The\n // flag has to be checked DURING the wait — an earlier version only\n // consulted it afterwards, which made the watcher useless.\n // Exponential backoff while a connect keeps failing, capped — but the\n // watcher still wakes us instantly when credentials change, so backing off\n // never delays a real sign-in.\n const waitMs = failures === 0 ? POLL_MS : Math.min(POLL_MS * 2 ** Math.min(failures, 6), MAX_BACKOFF_MS)\n nudged = false\n const deadline = Date.now() + waitMs\n while (!nudged && !shuttingDown && Date.now() < deadline) {\n await sleep(TICK_MS)\n }\n }\n\n lock.release()\n return 0\n}\n","import * as path from 'node:path'\nimport { resolveIdentity } from '../identity/credentials.js'\nimport { getMeLite } from '../wire/index.js'\n\n// ─── Daemon identity resolution ─────────────────────────────────────────────\n//\n// The daemon runs AS one host agent — the same identity that agent's in-session\n// hooks use, never a separate account. It reads that credential from the home\n// it is GIVEN.\n//\n// The predecessor of this file mapped a `runtime` enum to a home\n// (`codex → ~/.codex/agentchat`, `claude-code → ~/.claude/agentchat`). That\n// mapping is exactly the \"a function that decides can decide wrong\" defect this\n// package exists to make unrepresentable, so it is gone: the caller passes its\n// own home and there is no enum to mis-set.\n\nexport interface DaemonConfig {\n apiKey: string\n handle: string\n apiBase: string\n wsUrl: string\n /** The identity home. Credentials, leader lock, and heartbeat all live here. */\n home: string\n /** Scratch dir for the adapter (spawned-turn cwd, generated MCP config). */\n workdir: string\n}\n\n/** `https://api.agentchat.me` → `wss://api.agentchat.me/v1/ws`. */\nexport function wsUrlFor(apiBase: string): string {\n return apiBase.replace(/^http/, 'ws').replace(/\\/+$/, '') + '/v1/ws'\n}\n\nexport interface ResolveDaemonOpts {\n /** THE identity home. Required — this module never derives one. */\n home: string\n workdir?: string\n}\n\n/**\n * Resolve the identity the daemon runs as.\n *\n * Async because the handle is load-bearing at runtime — it filters this agent's\n * own outbound echoed back by server fan-out, and decides whether a group\n * mention names it. An env-only identity (`AGENTCHAT_API_KEY`, no credentials\n * file) has no handle on disk, so we ask the server rather than starting up\n * blind and replying to ourselves.\n */\nexport async function resolveDaemonConfig(opts: ResolveDaemonOpts): Promise<DaemonConfig> {\n const home = path.resolve(opts.home)\n const id = resolveIdentity(home)\n if (id === null) {\n throw new Error(`no AgentChat identity in ${home} — register this agent first`)\n }\n\n let handle = id.handle\n if (handle === null) {\n const me = await getMeLite({ apiKey: id.apiKey, apiBase: id.apiBase })\n if (me === null) {\n throw new Error(\n 'could not determine this agent’s handle (no credentials file, and /v1/agents/me did not answer)',\n )\n }\n handle = me.handle\n }\n\n return {\n apiKey: id.apiKey,\n handle,\n apiBase: id.apiBase,\n wsUrl: wsUrlFor(id.apiBase),\n home,\n workdir: opts.workdir ?? path.join(home, 'daemon-workdir'),\n }\n}\n","import * as os from 'node:os'\nimport { log } from '../util/log.js'\nimport type { DaemonConfig } from './config.js'\nimport { AgentWsClient } from './ws-client.js'\nimport { ReplyCoord } from './coord.js'\nimport { beat } from './health.js'\nimport { contextOf, senderOf, type SyncRow } from './frames.js'\nimport type { RuntimeAdapter } from './adapter-types.js'\n\n// ─── The core loop ──────────────────────────────────────────────────────────\n//\n// WS pushes message.new → dedup → coexistence check (yield to a live session,\n// then claim the sole right to reply) → (per-conversation serialized, globally\n// capped) run one runtime turn → ack on success. Not acking on failure means\n// the server re-drains the message on the next reconnect (at-least-once); a\n// per-message attempt cap drops poison after N tries so it can't loop forever.\n//\n// Host-agnostic by construction: everything it knows about the agent arrives in\n// `DaemonConfig`, and everything it knows about the coding agent arrives as a\n// `RuntimeAdapter`. It cannot name a host, so it cannot act on the wrong one.\n\nconst MAX_CONCURRENT_TURNS = 3\nconst MAX_ATTEMPTS = 3\nconst HEARTBEAT_MS = 30_000\n// When the agent's live coding session is actively working, wait this long\n// before claiming — a head start so the human-driven session (priority) can\n// grab the message first. Only applies while a session is active; the common\n// \"no session, daemon only\" path has zero added latency. Tunable for testing.\nconst YIELD_MS = Number(process.env['AGENTCHATD_YIELD_MS'] ?? 10_000)\n\nconst delay = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms))\n\nexport class Daemon {\n private readonly ws: AgentWsClient\n private readonly coord: ReplyCoord\n private readonly seen = new Map<string, number>() // message id → attempts\n private readonly convChains = new Map<string, Promise<void>>()\n private inFlight = 0\n private readonly waiters: Array<() => void> = []\n private stopping = false\n private heartbeatTimer: NodeJS.Timeout | null = null\n\n constructor(\n private readonly cfg: DaemonConfig,\n private readonly adapter: RuntimeAdapter,\n ws?: AgentWsClient, // injectable for tests; defaults to a real socket\n /** Called when the socket gives up for good (auth refused). The supervisor\n * above decides what happens next — this class does not end the process. */\n private readonly onTerminal?: (reason: string) => void,\n ) {\n // Stable holder token: the same across a restart on THIS host, so a\n // restarted daemon re-claims its own in-flight messages instead of being\n // locked out by its own prior claim. (Two daemons per agent on one host\n // are already prevented by the leader lock.)\n this.coord = new ReplyCoord({\n apiKey: cfg.apiKey,\n apiBase: cfg.apiBase,\n holder: `daemon:${os.hostname()}`,\n })\n this.ws = ws ?? new AgentWsClient(cfg.wsUrl, cfg.apiKey)\n this.ws.on('inbound', (row: SyncRow) => this.onInbound(row))\n // Every fresh connection stamps the beacon immediately (don't wait up to 30s\n // for the first interval tick to prove we're live).\n this.ws.on('ready', () => beat(this.cfg.home))\n this.ws.on('terminal', (reason: string) => {\n // Auth refused. Do NOT end the process: this daemon is resident, and a\n // rejected key is a state to sit out, not to die from — the user may sign\n // in again with a good one. Setting process.exitCode here made the\n // service manager restart the whole thing on a loop instead.\n log.error(`daemon terminal: ${reason}`)\n this.stop()\n this.onTerminal?.(reason)\n })\n }\n\n async start(): Promise<void> {\n const pre = await this.adapter.preflight()\n if (!pre.ok) {\n throw new Error(`runtime (${this.adapter.name}) not ready: ${pre.detail}`)\n }\n log.info(`agentchat daemon up as @${this.cfg.handle} via ${this.adapter.name}; holding the wire`)\n this.ws.start()\n // Keep the beacon fresh while connected. unref so it never by itself keeps\n // the process alive.\n this.heartbeatTimer = setInterval(() => {\n if (this.ws.connected) beat(this.cfg.home)\n }, HEARTBEAT_MS)\n this.heartbeatTimer.unref()\n }\n\n stop(): void {\n this.stopping = true\n if (this.heartbeatTimer) clearInterval(this.heartbeatTimer)\n this.ws.stop()\n }\n\n private onInbound(row: SyncRow): void {\n // Ignore our own outbound echoed back by server fan-out.\n if (senderOf(row) === this.cfg.handle) return\n if (this.seen.has(row.id)) return // dedup (reconnect replay)\n this.seen.set(row.id, 0)\n this.enqueue(row)\n }\n\n /** Serialize turns within a conversation; the global semaphore caps total. */\n private enqueue(row: SyncRow): void {\n const prev = this.convChains.get(row.conversation_id) ?? Promise.resolve()\n const next = prev\n .then(() => this.handle(row))\n .catch((err) => {\n log.warn(`unhandled in conv ${row.conversation_id}: ${String(err)}`)\n })\n this.convChains.set(row.conversation_id, next)\n // Prune the chain entry once it settles (avoid unbounded map growth).\n void next.then(() => {\n if (this.convChains.get(row.conversation_id) === next) this.convChains.delete(row.conversation_id)\n })\n }\n\n private async handle(row: SyncRow): Promise<void> {\n if (this.stopping) return\n\n // ── Coexistence: agree on exactly one replier ──\n // If the agent's live coding session is actively working, yield briefly so\n // its hook can claim + handle this first (the human-driven session has\n // priority). Then claim the sole right to reply; whoever wins is it.\n if (await this.coord.isSessionActive()) {\n log.info(`msg ${row.id}: live session active — yielding for ${YIELD_MS}ms`)\n await delay(YIELD_MS)\n if (this.stopping) return\n }\n if (!(await this.coord.claim(row.id))) {\n // A live session owns this one. Do NOT ack — leave it 'stored' so the\n // session's sync-peek still sees it and marks it delivered on handling.\n log.info(`msg ${row.id}: claimed by the live session — standing down`)\n return\n }\n\n await this.acquireSlot()\n try {\n const attempts = (this.seen.get(row.id) ?? 0) + 1\n this.seen.set(row.id, attempts)\n log.info(`turn for msg ${row.id} from @${senderOf(row)} (attempt ${attempts})`)\n\n const ctx = contextOf(row)\n const result = await this.adapter.runTurn({\n conversationId: row.conversation_id,\n sender: senderOf(row),\n text: typeof row.content?.['text'] === 'string' ? (row.content['text'] as string) : '',\n createdAt: typeof row.created_at === 'string' ? row.created_at : undefined,\n type: typeof row.type === 'string' ? row.type : undefined,\n senderDisplayName: ctx.senderDisplayName,\n senderKind: ctx.senderKind,\n groupName: ctx.groupName,\n mentioned: ctx.mentions.includes(this.cfg.handle.toLowerCase()),\n })\n\n if (result.ok) {\n this.ws.ack(row.id)\n } else if (result.fatal) {\n log.error(`fatal turn error: ${result.detail} — not acking (will re-drain)`)\n } else if (attempts >= MAX_ATTEMPTS) {\n log.warn(`msg ${row.id} failed ${attempts}× (${result.detail}); acking to drop (poison guard)`)\n this.ws.ack(row.id)\n } else {\n log.warn(`turn failed for ${row.id}: ${result.detail}; leaving unacked for re-drain`)\n }\n } finally {\n this.releaseSlot()\n }\n }\n\n // ─── global concurrency semaphore ─────────────────────────────────────────\n // A waiter inherits the releaser's slot directly (inFlight unchanged on\n // hand-off) — no decrement-then-reincrement window that could momentarily\n // exceed the cap.\n private acquireSlot(): Promise<void> {\n if (this.inFlight < MAX_CONCURRENT_TURNS) {\n this.inFlight++\n return Promise.resolve()\n }\n return new Promise<void>((resolve) => this.waiters.push(resolve))\n }\n\n private releaseSlot(): void {\n const next = this.waiters.shift()\n if (next) next() // pass the slot on; inFlight stays at the cap\n else this.inFlight--\n }\n}\n"],"mappings":"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","names":["path","resolve","MAX_BACKOFF_MS"]}
|
|
1
|
+
{"version":3,"sources":["../src/daemon/ws-client.ts","../src/daemon/frames.ts","../src/daemon/coord.ts","../src/daemon/format.ts","../src/daemon/run.ts","../src/daemon/config.ts","../src/daemon/loop.ts"],"sourcesContent":["import { WebSocket } from 'ws'\nimport { EventEmitter } from 'node:events'\nimport { log } from '../util/log.js'\nimport { parseInbound, type SyncRow } from './frames.js'\nimport { CODING_AGENTS_CLIENT_HEADERS } from '../client-identity.js'\n\n// ─── Agent WebSocket client ─────────────────────────────────────────────────\n//\n// Connects to /v1/ws as the agent (Bearer auth). The server drains undelivered\n// as `message.new` frames on connect AND pushes them in real time. The `ws`\n// library auto-pongs the server's heartbeat pings, which keeps presence alive.\n// We add reconnect with exponential backoff + jitter, a liveness watchdog, and\n// a terminal state for auth failure so a bad key doesn't reconnect forever.\n\ntype State = 'connecting' | 'ready' | 'reconnecting' | 'terminal' | 'closed'\n\nconst BASE_BACKOFF_MS = 1_000\nconst MAX_BACKOFF_MS = 60_000\nconst ACK_RETRY_MS = 1_000\n// If no frame/ping arrives for this long, treat the socket as dead. The server\n// pings every 45s, so ~2 missed cycles.\nconst LIVENESS_MS = 100_000\n\nexport interface WsClientEvents {\n inbound: (row: SyncRow) => void\n ready: () => void\n terminal: (reason: string) => void\n}\n\nexport class AgentWsClient extends EventEmitter {\n private ws: WebSocket | null = null\n private state: State = 'closed'\n private attempt = 0\n private reconnectTimer: NodeJS.Timeout | null = null\n private livenessTimer: NodeJS.Timeout | null = null\n private ackRetryTimer: NodeJS.Timeout | null = null\n private stopped = false\n private ackMode = false\n private inboundPaused = false\n private readonly pendingAcks = new Set<string>()\n private readonly acksInFlight = new Set<string>()\n\n constructor(\n private readonly url: string,\n private readonly apiKey: string,\n ) {\n super()\n }\n\n /** True only while the socket is live and ready. The heartbeat writer keys\n * off this, so a reconnecting/terminal daemon lets its heartbeat go stale\n * and the next session detects that always-on is actually down. */\n get connected(): boolean {\n return this.state === 'ready'\n }\n\n start(): void {\n this.stopped = false\n this.open()\n }\n\n stop(): void {\n this.stopped = true\n this.state = 'closed'\n this.clearTimers()\n this.acksInFlight.clear()\n if (this.ws) {\n try {\n this.ws.close(1000, 'daemon shutdown')\n } catch {\n /* already closed */\n }\n this.ws = null\n }\n }\n\n /**\n * Apply TCP backpressure while the model-turn queue is saturated. `ws`\n * delegates this to the underlying socket; no delivered frame is discarded.\n * A server heartbeat may close a very long pause, which is safe because all\n * unacked messages re-drain after reconnect.\n */\n pauseInbound(): void {\n if (this.inboundPaused) return\n this.inboundPaused = true\n try {\n this.ws?.pause()\n } catch {\n /* reconnect drain remains the fallback */\n }\n }\n\n resumeInbound(): void {\n if (!this.inboundPaused) return\n this.inboundPaused = false\n try {\n this.ws?.resume()\n if (this.state === 'ready') this.armLiveness()\n } catch {\n /* reconnect drain remains the fallback */\n }\n }\n\n getState(): State {\n return this.state\n }\n\n /**\n * Confirm a message as handled: `{\"type\":\"ack\",\"message_id\":\"msg_...\"}`.\n * Fire-and-forget by design — a dropped ack is loss-free (the delivery\n * stays 'stored' and re-drains on the next reconnect, where dedup absorbs\n * the replay). Acking by message id (not delivery id) is what lets a\n * real-time push — which carries no delivery_id — be acked at all.\n */\n ack(messageId: string): void {\n this.pendingAcks.add(messageId)\n this.flushAcks()\n }\n\n private flushAcks(): void {\n if (this.state !== 'ready' || !this.ws) return\n for (const messageId of this.pendingAcks) {\n if (this.acksInFlight.has(messageId)) continue\n this.acksInFlight.add(messageId)\n try {\n this.ws.send(\n JSON.stringify({ type: 'ack', message_id: messageId }),\n (err?: Error) => {\n this.acksInFlight.delete(messageId)\n if (!err) this.pendingAcks.delete(messageId)\n else {\n log.debug(`ack send failed for ${messageId} (will retry): ${String(err)}`)\n this.scheduleAckRetry()\n }\n },\n )\n } catch (err) {\n this.acksInFlight.delete(messageId)\n log.debug(`ack send failed for ${messageId} (will retry): ${String(err)}`)\n this.scheduleAckRetry()\n }\n }\n }\n\n private scheduleAckRetry(): void {\n if (this.stopped || this.ackRetryTimer) return\n this.ackRetryTimer = setTimeout(() => {\n this.ackRetryTimer = null\n this.flushAcks()\n }, ACK_RETRY_MS)\n this.ackRetryTimer.unref()\n }\n\n private open(): void {\n if (this.stopped) return\n this.state = this.attempt === 0 ? 'connecting' : 'reconnecting'\n log.info(`ws ${this.state} (attempt ${this.attempt + 1}) → ${this.url}`)\n\n const ws = new WebSocket(this.url, {\n headers: {\n ...CODING_AGENTS_CLIENT_HEADERS,\n authorization: `Bearer ${this.apiKey}`,\n // Opt into the delivery-ack protocol: the server then leaves each\n // delivery 'stored' until we ack it (by message id) instead of\n // marking it delivered the instant it hits the socket. A crash\n // mid-turn therefore re-drains on reconnect — at-least-once.\n 'x-agentchat-capabilities': 'ack',\n },\n })\n this.ws = ws\n\n ws.on('open', () => {\n this.attempt = 0\n this.state = 'ready'\n this.armLiveness()\n if (this.inboundPaused) ws.pause()\n log.info('ws ready — draining + listening')\n this.emit('ready')\n this.flushAcks()\n })\n\n ws.on('message', (data) => {\n this.armLiveness()\n let frame: unknown\n try {\n frame = JSON.parse(data.toString())\n } catch {\n return // non-JSON frame — ignore\n }\n const f = frame as { type?: string; payload?: unknown; capabilities?: unknown }\n if (f?.type === 'message.new') {\n const row = parseInbound(f.payload)\n if (row) this.emit('inbound', row)\n else log.warn(`message.new payload failed to parse: ${JSON.stringify(f.payload).slice(0, 300)}`)\n } else if (f?.type === 'hello.ok') {\n const caps = Array.isArray(f.capabilities) ? (f.capabilities as string[]) : []\n this.ackMode = caps.includes('ack')\n log.info(`ws hello.ok — ack-mode ${this.ackMode ? 'ON' : 'OFF (legacy)'}`)\n } else {\n log.debug(`ws frame: ${f?.type}`)\n }\n // presence.update, typing.* etc. — not acted on here.\n })\n\n ws.on('ping', () => this.armLiveness()) // ws auto-pongs; just refresh liveness\n\n ws.on('unexpected-response', (_req, res) => {\n if (this.stopped) return\n if (res.statusCode === 401 || res.statusCode === 403) {\n this.state = 'terminal'\n this.clearTimers()\n const reason = `auth rejected (${res.statusCode}) — check the agent's API key`\n log.error(`ws ${reason}`)\n this.emit('terminal', reason)\n return\n }\n log.warn(`ws unexpected response ${res.statusCode} — will reconnect`)\n })\n\n ws.on('error', (err) => {\n log.warn(`ws error: ${String(err)}`)\n // 'close' fires after 'error'; reconnect is scheduled there.\n })\n\n ws.on('close', (code) => {\n if (this.state === 'terminal' || this.stopped) return\n this.acksInFlight.clear()\n log.warn(`ws closed (${code}) — scheduling reconnect`)\n this.scheduleReconnect()\n })\n }\n\n private scheduleReconnect(): void {\n if (this.stopped || this.state === 'terminal') return\n // A liveness timeout terminates the socket and may race its `close` event.\n // Both paths request a reconnect; one timer is enough.\n if (this.reconnectTimer) return\n this.state = 'reconnecting'\n this.clearTimers()\n const backoff = Math.min(BASE_BACKOFF_MS * 2 ** this.attempt, MAX_BACKOFF_MS)\n const jitter = backoff * (0.5 + Math.random() * 0.5) // 50–100% of backoff\n this.attempt++\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null\n this.open()\n }, jitter)\n }\n\n private armLiveness(): void {\n if (this.livenessTimer) clearTimeout(this.livenessTimer)\n this.livenessTimer = setTimeout(() => {\n log.warn('ws liveness timeout — forcing reconnect')\n try {\n this.ws?.terminate()\n } catch {\n /* ignore */\n }\n this.scheduleReconnect()\n }, LIVENESS_MS)\n }\n\n private clearTimers(): void {\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer)\n this.reconnectTimer = null\n }\n if (this.livenessTimer) {\n clearTimeout(this.livenessTimer)\n this.livenessTimer = null\n }\n if (this.ackRetryTimer) {\n clearTimeout(this.ackRetryTimer)\n this.ackRetryTimer = null\n }\n }\n}\n","import { z } from 'zod'\nimport { log } from '../util/log.js'\nimport { CODING_AGENTS_CLIENT_HEADERS } from '../client-identity.js'\n\n// ─── Wire shapes + HTTP fallback drain ──────────────────────────────────────\n//\n// The socket is ACK-CAPABLE (opted in via the `x-agentchat-capabilities: ack`\n// request header): the server leaves deliveries 'stored' until we ack, so a\n// crash mid-processing re-drains on reconnect (at-least-once). We ack over the\n// WS by MESSAGE id (`{\"type\":\"ack\",\"message_id\":\"msg_...\"}`) — the one field\n// present on BOTH real-time pushes and reconnect-drain frames. Real-time frames\n// carry NO delivery_id (that's a REST /sync concept), so the schema treats it\n// as optional. syncPeek/syncAck below are the belt-and-suspenders REST fallback\n// (that path always has delivery_id). Same bare-array / string-cursor wire the\n// coding-agents CLI uses (SDK still mis-types this path).\n\nexport interface WireConfig {\n apiKey: string\n apiBase: string\n timeoutMs?: number\n}\n\nconst SyncRowSchema = z\n .object({\n id: z.string(),\n conversation_id: z.string(),\n // Present on REST /sync + reconnect-drain rows; ABSENT on real-time pushes.\n delivery_id: z.string().nullish(),\n sender: z.string().optional(),\n sender_handle: z.string().optional(),\n type: z.string().optional(),\n content: z.record(z.unknown()).optional(),\n created_at: z.string().optional(),\n })\n .passthrough()\n\nexport type SyncRow = z.infer<typeof SyncRowSchema>\n\nasync function request(cfg: WireConfig, method: 'GET' | 'POST', pathname: string, body?: unknown): Promise<unknown> {\n const url = cfg.apiBase.replace(/\\/+$/, '') + pathname\n const res = await fetch(url, {\n method,\n headers: {\n ...CODING_AGENTS_CLIENT_HEADERS,\n authorization: `Bearer ${cfg.apiKey}`,\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n signal: AbortSignal.timeout(cfg.timeoutMs ?? 6000),\n })\n if (!res.ok) throw new Error(`AgentChat API ${res.status}: ${(await res.text().catch(() => '')).slice(0, 200)}`)\n return res.json()\n}\n\nexport function parseInbound(payload: unknown): SyncRow | null {\n const parsed = SyncRowSchema.safeParse(payload)\n return parsed.success ? parsed.data : null\n}\n\nexport function senderOf(row: SyncRow): string {\n return row.sender ?? row.sender_handle ?? 'unknown'\n}\n\n/** Platform-authored trusted context (server `message.context`) — resolved\n * sender identity, the conversation descriptor, and the parsed mention list.\n * Read defensively off the passthrough row; a message predating the server\n * enrichment yields all-null/empty and the caller degrades to bare handles. */\nexport interface MessageContext {\n senderDisplayName: string | null\n senderKind: 'agent' | 'system'\n groupName: string | null\n memberCount: number | null\n mentions: string[]\n}\n\nexport function contextOf(row: SyncRow): MessageContext {\n const raw = (row as { context?: unknown }).context\n const c = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n const sender = (c.sender && typeof c.sender === 'object' ? c.sender : {}) as Record<\n string,\n unknown\n >\n const conv = (c.conversation && typeof c.conversation === 'object'\n ? c.conversation\n : {}) as Record<string, unknown>\n return {\n senderDisplayName: typeof sender.display_name === 'string' ? sender.display_name : null,\n senderKind: sender.kind === 'system' ? 'system' : 'agent',\n groupName: typeof conv.group_name === 'string' ? conv.group_name : null,\n memberCount: typeof conv.member_count === 'number' ? conv.member_count : null,\n mentions: Array.isArray(c.mentions)\n ? c.mentions.filter((m): m is string => typeof m === 'string').map((m) => m.toLowerCase())\n : [],\n }\n}\n\n/** Commit deliveries at-or-before the cursor. Injection/handling = delivered. */\nexport async function syncAck(cfg: WireConfig, lastDeliveryId: string): Promise<number> {\n const data = await request(cfg, 'POST', '/v1/messages/sync/ack', { last_delivery_id: lastDeliveryId })\n const parsed = z.object({ acked: z.number() }).safeParse(data)\n return parsed.success ? parsed.data.acked : 0\n}\n\n/** Non-destructive peek — a fallback drain if the WS ever misses (belt-and-\n * suspenders; the WS already drains on connect). */\nexport async function syncPeek(cfg: WireConfig, after?: string): Promise<SyncRow[]> {\n const qs = after ? `?after=${encodeURIComponent(after)}&limit=200` : '?limit=200'\n const data = await request(cfg, 'GET', `/v1/messages/sync${qs}`)\n if (!Array.isArray(data)) {\n log.warn(`sync returned non-array (${typeof data})`)\n return []\n }\n const rows: SyncRow[] = []\n for (const item of data) {\n const p = SyncRowSchema.safeParse(item)\n if (p.success) rows.push(p.data)\n else break // never ack past an unparseable row\n }\n return rows\n}\n","import { log } from '../util/log.js'\nimport { CODING_AGENTS_CLIENT_HEADERS } from '../client-identity.js'\n\n// ─── Reply-coordination client (/v1/reply) ───────────────────────────────────\n//\n// Lets this daemon agree with the agent's live coding session on ONE replier\n// per message, so a message is never answered twice when both are present.\n//\n// Design rule: EVERY call fails OPEN toward replying. A coordination outage\n// (Redis/API blip) must never make the daemon go silent — a missed reply is\n// worse than a rare double. So `claim` fails to TRUE (reply anyway) and\n// `isSessionActive` fails to FALSE (don't yield to a session we can't see).\n\nexport interface CoordConfig {\n apiKey: string\n apiBase: string\n /** Stable, replier-unique token, e.g. \"daemon:<host>\". Same token across a\n * restart on the same host so the daemon re-claims its own in-flight work. */\n holder: string\n timeoutMs?: number\n}\n\nexport class ReplyCoord {\n constructor(private readonly cfg: CoordConfig) {}\n\n private async req(method: 'GET' | 'POST', pathname: string, body?: unknown): Promise<unknown> {\n const url = this.cfg.apiBase.replace(/\\/+$/, '') + pathname\n const res = await fetch(url, {\n method,\n headers: {\n ...CODING_AGENTS_CLIENT_HEADERS,\n authorization: `Bearer ${this.cfg.apiKey}`,\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n signal: AbortSignal.timeout(this.cfg.timeoutMs ?? 5_000),\n })\n if (!res.ok) throw new Error(`reply-coord ${res.status}`)\n return res.json()\n }\n\n /** Is the agent's live coding session actively working? Fail-open → FALSE. */\n async isSessionActive(): Promise<boolean> {\n try {\n const d = (await this.req('GET', '/v1/reply/active')) as { active?: boolean }\n return d?.active === true\n } catch (err) {\n log.debug(`coord isSessionActive failed (assuming inactive): ${String(err)}`)\n return false\n }\n }\n\n /**\n * Claim the sole right to reply to a message. Returns true if THIS daemon is\n * the designated replier, false if a live session already owns it. Fail-open\n * → TRUE (reply anyway rather than drop).\n */\n async claim(messageId: string): Promise<boolean> {\n try {\n const d = (await this.req('POST', '/v1/reply/claim', {\n message_id: messageId,\n holder: this.cfg.holder,\n })) as { claimed?: boolean }\n return d?.claimed !== false\n } catch (err) {\n log.debug(`coord claim failed (proceeding): ${String(err)}`)\n return true\n }\n }\n}\n","import type { TurnContext } from './adapter-types.js'\n\n// Shared first-touch orientation fragments for the daemon adapters (claude +\n// codex render identical framing). Group labels keep the conversation id so the\n// agent can pass it straight to agentchat_get_conversation.\n\n/** \"the group \\\"Ops\\\" (grp_x)\", or a bare \"the group conversation grp_x\" when\n * the server supplied no name, or \"the direct conversation conv_x\". */\nexport function describeConversation(ctx: TurnContext): string {\n if (!ctx.conversationId.startsWith('grp_')) {\n return `the direct conversation ${ctx.conversationId}`\n }\n return ctx.groupName\n ? `the group \"${ctx.groupName}\" (${ctx.conversationId})`\n : `the group conversation ${ctx.conversationId}`\n}\n\n/** Resolved sender identity: \"Display Name (@handle)\" or \"@handle\", flagging a\n * system agent so the model weights its words as platform-authored. */\nexport function describeSender(ctx: TurnContext): string {\n const named = ctx.senderDisplayName\n ? `${ctx.senderDisplayName} (@${ctx.sender})`\n : `@${ctx.sender}`\n return ctx.senderKind === 'system' ? `${named}, a system agent` : named\n}\n","import * as path from 'node:path'\nimport * as fs from 'node:fs'\nimport { log } from '../util/log.js'\nimport { acquireLeaderLock } from './leader-lock.js'\nimport { resolveIdentity, credentialsPath } from '../identity/credentials.js'\nimport { resolveDaemonConfig } from './config.js'\nimport { Daemon } from './loop.js'\nimport { idle } from './health.js'\nimport type { RuntimeAdapter } from './adapter-types.js'\n\n// ─── The always-on supervisor ───────────────────────────────────────────────\n//\n// This process is RESIDENT. It is registered as a service when the integration\n// is installed, and from then on it simply exists — whether or not anyone has\n// signed in.\n//\n// That separation is the whole design, and getting it wrong was a real defect:\n// the service used to be created by `daemon install`, which refuses without\n// credentials. So the daemon's EXISTENCE was tied to the user's LOGIN STATE,\n// and three things followed. Installing the product did not give you always-on.\n// `logout` deleted the credentials but left the service, so the daemon threw\n// \"no identity\", exited 1, and KeepAlive restarted it — forever. And signing\n// back in restored nothing, because nothing re-created the service.\n//\n// Installation and authentication are different lifecycles. This is the shape\n// every comparable daemon uses (tailscaled is installed and running before\n// `tailscale up`; logging out idles it rather than uninstalling it).\n//\n// So:\n// no credentials → idle. No socket, no retries, no CPU. Just watch.\n// credentials → connect and serve.\n// credentials change (sign out, sign in, swap agents) → follow them.\n//\n// The only thing that removes this process is an explicit `daemon disable`.\n\n/** How often to re-read the identity. A `stat`; the watcher usually beats it. */\nconst POLL_MS = 5_000\n/** How often the watcher flag is consulted while waiting out a poll. */\nconst TICK_MS = 250\n/** Ceiling for retry backoff when the runtime simply is not usable yet. */\nconst MAX_BACKOFF_MS = 5 * 60_000\nconst MAX_LOG_BYTES = 5 * 1024 * 1024\nconst KEEP_LOG_BYTES = 1024 * 1024\n\nexport interface RunDaemonOpts {\n /** THE identity home for the agent this daemon serves. */\n home: string\n /** How to spawn one headless turn of this integration's coding agent. */\n adapter: RuntimeAdapter\n /** Scratch dir override; defaults to `<home>/daemon-workdir`. */\n workdir?: string\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms))\n\n/**\n * Identity fingerprint: changes when the user signs out, signs in, or swaps to\n * a different agent. Comparing it is how the supervisor notices without caring\n * why.\n */\nfunction fingerprint(home: string): string | null {\n const id = resolveIdentity(home)\n return id === null ? null : `${id.apiKey}:${id.handle ?? ''}`\n}\n\n/** Bound launchd's append-only file without losing the most recent diagnosis. */\nfunction boundDaemonLog(home: string): void {\n const file = path.join(home, 'daemon.log')\n try {\n const size = fs.statSync(file).size\n if (size <= MAX_LOG_BYTES) return\n const fd = fs.openSync(file, 'r')\n try {\n const keep = Buffer.alloc(Math.min(KEEP_LOG_BYTES, size))\n fs.readSync(fd, keep, 0, keep.length, size - keep.length)\n fs.writeFileSync(\n file,\n `[agentchat:info] older daemon log output truncated at ${new Date().toISOString()}\\n${keep.toString('utf-8')}`,\n )\n } finally {\n fs.closeSync(fd)\n }\n } catch {\n /* absent/unreadable log is not a runtime failure */\n }\n}\n\n/**\n * Run the always-on daemon. Returns only on a condition that makes running\n * pointless — namely another daemon already holding this home's lock.\n */\nexport async function runDaemon(opts: RunDaemonOpts): Promise<number> {\n const home = path.resolve(opts.home)\n const workdir = opts.workdir ?? path.join(home, 'daemon-workdir')\n boundDaemonLog(home)\n\n // Hooks default to `warn` because they run on every session start and must\n // stay silent. A resident service is the opposite case: its output goes to a\n // log file nobody sees until something is wrong, and an empty log is useless\n // for answering \"is always-on actually working?\". Still overridable.\n if (process.env['AGENTCHAT_LOG_LEVEL'] === undefined) process.env['AGENTCHAT_LOG_LEVEL'] = 'info'\n\n // Taken for the PROCESS, not for a connection: one resident daemon per\n // identity home, signed in or not.\n const lock = acquireLeaderLock(home)\n if (lock === null) return 1\n\n let live: Daemon | null = null\n let liveFingerprint: string | null = null\n let observedFingerprint: string | null = null\n let adapterFingerprint: string | null = null\n /** A credential the server refused. Sit out until it CHANGES. */\n let refused: string | null = null\n /** Consecutive connect failures, for backoff. Reset on success or on a new\n * credential — a fresh sign-in always deserves a fast first attempt. */\n let failures = 0\n /** Last failure message, so a persistent condition is logged once. */\n let lastFailure: string | null = null\n let shuttingDown = false\n\n const disconnect = (why: string): void => {\n if (live === null) return\n log.info(`${why} — disconnecting, staying resident`)\n live.stop()\n live = null\n liveFingerprint = null\n idle(home)\n }\n\n const shutdown = (sig: string): void => {\n if (shuttingDown) return\n shuttingDown = true\n log.info(`${sig} — shutting down`)\n live?.stop()\n idle(home)\n lock.release()\n process.exit(0)\n }\n process.on('SIGINT', () => shutdown('SIGINT'))\n process.on('SIGTERM', () => shutdown('SIGTERM'))\n\n // Best-effort accelerator so signing in connects in about a second instead of\n // waiting out a poll. fs.watch is unreliable on some filesystems and network\n // mounts, so the poll below is the real guarantee and this is pure upside.\n let nudged = false\n try {\n fs.mkdirSync(home, { recursive: true })\n const watcher = fs.watch(home, (_event, filename) => {\n if (filename === null || String(filename).startsWith('credentials')) nudged = true\n })\n // Resource exhaustion and unsupported filesystems can surface as an\n // asynchronous `error` after fs.watch has returned. Without a listener\n // Node treats that as fatal; polling is already the source of truth, so\n // losing this best-effort accelerator must never take down the daemon.\n watcher.on('error', (err) => {\n log.warn(`credential watcher unavailable; polling instead: ${String(err)}`)\n try {\n watcher.close()\n } catch {\n /* already closed */\n }\n })\n watcher.unref()\n } catch {\n /* polling covers it */\n }\n\n log.info(`always-on resident for ${home} (${credentialsPath(home)})`)\n idle(home)\n\n for (;;) {\n if (shuttingDown) break\n const fp = fingerprint(home)\n const identityChanged = fp !== observedFingerprint\n if (identityChanged) {\n disconnect(fp === null ? 'signed out' : 'identity changed')\n observedFingerprint = fp\n failures = 0\n lastFailure = null\n // A refusal applies only to the exact rejected credential.\n if (fp !== refused) refused = null\n }\n\n if (fp === null) {\n // Signed out, or never signed in. Idle — and forget any refusal, since\n // the next credential to appear deserves a fresh attempt.\n if (refused !== null) refused = null\n } else if (fp !== liveFingerprint) {\n if (fp === refused) {\n // Same key the server already rejected; wait for a different one\n // rather than hammering the endpoint.\n } else {\n try {\n const cfg = await resolveDaemonConfig({ home, workdir })\n // A new AgentChat identity must not inherit the previous identity's\n // Codex/Claude conversation transcripts.\n if (adapterFingerprint !== fp) {\n opts.adapter.reset?.(`${cfg.apiBase}:${cfg.handle}`)\n adapterFingerprint = fp\n }\n const candidate = new Daemon(cfg, opts.adapter, undefined, (failure) => {\n if (failure.kind === 'socket-auth') {\n // Auth refused: stop trying THIS credential, keep the process.\n log.warn(`credential refused (${failure.reason}) — idling until it changes`)\n refused = fp\n } else {\n // Runtime auth/setup can fail after preflight. Retry the same\n // AgentChat identity through the normal bounded supervisor loop.\n log.warn(`runtime became unhealthy (${failure.reason}) — re-running preflight`)\n failures += 1\n }\n live = null\n liveFingerprint = null\n idle(home)\n })\n await candidate.start()\n live = candidate\n liveFingerprint = fp\n failures = 0\n lastFailure = null\n } catch (err) {\n // Runtime not ready (host CLI missing or not logged in), network\n // down, whatever. Stay resident and try again later — exiting would\n // just make the service manager restart us in a loop.\n //\n // Backed off and de-duplicated: \"codex CLI not found on PATH\" is a\n // condition that can last days, and retrying every 5s would write\n // ~17k identical lines a day into a log meant to be readable.\n const msg = String(err instanceof Error ? err.message : err)\n if (msg !== lastFailure) {\n log.warn(`not connecting yet: ${msg}`)\n lastFailure = msg\n }\n failures += 1\n live = null\n liveFingerprint = null\n idle(home)\n }\n }\n }\n\n // Wait out the poll interval, but wake as soon as the watcher fires so a\n // sign-in connects in well under a second instead of up to POLL_MS. The\n // flag has to be checked DURING the wait — an earlier version only\n // consulted it afterwards, which made the watcher useless.\n // Exponential backoff while a connect keeps failing, capped — but the\n // watcher still wakes us instantly when credentials change, so backing off\n // never delays a real sign-in.\n const waitMs = failures === 0 ? POLL_MS : Math.min(POLL_MS * 2 ** Math.min(failures, 6), MAX_BACKOFF_MS)\n nudged = false\n const deadline = Date.now() + waitMs\n while (!nudged && !shuttingDown && Date.now() < deadline) {\n await sleep(TICK_MS)\n }\n }\n\n lock.release()\n return 0\n}\n","import * as path from 'node:path'\nimport { resolveIdentity } from '../identity/credentials.js'\nimport { getMeLite } from '../wire/index.js'\n\n// ─── Daemon identity resolution ─────────────────────────────────────────────\n//\n// The daemon runs AS one host agent — the same identity that agent's in-session\n// hooks use, never a separate account. It reads that credential from the home\n// it is GIVEN.\n//\n// The predecessor of this file mapped a `runtime` enum to a home\n// (`codex → ~/.codex/agentchat`, `claude-code → ~/.claude/agentchat`). That\n// mapping is exactly the \"a function that decides can decide wrong\" defect this\n// package exists to make unrepresentable, so it is gone: the caller passes its\n// own home and there is no enum to mis-set.\n\nexport interface DaemonConfig {\n apiKey: string\n handle: string\n apiBase: string\n wsUrl: string\n /** The identity home. Credentials, leader lock, and heartbeat all live here. */\n home: string\n /** Scratch dir for the adapter (spawned-turn cwd, generated MCP config). */\n workdir: string\n}\n\n/** `https://api.agentchat.me` → `wss://api.agentchat.me/v1/ws`. */\nexport function wsUrlFor(apiBase: string): string {\n return apiBase.replace(/^http/, 'ws').replace(/\\/+$/, '') + '/v1/ws'\n}\n\nexport interface ResolveDaemonOpts {\n /** THE identity home. Required — this module never derives one. */\n home: string\n workdir?: string\n}\n\n/**\n * Resolve the identity the daemon runs as.\n *\n * Async because the handle is load-bearing at runtime — it filters this agent's\n * own outbound echoed back by server fan-out, and decides whether a group\n * mention names it. An env-only identity (`AGENTCHAT_API_KEY`, no credentials\n * file) has no handle on disk, so we ask the server rather than starting up\n * blind and replying to ourselves.\n */\nexport async function resolveDaemonConfig(opts: ResolveDaemonOpts): Promise<DaemonConfig> {\n const home = path.resolve(opts.home)\n const id = resolveIdentity(home)\n if (id === null) {\n throw new Error(`no AgentChat identity in ${home} — register this agent first`)\n }\n\n let handle = id.handle\n if (handle === null) {\n const me = await getMeLite({ apiKey: id.apiKey, apiBase: id.apiBase })\n if (me === null) {\n throw new Error(\n 'could not determine this agent’s handle (no credentials file, and /v1/agents/me did not answer)',\n )\n }\n handle = me.handle\n }\n\n return {\n apiKey: id.apiKey,\n handle,\n apiBase: id.apiBase,\n wsUrl: wsUrlFor(id.apiBase),\n home,\n workdir: opts.workdir ?? path.join(home, 'daemon-workdir'),\n }\n}\n","import * as crypto from 'node:crypto'\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { log } from '../util/log.js'\nimport { atomicWriteFile } from '../util/fsutil.js'\nimport type { DaemonConfig } from './config.js'\nimport { AgentWsClient } from './ws-client.js'\nimport { ReplyCoord } from './coord.js'\nimport { beat } from './health.js'\nimport { contextOf, senderOf, type SyncRow } from './frames.js'\nimport type { RuntimeAdapter } from './adapter-types.js'\n\n// ─── The core loop ──────────────────────────────────────────────────────────\n//\n// WS pushes message.new → dedup → coexistence check (yield to a live session,\n// then claim the sole right to reply) → (per-conversation serialized, globally\n// capped) run one runtime turn per message → ack that message on success.\n// Failures retry with bounded exponential backoff and remain unacknowledged\n// until they genuinely succeed.\n//\n// Host-agnostic by construction: everything it knows about the agent arrives in\n// `DaemonConfig`, and everything it knows about the coding agent arrives as a\n// `RuntimeAdapter`. It cannot name a host, so it cannot act on the wrong one.\n\nconst MAX_TIMER_MS = 2_147_483_647\n\nfunction positiveBoundedEnv(name: string, fallback: number): number {\n const parsed = Number(process.env[name])\n return Number.isFinite(parsed) && parsed > 0\n ? Math.min(parsed, MAX_TIMER_MS)\n : fallback\n}\n\nconst MAX_CONCURRENT_TURNS = 3\nconst HEARTBEAT_MS = 30_000\nconst SEEN_TTL_MS = 24 * 60 * 60_000\nconst MAX_COMPLETED_SEEN = 10_000\nconst PAUSE_AT_PENDING = Math.max(\n 1,\n Math.floor(positiveBoundedEnv('AGENTCHATD_MAX_PENDING', 2_000)),\n)\nconst RESUME_AT_PENDING = Math.max(1, Math.floor(PAUSE_AT_PENDING / 2))\nconst RETRY_BASE_MS = positiveBoundedEnv('AGENTCHATD_RETRY_MS', 1_000)\nconst RETRY_MAX_MS = Math.max(\n RETRY_BASE_MS,\n positiveBoundedEnv('AGENTCHATD_RETRY_MAX_MS', 5 * 60_000),\n)\n// When the agent's live coding session is actively working, wait this long\n// before claiming — a head start so the human-driven session (priority) can\n// grab the message first. Only applies while a session is active; the common\n// \"no session, daemon only\" path has zero added latency. Tunable for testing.\nconst YIELD_MS = Number(process.env['AGENTCHATD_YIELD_MS'] ?? 10_000)\n\nconst delay = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms))\n\nfunction retryDelay(attempt: number): number {\n // Clamp the exponent before multiplication so a long-lived outage never\n // overflows setTimeout or turns into a tight retry loop.\n return Math.min(RETRY_BASE_MS * 2 ** Math.min(20, Math.max(0, attempt - 1)), RETRY_MAX_MS)\n}\n\ntype DeliveryStatus = 'queued' | 'running' | 'retry-wait' | 'handled'\n\ninterface DeliveryState {\n row: SyncRow\n status: DeliveryStatus\n attempts: number\n updatedAt: number\n}\n\nexport interface DaemonFailure {\n kind: 'socket-auth' | 'runtime'\n reason: string\n}\n\nfunction installationId(home: string): string {\n const file = path.join(home, 'daemon.installation-id')\n try {\n const existing = fs.readFileSync(file, 'utf-8').trim()\n if (/^[0-9a-f-]{36}$/i.test(existing)) return existing\n } catch {\n /* create below */\n }\n const id = crypto.randomUUID()\n try {\n atomicWriteFile(file, `${id}\\n`, 0o600)\n } catch (err) {\n // A read-only home must not make delivery disappear. The random fallback\n // is process-unique, so it still avoids cross-machine hostname collisions;\n // it simply cannot reclaim its prior claim after a restart.\n log.warn(`could not persist daemon installation id: ${String(err)}`)\n }\n return id\n}\n\nexport class Daemon {\n private readonly ws: AgentWsClient\n private readonly coord: ReplyCoord\n private readonly seen = new Map<string, DeliveryState>()\n private readonly convQueues = new Map<string, SyncRow[]>()\n private readonly convWorkers = new Set<string>()\n private pending = 0\n private inFlight = 0\n private readonly waiters: Array<() => void> = []\n private stopping = false\n private heartbeatTimer: NodeJS.Timeout | null = null\n\n constructor(\n private readonly cfg: DaemonConfig,\n private readonly adapter: RuntimeAdapter,\n ws?: AgentWsClient, // injectable for tests; defaults to a real socket\n /** Called when the socket gives up for good (auth refused). The supervisor\n * above decides what happens next — this class does not end the process. */\n private readonly onTerminal?: (failure: DaemonFailure) => void,\n ) {\n // Stable holder token: the same across a restart of THIS installation, so a\n // restarted daemon re-claims its own in-flight messages instead of being\n // locked out by its own prior claim. A hostname is not unique: two laptops\n // called \"macbook\" can legitimately use the same AgentChat identity.\n this.coord = new ReplyCoord({\n apiKey: cfg.apiKey,\n apiBase: cfg.apiBase,\n holder: `daemon:${installationId(cfg.home)}`,\n })\n this.ws = ws ?? new AgentWsClient(cfg.wsUrl, cfg.apiKey)\n this.ws.on('inbound', (row: SyncRow) => this.onInbound(row))\n // Every fresh connection stamps the beacon immediately (don't wait up to 30s\n // for the first interval tick to prove we're live).\n this.ws.on('ready', () => beat(this.cfg.home))\n this.ws.on('terminal', (reason: string) => {\n // Auth refused. Do NOT end the process: this daemon is resident, and a\n // rejected key is a state to sit out, not to die from — the user may sign\n // in again with a good one. Setting process.exitCode here made the\n // service manager restart the whole thing on a loop instead.\n log.error(`daemon terminal: ${reason}`)\n this.stop()\n this.onTerminal?.({ kind: 'socket-auth', reason })\n })\n }\n\n async start(): Promise<void> {\n const pre = await this.adapter.preflight()\n if (!pre.ok) {\n throw new Error(`runtime (${this.adapter.name}) not ready: ${pre.detail}`)\n }\n log.info(`agentchat daemon up as @${this.cfg.handle} via ${this.adapter.name}; holding the wire`)\n this.ws.start()\n // Keep the beacon fresh while connected. unref so it never by itself keeps\n // the process alive.\n this.heartbeatTimer = setInterval(() => {\n if (this.ws.connected) beat(this.cfg.home)\n }, HEARTBEAT_MS)\n this.heartbeatTimer.unref()\n }\n\n stop(): void {\n this.stopping = true\n if (this.heartbeatTimer) clearInterval(this.heartbeatTimer)\n this.ws.stop()\n }\n\n private onInbound(row: SyncRow): void {\n // Ignore our own outbound echoed back by server fan-out.\n if (senderOf(row) === this.cfg.handle) return\n this.pruneSeen()\n const prior = this.seen.get(row.id)\n if (prior) {\n prior.updatedAt = Date.now()\n // A replay after a lost ack must be ACKED again, not merely swallowed.\n if (prior.status === 'handled') this.ws.ack(row.id)\n return\n }\n this.seen.set(row.id, { row, status: 'queued', attempts: 0, updatedAt: Date.now() })\n this.pending += 1\n if (this.pending >= PAUSE_AT_PENDING) this.ws.pauseInbound()\n this.enqueueExisting(row)\n }\n\n /** Queue one already-tracked row and ensure exactly one worker for its conversation. */\n private enqueueExisting(row: SyncRow): void {\n const queue = this.convQueues.get(row.conversation_id) ?? []\n queue.push(row)\n this.convQueues.set(row.conversation_id, queue)\n if (this.convWorkers.has(row.conversation_id)) return\n this.convWorkers.add(row.conversation_id)\n void this.drainConversation(row.conversation_id)\n }\n\n /** Process each message independently, in arrival order within a conversation. */\n private async drainConversation(conversationId: string): Promise<void> {\n try {\n while (!this.stopping) {\n const queue = this.convQueues.get(conversationId)\n if (!queue || queue.length === 0) break\n const row = queue.shift()\n if (!row) break\n await this.handle(row)\n }\n } catch (err) {\n log.warn(`unhandled in conv ${conversationId}: ${String(err)}`)\n } finally {\n this.convWorkers.delete(conversationId)\n const queue = this.convQueues.get(conversationId)\n if (!queue || queue.length === 0) this.convQueues.delete(conversationId)\n else if (!this.stopping) {\n // A row may have landed between the final empty check and deleting the\n // worker marker. Re-arm rather than leaving it stranded.\n this.convWorkers.add(conversationId)\n void this.drainConversation(conversationId)\n }\n }\n }\n\n private async handle(row: SyncRow): Promise<void> {\n if (this.stopping) return\n const initial = this.seen.get(row.id)\n if (!initial || initial.status !== 'queued') return\n\n // ── Coexistence: agree on exactly one replier ──\n // If the agent's live coding session is actively working, yield briefly so\n // its hook can claim + handle this first (the human-driven session has\n // priority). Then claim the sole right to reply; whoever wins is it.\n if (await this.coord.isSessionActive()) {\n log.info(`msg ${row.id}: live session active — yielding for ${YIELD_MS}ms`)\n await delay(YIELD_MS)\n if (this.stopping) return\n }\n\n if (!(await this.coord.claim(row.id))) {\n // A live session owns this one. Forget our dedup state and do NOT ack:\n // the session's sync path still needs to see and commit it.\n log.info(`msg ${row.id}: claimed by the live session — standing down`)\n this.seen.delete(row.id)\n this.markNoLongerPending()\n return\n }\n\n // A failed message stays at the head of its conversation. This preserves\n // ordering: a later message in the same thread cannot be acknowledged\n // before the earlier one has actually been handled.\n while (!this.stopping) {\n const state = this.seen.get(row.id)\n if (!state || state.status === 'handled') return\n state.status = 'running'\n state.attempts += 1\n state.updatedAt = Date.now()\n const attempt = state.attempts\n\n await this.acquireSlot()\n if (this.stopping) {\n this.releaseSlot()\n return\n }\n let result\n try {\n log.info(\n `turn for msg ${row.id} in ${row.conversation_id} from @${senderOf(row)} (attempt ${attempt})`,\n )\n const ctx = contextOf(row)\n result = await this.adapter.runTurn({\n messageId: row.id,\n conversationId: row.conversation_id,\n sender: senderOf(row),\n text:\n typeof row.content?.['text'] === 'string'\n ? (row.content['text'] as string)\n : '',\n createdAt: typeof row.created_at === 'string' ? row.created_at : undefined,\n type: typeof row.type === 'string' ? row.type : undefined,\n senderDisplayName: ctx.senderDisplayName,\n senderKind: ctx.senderKind,\n groupName: ctx.groupName,\n mentioned: ctx.mentions.includes(this.cfg.handle.toLowerCase()),\n })\n } catch (err) {\n result = { ok: false, detail: `adapter threw: ${String(err)}` }\n } finally {\n this.releaseSlot()\n }\n\n if (result.ok) {\n this.markHandled(row.id)\n return\n }\n if (result.fatal) {\n log.error(`fatal turn error: ${result.detail} — stopping runtime so preflight can recover`)\n this.stop()\n this.onTerminal?.({ kind: 'runtime', reason: result.detail ?? 'runtime failed' })\n return\n }\n\n const retryMs = retryDelay(attempt)\n state.status = 'retry-wait'\n state.updatedAt = Date.now()\n log.warn(\n `turn failed for msg ${row.id}: ${result.detail}; retrying in ${retryMs}ms without acknowledging it`,\n )\n await delay(retryMs)\n }\n }\n\n private markHandled(messageId: string): void {\n const state = this.seen.get(messageId)\n if (!state || state.status === 'handled') return\n state.status = 'handled'\n state.updatedAt = Date.now()\n this.markNoLongerPending()\n this.ws.ack(messageId)\n }\n\n private markNoLongerPending(): void {\n this.pending = Math.max(0, this.pending - 1)\n if (this.pending <= RESUME_AT_PENDING) this.ws.resumeInbound()\n }\n\n /** Bound reconnect-dedup memory without ever evicting unfinished work. */\n private pruneSeen(): void {\n const cutoff = Date.now() - SEEN_TTL_MS\n const completed: Array<[string, DeliveryState]> = []\n for (const entry of this.seen.entries()) {\n const [id, state] = entry\n if (state.status !== 'handled') continue\n if (state.updatedAt < cutoff) this.seen.delete(id)\n else completed.push(entry)\n }\n if (completed.length <= MAX_COMPLETED_SEEN) return\n completed.sort((a, b) => a[1].updatedAt - b[1].updatedAt)\n for (const [id] of completed.slice(0, completed.length - MAX_COMPLETED_SEEN)) {\n this.seen.delete(id)\n }\n }\n\n // ─── global concurrency semaphore ─────────────────────────────────────────\n // A waiter inherits the releaser's slot directly (inFlight unchanged on\n // hand-off) — no decrement-then-reincrement window that could momentarily\n // exceed the cap.\n private acquireSlot(): Promise<void> {\n if (this.inFlight < MAX_CONCURRENT_TURNS) {\n this.inFlight++\n return Promise.resolve()\n }\n return new Promise<void>((resolve) => this.waiters.push(resolve))\n }\n\n private releaseSlot(): void {\n const next = this.waiters.shift()\n if (next) next() // pass the slot on; inFlight stays at the cap\n else this.inFlight--\n }\n}\n"],"mappings":"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","names":["path","fs","path","resolve","MAX_BACKOFF_MS"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
+
/** Low-cardinality identity attached to every coding-agent API operation. */
|
|
4
|
+
declare const CODING_AGENTS_CLIENT_IDENTITY: {
|
|
5
|
+
readonly name: "coding_agents";
|
|
6
|
+
readonly version: "0.0.1312";
|
|
7
|
+
};
|
|
8
|
+
/** Headers for raw HTTP and WebSocket transports that bypass the SDK. */
|
|
9
|
+
declare const CODING_AGENTS_CLIENT_HEADERS: Readonly<Record<string, string>>;
|
|
10
|
+
|
|
11
|
+
/** Published package version, kept in lockstep with package.json by tests. */
|
|
12
|
+
declare const VERSION = "0.0.1312";
|
|
13
|
+
|
|
3
14
|
declare const SyncRowSchema: z.ZodObject<{
|
|
4
15
|
id: z.ZodString;
|
|
5
16
|
conversation_id: z.ZodString;
|
|
@@ -226,6 +237,9 @@ declare function markAlwaysOnWanted(home: string): void;
|
|
|
226
237
|
/** Forget the intent (user chose session-only, or uninstalled). */
|
|
227
238
|
declare function clearAlwaysOnWanted(home: string): void;
|
|
228
239
|
declare function alwaysOnWanted(home: string): boolean;
|
|
240
|
+
declare function readAlwaysOnInstalledVersion(home: string): string | null;
|
|
241
|
+
declare function markAlwaysOnInstalledVersion(home: string, version: string): void;
|
|
242
|
+
declare function clearAlwaysOnInstalledVersion(home: string): void;
|
|
229
243
|
/** Remember that the user switched always-on off. Survives re-install. */
|
|
230
244
|
declare function markAlwaysOnOptOut(home: string): void;
|
|
231
245
|
/** Cleared only by an explicit `daemon install` — never implicitly. */
|
|
@@ -468,10 +482,10 @@ interface HostProfile {
|
|
|
468
482
|
* it. Hosts wired by their own installer (a Claude Code plugin) omit it.
|
|
469
483
|
*/
|
|
470
484
|
isWired?(): boolean;
|
|
471
|
-
/** Extra teardown on logout; returns descriptions of what was removed. */
|
|
472
|
-
removeWiring?(): string[];
|
|
473
485
|
/** Host-specific doctor checks, appended after the shared ones. */
|
|
474
|
-
extraDoctorChecks?(
|
|
486
|
+
extraDoctorChecks?(opts: {
|
|
487
|
+
fix?: boolean;
|
|
488
|
+
}): DoctorCheck[];
|
|
475
489
|
/** Extra lines printed after a successful logout. */
|
|
476
490
|
logoutHints?(): string[];
|
|
477
491
|
}
|
|
@@ -573,9 +587,17 @@ interface Plan {
|
|
|
573
587
|
env: Record<string, string>;
|
|
574
588
|
}
|
|
575
589
|
declare function planForTest(opts: ServiceOpts): Plan;
|
|
590
|
+
/** Quote one systemd command/environment word, including specifier escaping. */
|
|
591
|
+
declare function systemdQuote(value: string): string;
|
|
592
|
+
declare function systemdUnit(p: Plan): string;
|
|
593
|
+
declare function xmlEscape(value: string): string;
|
|
594
|
+
declare function launchdPlist(p: Plan): string;
|
|
576
595
|
declare function installService(opts: ServiceOpts): void;
|
|
577
596
|
declare function uninstallService(opts: ServiceRef): void;
|
|
578
597
|
declare function serviceStatus(opts: ServiceRef): string;
|
|
598
|
+
/** Pure on-disk check used by integration self-healing and doctor. */
|
|
599
|
+
declare function serviceDefinitionCurrent(opts: ServiceOpts): boolean;
|
|
600
|
+
declare function serviceInstalled(opts: ServiceRef): boolean;
|
|
579
601
|
|
|
580
602
|
declare const log: {
|
|
581
603
|
error: (msg: string) => void;
|
|
@@ -601,6 +623,8 @@ declare function relativeWhen(createdAt: string | undefined, now?: number): stri
|
|
|
601
623
|
declare function formatWhen(createdAt: string | undefined, now?: number): string;
|
|
602
624
|
|
|
603
625
|
declare function atomicWriteFile(filePath: string, data: string, mode?: number): void;
|
|
626
|
+
/** Crash-safe file replacement for shipped executable bundles. */
|
|
627
|
+
declare function atomicCopyFile(source: string, destination: string, mode?: number): void;
|
|
604
628
|
declare function readJsonFile<T>(filePath: string): T | null;
|
|
605
629
|
|
|
606
|
-
export { ANCHOR_END, ANCHOR_START, type AlwaysOnState, type AnchorAction, type Credentials, DEFAULT_API_BASE, type DoctorCheck, type DoctorOpts, HEARTBEAT_FILE, type HookContext, type HookDialect, type HookInput, type HookRunners, type HookState, type HostCopy, type HostProfile, type IdentityCommands, type LockHandle, type ManualCopy, type MessageContext, type PendingRegistration, type RegisterOpts, type ResolvedIdentity, type ServiceOpts, type ServiceRef, type SessionStartResult, type StopResult, type SyncRow, type Verdict, type WireConfig, WireError, absoluteUtc, acquireLeaderLock, alwaysOnHealth, alwaysOnOptedOut, alwaysOnState, alwaysOnWanted, anchorLabelOf, atomicWriteFile, beat, claimReply, clearAlwaysOnOptOut, clearAlwaysOnWanted, clearCredentials, clearOfferDeclined, clearPending, clearSessionActive, contextOf, createHookRunners, createIdentityCommands, credentialsPath, formatAlwaysOnDown, formatRegistrationOffer, formatSessionStart, formatStopPickup, formatWhen, getContinuations, getMeLite, hasAnchorAt, hooksDisabled, idle, installService, lastDeliveryId, log, markAlwaysOnOptOut, markAlwaysOnWanted, markSessionActive, offerDeclined, pendingPath, planForTest, readAnchorHandleAt, readAnchorHandleFrom, readCredentials, readHookInput, readJsonFile, readPending, readState, recordContinuation, recordOfferDeclined, recordRegistrationOffer, relativeAge, relativeWhen, removeAnchorAt, renderAnchorBlock, renderDeclinedBlock, renderManual, renderUnregisteredBlock, resetSession, resolveIdentity, serviceStatus, sessionStart, setPendingAck, shouldOfferRegistration, statePath, stop, stripAnchorBlock, syncAck, syncPeek, takePendingAck, uninstallService, upsertAnchorBlock, userPrompt, writeAnchor, writeCredentials, writePending, writeState };
|
|
630
|
+
export { ANCHOR_END, ANCHOR_START, type AlwaysOnState, type AnchorAction, CODING_AGENTS_CLIENT_HEADERS, CODING_AGENTS_CLIENT_IDENTITY, type Credentials, DEFAULT_API_BASE, type DoctorCheck, type DoctorOpts, HEARTBEAT_FILE, type HookContext, type HookDialect, type HookInput, type HookRunners, type HookState, type HostCopy, type HostProfile, type IdentityCommands, type LockHandle, type ManualCopy, type MessageContext, type PendingRegistration, type Plan, type RegisterOpts, type ResolvedIdentity, type ServiceOpts, type ServiceRef, type SessionStartResult, type StopResult, type SyncRow, VERSION, type Verdict, type WireConfig, WireError, absoluteUtc, acquireLeaderLock, alwaysOnHealth, alwaysOnOptedOut, alwaysOnState, alwaysOnWanted, anchorLabelOf, atomicCopyFile, atomicWriteFile, beat, claimReply, clearAlwaysOnInstalledVersion, clearAlwaysOnOptOut, clearAlwaysOnWanted, clearCredentials, clearOfferDeclined, clearPending, clearSessionActive, contextOf, createHookRunners, createIdentityCommands, credentialsPath, formatAlwaysOnDown, formatRegistrationOffer, formatSessionStart, formatStopPickup, formatWhen, getContinuations, getMeLite, hasAnchorAt, hooksDisabled, idle, installService, lastDeliveryId, launchdPlist, log, markAlwaysOnInstalledVersion, markAlwaysOnOptOut, markAlwaysOnWanted, markSessionActive, offerDeclined, pendingPath, planForTest, readAlwaysOnInstalledVersion, readAnchorHandleAt, readAnchorHandleFrom, readCredentials, readHookInput, readJsonFile, readPending, readState, recordContinuation, recordOfferDeclined, recordRegistrationOffer, relativeAge, relativeWhen, removeAnchorAt, renderAnchorBlock, renderDeclinedBlock, renderManual, renderUnregisteredBlock, resetSession, resolveIdentity, serviceDefinitionCurrent, serviceInstalled, serviceStatus, sessionStart, setPendingAck, shouldOfferRegistration, statePath, stop, stripAnchorBlock, syncAck, syncPeek, systemdQuote, systemdUnit, takePendingAck, uninstallService, upsertAnchorBlock, userPrompt, writeAnchor, writeCredentials, writePending, writeState, xmlEscape };
|