@agentchatme/agent-core 0.0.1

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/daemon/ws-client.ts","../src/daemon/frames.ts","../src/daemon/coord.ts","../src/daemon/format.ts","../src/daemon/config.ts","../src/daemon/loop.ts","../src/daemon/run.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 { 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 ) {\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 log.error(`daemon terminal: ${reason}`)\n this.stop()\n process.exitCode = 1\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","import { log } from '../util/log.js'\nimport { acquireLeaderLock } from './leader-lock.js'\nimport { resolveDaemonConfig } from './config.js'\nimport { Daemon } from './loop.js'\nimport type { RuntimeAdapter } from './adapter-types.js'\n\n// ─── The always-on entrypoint ───────────────────────────────────────────────\n//\n// One call an integration's daemon binary makes. It owns everything that is\n// the same for every coding agent — identity resolution, the single-daemon\n// leader lock, signal handling, holding the process open — and nothing that\n// differs. The one thing that differs, \"how do I spawn a headless turn of my\n// runtime\", arrives as the `adapter` argument.\n//\n// Never returns while healthy: a daemon that returns is a daemon that stopped\n// answering, and the service manager would just restart it.\n\nexport interface RunDaemonOpts {\n /** THE identity home for the agent this daemon runs as. */\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\n/**\n * Run the always-on daemon. Resolves to a process exit code only on a failure\n * that makes running pointless (no identity, another daemon already holds the\n * lock, the runtime is not usable).\n */\nexport async function runDaemon(opts: RunDaemonOpts): Promise<number> {\n let cfg\n try {\n cfg = await resolveDaemonConfig({\n home: opts.home,\n ...(opts.workdir !== undefined ? { workdir: opts.workdir } : {}),\n })\n } catch (err) {\n console.error(String(err instanceof Error ? err.message : err))\n return 1\n }\n\n // One daemon per identity on this box.\n const lock = acquireLeaderLock(cfg.home)\n if (lock === null) return 1\n\n const daemon = new Daemon(cfg, opts.adapter)\n let releasing = false\n const shutdown = (sig: string): void => {\n if (releasing) return\n releasing = true\n log.info(`${sig} — shutting down`)\n daemon.stop()\n lock.release()\n process.exit(0)\n }\n process.on('SIGINT', () => shutdown('SIGINT'))\n process.on('SIGTERM', () => shutdown('SIGTERM'))\n\n try {\n await daemon.start()\n } catch (err) {\n console.error(String(err instanceof Error ? err.message : err))\n lock.release()\n return 1\n }\n\n // Hold the process open; the WS client keeps event-loop work alive.\n return await new Promise<number>(() => {})\n}\n"],"mappings":"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","names":["resolve"]}
@@ -0,0 +1,512 @@
1
+ import { z } from 'zod';
2
+
3
+ declare const SyncRowSchema: z.ZodObject<{
4
+ id: z.ZodString;
5
+ conversation_id: z.ZodString;
6
+ delivery_id: z.ZodNullable<z.ZodString>;
7
+ sender: z.ZodOptional<z.ZodString>;
8
+ sender_handle: z.ZodOptional<z.ZodString>;
9
+ type: z.ZodOptional<z.ZodString>;
10
+ content: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
11
+ created_at: z.ZodOptional<z.ZodString>;
12
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
13
+ id: z.ZodString;
14
+ conversation_id: z.ZodString;
15
+ delivery_id: z.ZodNullable<z.ZodString>;
16
+ sender: z.ZodOptional<z.ZodString>;
17
+ sender_handle: z.ZodOptional<z.ZodString>;
18
+ type: z.ZodOptional<z.ZodString>;
19
+ content: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
20
+ created_at: z.ZodOptional<z.ZodString>;
21
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
22
+ id: z.ZodString;
23
+ conversation_id: z.ZodString;
24
+ delivery_id: z.ZodNullable<z.ZodString>;
25
+ sender: z.ZodOptional<z.ZodString>;
26
+ sender_handle: z.ZodOptional<z.ZodString>;
27
+ type: z.ZodOptional<z.ZodString>;
28
+ content: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
29
+ created_at: z.ZodOptional<z.ZodString>;
30
+ }, z.ZodTypeAny, "passthrough">>;
31
+ type SyncRow = z.infer<typeof SyncRowSchema>;
32
+ /** Platform-authored trusted context (server `message.context`) — resolved
33
+ * sender identity, the conversation descriptor, and the parsed mention list.
34
+ * Read defensively off the passthrough row; kept in sync with the daemon's
35
+ * copy at daemon/src/wire.ts (same deliberate duplication as SyncRow). */
36
+ interface MessageContext {
37
+ senderDisplayName: string | null;
38
+ senderKind: 'agent' | 'system';
39
+ groupName: string | null;
40
+ memberCount: number | null;
41
+ mentions: string[];
42
+ }
43
+ declare function contextOf(row: SyncRow): MessageContext;
44
+ interface WireConfig {
45
+ apiKey: string;
46
+ apiBase: string;
47
+ timeoutMs?: number;
48
+ }
49
+ declare class WireError extends Error {
50
+ readonly status: number;
51
+ constructor(status: number, detail: string);
52
+ }
53
+ /**
54
+ * Non-destructive peek at undelivered messages, oldest first. Does not
55
+ * mark anything delivered — pair with `syncAck` once the rows have been
56
+ * handed to the agent.
57
+ */
58
+ declare function syncPeek(cfg: WireConfig, opts?: {
59
+ limit?: number;
60
+ after?: string;
61
+ }): Promise<SyncRow[]>;
62
+ /**
63
+ * Commit every delivery at-or-before the cursor as delivered. In the
64
+ * plugin model this is called at the moment rows are injected into the
65
+ * agent's context — injection IS delivery.
66
+ */
67
+ declare function syncAck(cfg: WireConfig, lastDeliveryId: string): Promise<number>;
68
+ /**
69
+ * Minimal self-lookup for hooks that resolved an env-var key with no
70
+ * credentials file (so no cached handle). Full profile reads go through
71
+ * the SDK; hooks only ever need the handle.
72
+ */
73
+ declare function getMeLite(cfg: WireConfig): Promise<{
74
+ handle: string;
75
+ } | null>;
76
+ /** Announce/refresh "this live session is actively working" so the daemon
77
+ * yields to it. Best-effort; a failure is a silent no-op. */
78
+ declare function markSessionActive(cfg: WireConfig, ttlSeconds?: number): Promise<void>;
79
+ /** Release the active flag (session ended) so the daemon resumes immediately
80
+ * instead of waiting out the TTL. Best-effort. */
81
+ declare function clearSessionActive(cfg: WireConfig): Promise<void>;
82
+ /** Claim the sole right to reply to one message so the daemon stands down for
83
+ * it. Fail-OPEN to TRUE: if coordination is unavailable, surface the message
84
+ * anyway (degrade to today's behavior) rather than hide it. */
85
+ declare function claimReply(cfg: WireConfig, messageId: string, holder: string): Promise<boolean>;
86
+ /** Latest ackable cursor from a batch of rows (rows arrive oldest-first). */
87
+ declare function lastDeliveryId(rows: SyncRow[]): string | null;
88
+
89
+ declare const DEFAULT_API_BASE = "https://api.agentchat.me";
90
+ declare const CredentialsSchema: z.ZodObject<{
91
+ api_key: z.ZodString;
92
+ handle: z.ZodString;
93
+ api_base: z.ZodOptional<z.ZodString>;
94
+ created_at: z.ZodOptional<z.ZodString>;
95
+ }, "strip", z.ZodTypeAny, {
96
+ handle: string;
97
+ api_key: string;
98
+ created_at?: string | undefined;
99
+ api_base?: string | undefined;
100
+ }, {
101
+ handle: string;
102
+ api_key: string;
103
+ created_at?: string | undefined;
104
+ api_base?: string | undefined;
105
+ }>;
106
+ type Credentials = z.infer<typeof CredentialsSchema>;
107
+ interface ResolvedIdentity {
108
+ apiKey: string;
109
+ apiBase: string;
110
+ /** Handle is only known when it came from the credentials file. */
111
+ handle: string | null;
112
+ source: 'env' | 'file';
113
+ }
114
+ declare function credentialsPath(home: string): string;
115
+ declare function pendingPath(home: string): string;
116
+ declare function statePath(home: string): string;
117
+ /** Read the credential stored in `home`, or null when absent/malformed. */
118
+ declare function readCredentials(home: string): Credentials | null;
119
+ /**
120
+ * The identity an integration should act as, for one specific home.
121
+ * `AGENTCHAT_API_KEY` overrides the file; `AGENTCHAT_API_BASE` overrides the
122
+ * base URL. Neither is host-specific, so both are read from the environment.
123
+ */
124
+ declare function resolveIdentity(home: string): ResolvedIdentity | null;
125
+ declare function writeCredentials(home: string, creds: Credentials): void;
126
+ /** Delete the credential and any half-finished registration in `home`.
127
+ * Touches nothing outside `home` — that is the caller's whole agent. */
128
+ declare function clearCredentials(home: string): boolean;
129
+ declare const PendingSchema: z.ZodObject<{
130
+ kind: z.ZodDefault<z.ZodEnum<["register", "recover"]>>;
131
+ pending_id: z.ZodString;
132
+ email: z.ZodString;
133
+ handle: z.ZodOptional<z.ZodString>;
134
+ api_base: z.ZodOptional<z.ZodString>;
135
+ created_at: z.ZodString;
136
+ }, "strip", z.ZodTypeAny, {
137
+ created_at: string;
138
+ kind: "register" | "recover";
139
+ pending_id: string;
140
+ email: string;
141
+ handle?: string | undefined;
142
+ api_base?: string | undefined;
143
+ }, {
144
+ created_at: string;
145
+ pending_id: string;
146
+ email: string;
147
+ kind?: "register" | "recover" | undefined;
148
+ handle?: string | undefined;
149
+ api_base?: string | undefined;
150
+ }>;
151
+ type PendingRegistration = z.infer<typeof PendingSchema>;
152
+ declare function readPending(home: string): PendingRegistration | null;
153
+ declare function writePending(home: string, pending: PendingRegistration): void;
154
+ declare function clearPending(home: string): void;
155
+
156
+ declare const StateSchema: z.ZodObject<{
157
+ sessions: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
158
+ continuations: z.ZodNumber;
159
+ updated_at: z.ZodString;
160
+ pending_ack: z.ZodOptional<z.ZodString>;
161
+ }, "strip", z.ZodTypeAny, {
162
+ continuations: number;
163
+ updated_at: string;
164
+ pending_ack?: string | undefined;
165
+ }, {
166
+ continuations: number;
167
+ updated_at: string;
168
+ pending_ack?: string | undefined;
169
+ }>>>;
170
+ last_offer_at: z.ZodOptional<z.ZodString>;
171
+ }, "strip", z.ZodTypeAny, {
172
+ sessions: Record<string, {
173
+ continuations: number;
174
+ updated_at: string;
175
+ pending_ack?: string | undefined;
176
+ }>;
177
+ last_offer_at?: string | undefined;
178
+ }, {
179
+ sessions?: Record<string, {
180
+ continuations: number;
181
+ updated_at: string;
182
+ pending_ack?: string | undefined;
183
+ }> | undefined;
184
+ last_offer_at?: string | undefined;
185
+ }>;
186
+ type HookState = z.infer<typeof StateSchema>;
187
+ declare function readState(home: string): HookState;
188
+ declare function writeState(home: string, state: HookState): void;
189
+ declare function getContinuations(home: string, sessionKey: string): number;
190
+ declare function recordContinuation(home: string, sessionKey: string, now?: Date): number;
191
+ /**
192
+ * Give a session a fresh continuation budget. Called by the session-start
193
+ * hook: resuming a capped session is a new sitting, and its stop hook
194
+ * should be allowed to pick messages up again.
195
+ */
196
+ declare function resetSession(home: string, sessionKey: string): void;
197
+ declare function setPendingAck(home: string, sessionKey: string, cursor: string, now?: Date): void;
198
+ /** Read-and-clear the pending cursor for a session (user-prompt hook). */
199
+ declare function takePendingAck(home: string, sessionKey: string, now?: Date): string | null;
200
+ declare function shouldOfferRegistration(home: string, now?: Date): boolean;
201
+ declare function recordRegistrationOffer(home: string, now?: Date): void;
202
+
203
+ declare function formatSessionStart(handle: string | null, rows: SyncRow[]): string;
204
+ declare function formatStopPickup(handle: string | null, rows: SyncRow[]): string;
205
+ /**
206
+ * Injected at session start when always-on was set up but the daemon isn't
207
+ * beating (its heartbeat is stale — see alwaysOnHealth). Written in the FIRST
208
+ * person because the agent relays it to its user, and deliberately careful not
209
+ * to imply loss: messages that arrive while away queue for the next session,
210
+ * they don't vanish. The one-line fix is inline so the agent can act on it.
211
+ */
212
+ declare function formatAlwaysOnDown(copy: HostCopy): string;
213
+ /**
214
+ * How ONE integration names itself in user-facing copy.
215
+ *
216
+ * There is deliberately no `--platform` anywhere in this module. An
217
+ * integration's CLI acts on exactly one agent — its own — so a flag naming
218
+ * which agent to act on has nothing to select between. Removing the flag is
219
+ * what makes the wrong-agent mistake unrepresentable rather than merely
220
+ * guarded against.
221
+ */
222
+ interface HostCopy {
223
+ /** Exactly what the user types, e.g. `npx -y @agentchatme/codex`, or
224
+ * `node "/abs/path/to/bin/agentchat"` for a plugin-shipped bundle. */
225
+ invoke: string;
226
+ /** Human label for the host, e.g. `Codex` or `Claude Code`. */
227
+ label: string;
228
+ }
229
+ declare function formatRegistrationOffer(copy: HostCopy): string;
230
+
231
+ declare const ANCHOR_START = "<!-- agentchat:start -->";
232
+ declare const ANCHOR_END = "<!-- agentchat:end -->";
233
+ /** The default identity block. Integrations whose host wants richer copy
234
+ * (Codex folds etiquette in, since its skills are on-demand) render their
235
+ * own and pass it to `writeAnchor` instead. */
236
+ declare function renderAnchorBlock(handle: string): string;
237
+ type AnchorAction = 'written' | 'removed' | 'noop';
238
+ /**
239
+ * Upsert `block` into the file at `filePath`, creating it if needed.
240
+ *
241
+ * Throws if `expectHandle` is given and does not appear in what was written —
242
+ * a silently-wrong identity block is worse than a loud failure, because the
243
+ * agent would spend every session announcing an address that isn't its own.
244
+ */
245
+ declare function writeAnchor(filePath: string, block: string, expectHandle?: string): AnchorAction;
246
+ declare function removeAnchorAt(filePath: string): AnchorAction;
247
+ declare function hasAnchorAt(filePath: string): boolean;
248
+ /**
249
+ * The handle a file's anchor block currently CLAIMS, or null if there is no
250
+ * block. Every anchor rendering states it as `**@handle**`, so one pattern
251
+ * covers them all.
252
+ *
253
+ * Lets an integration catch an anchor that disagrees with its own credential —
254
+ * an agent told it is @a while authenticating as @b hands peers an address
255
+ * that reaches someone else. Matched loosely (not against the canonical handle
256
+ * rule) precisely so a WRONG value is still read back and reported rather than
257
+ * silently treated as "no anchor".
258
+ */
259
+ declare function readAnchorHandleAt(filePath: string): string | null;
260
+ declare function readAnchorHandleFrom(text: string): string | null;
261
+ declare function upsertAnchorBlock(existing: string, block: string): string;
262
+ declare function stripAnchorBlock(existing: string): string;
263
+
264
+ interface HookInput {
265
+ sessionId: string;
266
+ /** Claude Code SessionStart source: startup | resume | clear | compact.
267
+ * Undefined on other hosts/events. */
268
+ source: string | undefined;
269
+ }
270
+ declare function readHookInput(stream?: NodeJS.ReadStream): Promise<HookInput>;
271
+
272
+ interface HookContext {
273
+ /** THIS integration's identity home. Never resolved here. */
274
+ home: string;
275
+ /** How this integration names itself in user-facing copy. */
276
+ copy: HostCopy;
277
+ }
278
+ interface SessionStartResult {
279
+ /** Text to inject into the session, or null for "say nothing". */
280
+ context: string | null;
281
+ }
282
+ interface StopResult {
283
+ /** Text to continue the session with, or null to let it stop. */
284
+ reason: string | null;
285
+ /**
286
+ * Commit the surfaced batch as delivered. Call this ONLY after the host has
287
+ * actually been given `reason` (invariant 3). Safe to call when `reason` is
288
+ * null — it is a no-op.
289
+ */
290
+ commit: () => Promise<void>;
291
+ }
292
+ declare function hooksDisabled(): boolean;
293
+ declare function sessionStart(ctx: HookContext, input: HookInput): Promise<SessionStartResult>;
294
+ /**
295
+ * A prompt is running, so the session is real — commit the digest batch that
296
+ * session-start injected. Silent in every outcome.
297
+ */
298
+ declare function userPrompt(ctx: HookContext, input: HookInput): Promise<void>;
299
+ declare function stop(ctx: HookContext, input: HookInput): Promise<StopResult>;
300
+
301
+ /**
302
+ * How one host wants hook output shaped. Each integration owns its own — every
303
+ * coding agent expects a different envelope, and a shared module choosing
304
+ * between them would be one more place to pick the wrong one.
305
+ */
306
+ interface HookDialect {
307
+ sessionStartOutput(context: string): Record<string, unknown>;
308
+ stopOutput(reason: string): Record<string, unknown>;
309
+ printJson(payload: Record<string, unknown>): void;
310
+ }
311
+ interface HookRunners {
312
+ runSessionStart(): Promise<void>;
313
+ runUserPrompt(): Promise<void>;
314
+ runStop(): Promise<void>;
315
+ }
316
+ /**
317
+ * Build the three hook entrypoints for one coding agent.
318
+ *
319
+ * `context` is a factory, not a value: the hooks run in a fresh process where
320
+ * the host's env (`CODEX_HOME` and friends) must be read at call time.
321
+ */
322
+ declare function createHookRunners(context: () => HookContext, dialect: HookDialect): HookRunners;
323
+
324
+ type Verdict = 'PASS' | 'WARN' | 'FAIL';
325
+ interface DoctorCheck {
326
+ name: string;
327
+ verdict: Verdict;
328
+ detail: string;
329
+ }
330
+ /**
331
+ * Everything the shared identity commands need to know about ONE coding agent.
332
+ *
333
+ * This is the seam. The register / login / recover / status / logout / doctor
334
+ * flows are the same everywhere because they are a contract with the AgentChat
335
+ * server; what differs per host is only what is described here. An integration
336
+ * builds one of these and gets the commands back.
337
+ *
338
+ * Note what is NOT here: any way to name a *different* host. A profile
339
+ * describes the caller and nothing else, so a command built from it cannot
340
+ * reach another agent's files. That is the same property the per-repo split
341
+ * gives, kept intact while the flow itself is shared once.
342
+ *
343
+ * The path-ish fields are functions, not strings: `CODEX_HOME` and friends are
344
+ * read per call, and evaluating them at module load would freeze a value the
345
+ * user can still change.
346
+ */
347
+ interface HostProfile {
348
+ /** Human name for output, e.g. `Claude Code`. */
349
+ label: string;
350
+ /** Stable machine identifier for `--json`, e.g. `claude-code`. */
351
+ id: string;
352
+ /** THE identity home for this agent. */
353
+ home(): string;
354
+ /** This host's always-loaded instruction file. */
355
+ anchorFile(): string;
356
+ /** Exactly what a user types to reach this integration. */
357
+ invocation(): string;
358
+ /** Render the anchor block body this host wants. */
359
+ renderAnchor(handle: string): string;
360
+ /**
361
+ * What to call the anchor in output. Defaults to the instruction file's own
362
+ * basename (`CLAUDE.md`, `AGENTS.md`), which is what a user actually looks
363
+ * for on disk.
364
+ */
365
+ anchorLabel?: string;
366
+ /**
367
+ * Whether this host is wired up enough for an anchor to mean anything.
368
+ * A host that must edit config files first (Codex) uses this so it never
369
+ * writes an identity block announcing a phone number with nothing to answer
370
+ * it. Hosts wired by their own installer (a Claude Code plugin) omit it.
371
+ */
372
+ isWired?(): boolean;
373
+ /** Extra teardown on logout; returns descriptions of what was removed. */
374
+ removeWiring?(): string[];
375
+ /** Host-specific doctor checks, appended after the shared ones. */
376
+ extraDoctorChecks?(): DoctorCheck[];
377
+ /** Extra lines printed after a successful logout. */
378
+ logoutHints?(): string[];
379
+ }
380
+ /** The label to use for this host's anchor in user-facing output. */
381
+ declare function anchorLabelOf(profile: HostProfile): string;
382
+
383
+ interface RegisterOpts {
384
+ email?: string;
385
+ handle?: string;
386
+ displayName?: string;
387
+ description?: string;
388
+ code?: string;
389
+ apiBase?: string;
390
+ }
391
+ interface DoctorOpts {
392
+ fix?: boolean;
393
+ }
394
+ interface IdentityCommands {
395
+ runRegister(opts: RegisterOpts): Promise<number>;
396
+ runLogin(opts: {
397
+ apiKey?: string;
398
+ apiBase?: string;
399
+ }): Promise<number>;
400
+ runRecover(opts: {
401
+ email?: string;
402
+ code?: string;
403
+ apiBase?: string;
404
+ }): Promise<number>;
405
+ runStatus(opts: {
406
+ json?: boolean;
407
+ }): Promise<number>;
408
+ runLogout(): number;
409
+ runDoctor(opts?: DoctorOpts): Promise<number>;
410
+ }
411
+ /** Build the identity command set for one coding agent. */
412
+ declare function createIdentityCommands(profile: HostProfile): IdentityCommands;
413
+
414
+ declare const HEARTBEAT_FILE = "daemon.heartbeat";
415
+ /** Record that the user wants always-on for this agent. */
416
+ declare function markAlwaysOnWanted(home: string): void;
417
+ /** Forget the intent (user chose session-only, or uninstalled). */
418
+ declare function clearAlwaysOnWanted(home: string): void;
419
+ declare function alwaysOnWanted(home: string): boolean;
420
+ /** Touch the liveness beacon. Called by the running daemon. */
421
+ declare function beat(home: string): void;
422
+ /**
423
+ * The health the session-start hook acts on. `wanted:false` means the user
424
+ * never opted into always-on (or turned it off) → the hook stays silent.
425
+ * `wanted:true, healthy:false` means always-on was set up but the daemon isn't
426
+ * beating → the hook warns. Pure reads (two stats), no subprocess, never throws.
427
+ */
428
+ declare function alwaysOnHealth(home: string): {
429
+ wanted: boolean;
430
+ healthy: boolean;
431
+ };
432
+
433
+ interface LockHandle {
434
+ release(): void;
435
+ }
436
+ declare function acquireLeaderLock(home: string): LockHandle | null;
437
+
438
+ /**
439
+ * Enough to ADDRESS an already-installed service. Removing one or reading its
440
+ * status needs only its name — deliberately a narrower type than install, so
441
+ * `uninstall` and `status` cannot be made to depend on a daemon entry they have
442
+ * no use for.
443
+ */
444
+ interface ServiceRef {
445
+ /** Unit/service name, e.g. `agentchatd-codex`. Must be unique per integration. */
446
+ label: string;
447
+ home: string;
448
+ }
449
+ interface ServiceOpts extends ServiceRef {
450
+ /**
451
+ * Absolute path to the integration's DAEMON bundle — the script this service
452
+ * runs. Required, and deliberately not defaulted.
453
+ *
454
+ * It used to default to `process.argv[1]`, i.e. whichever binary happened to
455
+ * call `install`. That was the CLI, which has no daemon in it, so every
456
+ * installed unit ran a command that exited 1 and then restart-looped forever
457
+ * under `KeepAlive`/`Restart=on-failure` while the CLI cheerfully reported
458
+ * "Always-on is ON". Making the caller name the entry is what stops a service
459
+ * from being installed against a script that cannot serve it.
460
+ *
461
+ * Must also be a STABLE path. A plugin's own bundle lives in a
462
+ * version-scoped cache directory that the next upgrade deletes, so an
463
+ * integration distributed that way copies itself somewhere durable first and
464
+ * passes that copy here.
465
+ */
466
+ entry: string;
467
+ /** Host-specific env the service must see because a systemd/launchd service
468
+ * does NOT inherit the login shell (e.g. CODEX_HOME). PATH is captured
469
+ * automatically. */
470
+ env?: Record<string, string>;
471
+ }
472
+ interface Plan {
473
+ label: string;
474
+ node: string;
475
+ bin: string;
476
+ home: string;
477
+ /** Host env captured from the current shell so the service sees the same
478
+ * CODEX_HOME / CLAUDE_CONFIG_DIR the user installed under. */
479
+ env: Record<string, string>;
480
+ }
481
+ declare function planForTest(opts: ServiceOpts): Plan;
482
+ declare function installService(opts: ServiceOpts): void;
483
+ declare function uninstallService(opts: ServiceRef): void;
484
+ declare function serviceStatus(opts: ServiceRef): string;
485
+
486
+ declare const log: {
487
+ error: (msg: string) => void;
488
+ warn: (msg: string) => void;
489
+ info: (msg: string) => void;
490
+ debug: (msg: string) => void;
491
+ };
492
+
493
+ /** Coarse, human-facing age of a duration in ms. Never negative (a slightly
494
+ * future timestamp from clock skew reads as "just now"). */
495
+ declare function relativeAge(ms: number): string;
496
+ /** "2026-07-24 14:32 UTC" — minute precision, timezone stated explicitly so an
497
+ * agent never has to guess. `t` is an epoch-ms instant, so this is pure. */
498
+ declare function absoluteUtc(t: number): string;
499
+ /** Relative age only, e.g. "3 minutes ago". Empty string if `createdAt` is
500
+ * missing or unparseable — callers append it conditionally so a bad/absent
501
+ * timestamp degrades to today's timestamp-less line rather than "time unknown"
502
+ * noise in a scan-list digest. */
503
+ declare function relativeWhen(createdAt: string | undefined, now?: number): string;
504
+ /** Relative age + absolute UTC, e.g. "3 minutes ago (2026-07-24 14:32 UTC)".
505
+ * Falls back to "at an unknown time" when the stamp is missing/unparseable so
506
+ * a single-message wake still reads as a sentence. */
507
+ declare function formatWhen(createdAt: string | undefined, now?: number): string;
508
+
509
+ declare function atomicWriteFile(filePath: string, data: string, mode?: number): void;
510
+ declare function readJsonFile<T>(filePath: string): T | null;
511
+
512
+ export { ANCHOR_END, ANCHOR_START, 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 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, alwaysOnWanted, anchorLabelOf, atomicWriteFile, beat, claimReply, clearAlwaysOnWanted, clearCredentials, clearPending, clearSessionActive, contextOf, createHookRunners, createIdentityCommands, credentialsPath, formatAlwaysOnDown, formatRegistrationOffer, formatSessionStart, formatStopPickup, formatWhen, getContinuations, getMeLite, hasAnchorAt, hooksDisabled, installService, lastDeliveryId, log, markAlwaysOnWanted, markSessionActive, pendingPath, planForTest, readAnchorHandleAt, readAnchorHandleFrom, readCredentials, readHookInput, readJsonFile, readPending, readState, recordContinuation, recordRegistrationOffer, relativeAge, relativeWhen, removeAnchorAt, renderAnchorBlock, resetSession, resolveIdentity, serviceStatus, sessionStart, setPendingAck, shouldOfferRegistration, statePath, stop, stripAnchorBlock, syncAck, syncPeek, takePendingAck, uninstallService, upsertAnchorBlock, userPrompt, writeAnchor, writeCredentials, writePending, writeState };