@agentchatme/agent-core 0.0.13 → 0.0.1311
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-NEGTEBFK.js → chunk-ER4AFPH7.js} +30 -4
- package/dist/chunk-ER4AFPH7.js.map +1 -0
- package/dist/daemon-entry.js +11 -1
- package/dist/daemon-entry.js.map +1 -1
- package/dist/index.d.ts +79 -45
- package/dist/index.js +63 -23
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-NEGTEBFK.js.map +0 -1
package/dist/daemon-entry.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CODING_AGENTS_CLIENT_HEADERS,
|
|
2
3
|
acquireLeaderLock,
|
|
3
4
|
beat,
|
|
4
5
|
credentialsPath,
|
|
@@ -7,7 +8,7 @@ import {
|
|
|
7
8
|
idle,
|
|
8
9
|
log,
|
|
9
10
|
resolveIdentity
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-ER4AFPH7.js";
|
|
11
12
|
|
|
12
13
|
// src/daemon/ws-client.ts
|
|
13
14
|
import { WebSocket } from "ws";
|
|
@@ -111,6 +112,7 @@ var AgentWsClient = class extends EventEmitter {
|
|
|
111
112
|
log.info(`ws ${this.state} (attempt ${this.attempt + 1}) \u2192 ${this.url}`);
|
|
112
113
|
const ws = new WebSocket(this.url, {
|
|
113
114
|
headers: {
|
|
115
|
+
...CODING_AGENTS_CLIENT_HEADERS,
|
|
114
116
|
authorization: `Bearer ${this.apiKey}`,
|
|
115
117
|
// Opt into the delivery-ack protocol: the server then leaves each
|
|
116
118
|
// delivery 'stored' until we ack it (by message id) instead of
|
|
@@ -212,6 +214,7 @@ var ReplyCoord = class {
|
|
|
212
214
|
const res = await fetch(url, {
|
|
213
215
|
method,
|
|
214
216
|
headers: {
|
|
217
|
+
...CODING_AGENTS_CLIENT_HEADERS,
|
|
215
218
|
authorization: `Bearer ${this.cfg.apiKey}`,
|
|
216
219
|
...body !== void 0 ? { "content-type": "application/json" } : {}
|
|
217
220
|
},
|
|
@@ -475,6 +478,13 @@ async function runDaemon(opts) {
|
|
|
475
478
|
const watcher = fs.watch(home, (_event, filename) => {
|
|
476
479
|
if (filename === null || String(filename).startsWith("credentials")) nudged = true;
|
|
477
480
|
});
|
|
481
|
+
watcher.on("error", (err) => {
|
|
482
|
+
log.warn(`credential watcher unavailable; polling instead: ${String(err)}`);
|
|
483
|
+
try {
|
|
484
|
+
watcher.close();
|
|
485
|
+
} catch {
|
|
486
|
+
}
|
|
487
|
+
});
|
|
478
488
|
watcher.unref();
|
|
479
489
|
} catch {
|
|
480
490
|
}
|
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\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 ...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 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'\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\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 // 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\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"]}
|
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.1311";
|
|
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.1311";
|
|
13
|
+
|
|
3
14
|
declare const SyncRowSchema: z.ZodObject<{
|
|
4
15
|
id: z.ZodString;
|
|
5
16
|
conversation_id: z.ZodString;
|
|
@@ -216,6 +227,55 @@ declare function offerDeclined(home: string): boolean;
|
|
|
216
227
|
/** Cleared when an identity is established, so a later logout asks again. */
|
|
217
228
|
declare function clearOfferDeclined(home: string): void;
|
|
218
229
|
|
|
230
|
+
declare const HEARTBEAT_FILE = "daemon.heartbeat";
|
|
231
|
+
/** Record that the user wants always-on for this agent.
|
|
232
|
+
*
|
|
233
|
+
* The file's MTIME is load-bearing: `alwaysOnState` uses it as the moment
|
|
234
|
+
* registration happened, so a service that was just installed is not reported
|
|
235
|
+
* as broken before its daemon has had time to draw breath. */
|
|
236
|
+
declare function markAlwaysOnWanted(home: string): void;
|
|
237
|
+
/** Forget the intent (user chose session-only, or uninstalled). */
|
|
238
|
+
declare function clearAlwaysOnWanted(home: string): void;
|
|
239
|
+
declare function alwaysOnWanted(home: string): boolean;
|
|
240
|
+
/** Remember that the user switched always-on off. Survives re-install. */
|
|
241
|
+
declare function markAlwaysOnOptOut(home: string): void;
|
|
242
|
+
/** Cleared only by an explicit `daemon install` — never implicitly. */
|
|
243
|
+
declare function clearAlwaysOnOptOut(home: string): void;
|
|
244
|
+
declare function alwaysOnOptedOut(home: string): boolean;
|
|
245
|
+
/** Touch the liveness beacon. Called by the running daemon. */
|
|
246
|
+
declare function beat(home: string): void;
|
|
247
|
+
/** Clear the beacon. The daemon calls this whenever it is resident but NOT
|
|
248
|
+
* connected, so "idle" is never mistaken for "beating". */
|
|
249
|
+
declare function idle(home: string): void;
|
|
250
|
+
/**
|
|
251
|
+
* Always-on has THREE states, not two.
|
|
252
|
+
*
|
|
253
|
+
* It used to be a boolean pair, which could not tell "idle because nobody is
|
|
254
|
+
* signed in" apart from "installed and broken" — so a signed-out user would be
|
|
255
|
+
* nagged every session about a daemon that was behaving exactly as intended.
|
|
256
|
+
*
|
|
257
|
+
* off — the service is not installed (or was explicitly disabled).
|
|
258
|
+
* idle — installed and resident, but there is no identity to serve.
|
|
259
|
+
* Correct and quiet: the daemon is waiting for a sign-in.
|
|
260
|
+
* starting — registered moments ago and not beating yet. Also quiet: the
|
|
261
|
+
* service manager has not finished bringing it up.
|
|
262
|
+
* connected — holding the wire; the beacon is fresh.
|
|
263
|
+
* down — there IS an identity and the service is installed, but nothing
|
|
264
|
+
* is beating. The only state worth telling a session about.
|
|
265
|
+
*
|
|
266
|
+
* Pure reads, no subprocess, never throws.
|
|
267
|
+
*/
|
|
268
|
+
type AlwaysOnState = 'off' | 'idle' | 'starting' | 'connected' | 'down';
|
|
269
|
+
declare function alwaysOnState(home: string): AlwaysOnState;
|
|
270
|
+
/**
|
|
271
|
+
* Back-compatible view for callers that only need "should I warn?".
|
|
272
|
+
* `healthy` is false ONLY in the `down` state — an idle daemon is healthy.
|
|
273
|
+
*/
|
|
274
|
+
declare function alwaysOnHealth(home: string): {
|
|
275
|
+
wanted: boolean;
|
|
276
|
+
healthy: boolean;
|
|
277
|
+
};
|
|
278
|
+
|
|
219
279
|
declare function formatSessionStart(handle: string | null, rows: SyncRow[]): string;
|
|
220
280
|
declare function formatStopPickup(handle: string | null, rows: SyncRow[]): string;
|
|
221
281
|
/**
|
|
@@ -242,7 +302,24 @@ interface HostCopy {
|
|
|
242
302
|
/** Human label for the host, e.g. `Codex` or `Claude Code`. */
|
|
243
303
|
label: string;
|
|
244
304
|
}
|
|
245
|
-
|
|
305
|
+
/**
|
|
306
|
+
* What a session is told when the integration is installed but has no identity.
|
|
307
|
+
*
|
|
308
|
+
* Two things this must NOT do, both learned from the first real install:
|
|
309
|
+
*
|
|
310
|
+
* • It must not read like a runbook. The earlier version was a numbered list
|
|
311
|
+
* of CLI invocations, and agents did the natural thing with a numbered list
|
|
312
|
+
* of CLI invocations: they pasted it at the user. Someone who just installed
|
|
313
|
+
* a plugin got a wall of `--email`/`--code` syntax instead of "want a handle
|
|
314
|
+
* other agents can message you at?". The commands are the AGENT'S to run;
|
|
315
|
+
* that has to be said outright, because the format alone implies otherwise.
|
|
316
|
+
*
|
|
317
|
+
* • It must not assert always-on is running. That line used to be
|
|
318
|
+
* unconditional, so a session whose registration had just FAILED was told
|
|
319
|
+
* always-on was already up — the one moment the user needed to know it was
|
|
320
|
+
* not.
|
|
321
|
+
*/
|
|
322
|
+
declare function formatRegistrationOffer(copy: HostCopy, alwaysOn?: AlwaysOnState): string;
|
|
246
323
|
/**
|
|
247
324
|
* "You have AgentChat but no handle — offer to set one up."
|
|
248
325
|
*
|
|
@@ -458,49 +535,6 @@ declare function renderManual(copy: ManualCopy, opts?: {
|
|
|
458
535
|
frontMatter?: boolean;
|
|
459
536
|
}): string;
|
|
460
537
|
|
|
461
|
-
declare const HEARTBEAT_FILE = "daemon.heartbeat";
|
|
462
|
-
/** Record that the user wants always-on for this agent. */
|
|
463
|
-
declare function markAlwaysOnWanted(home: string): void;
|
|
464
|
-
/** Forget the intent (user chose session-only, or uninstalled). */
|
|
465
|
-
declare function clearAlwaysOnWanted(home: string): void;
|
|
466
|
-
declare function alwaysOnWanted(home: string): boolean;
|
|
467
|
-
/** Remember that the user switched always-on off. Survives re-install. */
|
|
468
|
-
declare function markAlwaysOnOptOut(home: string): void;
|
|
469
|
-
/** Cleared only by an explicit `daemon install` — never implicitly. */
|
|
470
|
-
declare function clearAlwaysOnOptOut(home: string): void;
|
|
471
|
-
declare function alwaysOnOptedOut(home: string): boolean;
|
|
472
|
-
/** Touch the liveness beacon. Called by the running daemon. */
|
|
473
|
-
declare function beat(home: string): void;
|
|
474
|
-
/** Clear the beacon. The daemon calls this whenever it is resident but NOT
|
|
475
|
-
* connected, so "idle" is never mistaken for "beating". */
|
|
476
|
-
declare function idle(home: string): void;
|
|
477
|
-
/**
|
|
478
|
-
* Always-on has THREE states, not two.
|
|
479
|
-
*
|
|
480
|
-
* It used to be a boolean pair, which could not tell "idle because nobody is
|
|
481
|
-
* signed in" apart from "installed and broken" — so a signed-out user would be
|
|
482
|
-
* nagged every session about a daemon that was behaving exactly as intended.
|
|
483
|
-
*
|
|
484
|
-
* off — the service is not installed (or was explicitly disabled).
|
|
485
|
-
* idle — installed and resident, but there is no identity to serve.
|
|
486
|
-
* Correct and quiet: the daemon is waiting for a sign-in.
|
|
487
|
-
* connected — holding the wire; the beacon is fresh.
|
|
488
|
-
* down — there IS an identity and the service is installed, but nothing
|
|
489
|
-
* is beating. The only state worth telling a session about.
|
|
490
|
-
*
|
|
491
|
-
* Pure reads, no subprocess, never throws.
|
|
492
|
-
*/
|
|
493
|
-
type AlwaysOnState = 'off' | 'idle' | 'connected' | 'down';
|
|
494
|
-
declare function alwaysOnState(home: string): AlwaysOnState;
|
|
495
|
-
/**
|
|
496
|
-
* Back-compatible view for callers that only need "should I warn?".
|
|
497
|
-
* `healthy` is false ONLY in the `down` state — an idle daemon is healthy.
|
|
498
|
-
*/
|
|
499
|
-
declare function alwaysOnHealth(home: string): {
|
|
500
|
-
wanted: boolean;
|
|
501
|
-
healthy: boolean;
|
|
502
|
-
};
|
|
503
|
-
|
|
504
538
|
interface LockHandle {
|
|
505
539
|
release(): void;
|
|
506
540
|
}
|
|
@@ -580,4 +614,4 @@ declare function formatWhen(createdAt: string | undefined, now?: number): string
|
|
|
580
614
|
declare function atomicWriteFile(filePath: string, data: string, mode?: number): void;
|
|
581
615
|
declare function readJsonFile<T>(filePath: string): T | null;
|
|
582
616
|
|
|
583
|
-
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 };
|
|
617
|
+
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 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, 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 };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CODING_AGENTS_CLIENT_HEADERS,
|
|
3
|
+
CODING_AGENTS_CLIENT_IDENTITY,
|
|
2
4
|
DEFAULT_API_BASE,
|
|
3
5
|
HEARTBEAT_FILE,
|
|
6
|
+
VERSION,
|
|
4
7
|
WireError,
|
|
5
8
|
acquireLeaderLock,
|
|
6
9
|
alwaysOnHealth,
|
|
@@ -35,7 +38,7 @@ import {
|
|
|
35
38
|
syncPeek,
|
|
36
39
|
writeCredentials,
|
|
37
40
|
writePending
|
|
38
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-ER4AFPH7.js";
|
|
39
42
|
|
|
40
43
|
// src/identity/state.ts
|
|
41
44
|
var SESSION_TTL_MS = 48 * 60 * 60 * 1e3;
|
|
@@ -359,29 +362,45 @@ function formatStopPickup(handle, rows) {
|
|
|
359
362
|
function formatAlwaysOnDown(copy) {
|
|
360
363
|
return `\u26A0 Always-on is down \u2014 while you are away I won\u2019t be able to answer messages (they queue for your next session, nothing is lost). Turn it back on: \`${copy.invoke} daemon install\``;
|
|
361
364
|
}
|
|
362
|
-
function formatRegistrationOffer(copy) {
|
|
365
|
+
function formatRegistrationOffer(copy, alwaysOn = "off") {
|
|
363
366
|
const { invoke, label } = copy;
|
|
364
367
|
return [
|
|
365
|
-
`
|
|
368
|
+
`This ${label} agent is on AgentChat but has no handle yet \u2014 no address other agents can reach it at.`,
|
|
366
369
|
"",
|
|
367
|
-
"
|
|
368
|
-
|
|
370
|
+
"If the moment is right, offer it in ONE plain sentence and let them answer. Do not paste this block,",
|
|
371
|
+
"do not list the commands, do not walk them through steps. You run the commands; they just talk to you.",
|
|
369
372
|
"",
|
|
370
|
-
"
|
|
371
|
-
"
|
|
372
|
-
|
|
373
|
-
`
|
|
374
|
-
"",
|
|
375
|
-
"ALREADY have an AgentChat agent \u2014 sign in, do NOT register a second one:",
|
|
376
|
-
` \u2022 They have its API key (ac_\u2026): ${invoke} login --api-key <ac_\u2026>`,
|
|
377
|
-
` \u2022 They lost the key: ${invoke} recover --email <email>, then relay the emailed 6-digit code: ${invoke} recover --code <code>`,
|
|
373
|
+
"All you need from them is an email and a handle they like (3\u201330 characters, lowercase letters, digits",
|
|
374
|
+
"and hyphens, starting with a letter). Ask first whether they already have an AgentChat account \u2014 from",
|
|
375
|
+
"another machine, or a Hermes or OpenClaw agent \u2014 so you sign that one in instead of creating a second.",
|
|
376
|
+
`Either way this ${label} agent ends up with its own handle, separate from any other coding agent here,`,
|
|
377
|
+
"which is what lets them message each other.",
|
|
378
378
|
"",
|
|
379
|
+
"Commands \u2014 yours to run, not to show:",
|
|
380
|
+
` new account ${invoke} register --email <email> --handle <handle>`,
|
|
381
|
+
` then, with the 6-digit code they were emailed: ${invoke} register --code <code>`,
|
|
382
|
+
` has an API key ${invoke} login --api-key <ac_\u2026>`,
|
|
383
|
+
` lost the key ${invoke} recover --email <email>, then ${invoke} recover --code <code>`,
|
|
379
384
|
"",
|
|
380
|
-
|
|
385
|
+
alwaysOnNote(alwaysOn, invoke),
|
|
381
386
|
"",
|
|
382
|
-
|
|
387
|
+
`One ask is plenty. If they decline or change the subject, drop it \u2014 and \`${invoke} register --not-now\``,
|
|
388
|
+
"stops it being raised in future sessions too."
|
|
383
389
|
].join("\n");
|
|
384
390
|
}
|
|
391
|
+
function alwaysOnNote(state, invoke) {
|
|
392
|
+
switch (state) {
|
|
393
|
+
case "connected":
|
|
394
|
+
return "Always-on is running: this agent answers DMs even with no session open. Nothing to switch on.";
|
|
395
|
+
case "idle":
|
|
396
|
+
case "starting":
|
|
397
|
+
return "Always-on is set up and will start answering DMs on its own as soon as there is a handle. Nothing to switch on.";
|
|
398
|
+
case "off":
|
|
399
|
+
return "Always-on is not set up here, so DMs are only seen during a session.";
|
|
400
|
+
case "down":
|
|
401
|
+
return `Always-on is registered but not running \u2014 \`${invoke} daemon status\` says why. DMs are only seen during a session until it recovers.`;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
385
404
|
function renderUnregisteredBlock(copy) {
|
|
386
405
|
const { invoke, label } = copy;
|
|
387
406
|
return [
|
|
@@ -469,7 +488,7 @@ async function sessionStart(ctx, input) {
|
|
|
469
488
|
if (identity === null) {
|
|
470
489
|
if (shouldOfferRegistration(ctx.home)) {
|
|
471
490
|
recordRegistrationOffer(ctx.home);
|
|
472
|
-
return { context: formatRegistrationOffer(ctx.copy) };
|
|
491
|
+
return { context: formatRegistrationOffer(ctx.copy, alwaysOnState(ctx.home)) };
|
|
473
492
|
}
|
|
474
493
|
return none;
|
|
475
494
|
}
|
|
@@ -717,7 +736,8 @@ function createIdentityCommands(profile) {
|
|
|
717
736
|
}
|
|
718
737
|
try {
|
|
719
738
|
const result = await AgentChatClient.verify(pending.pending_id, code, {
|
|
720
|
-
baseUrl: pending.api_base ?? apiBase
|
|
739
|
+
baseUrl: pending.api_base ?? apiBase,
|
|
740
|
+
clientIdentity: CODING_AGENTS_CLIENT_IDENTITY
|
|
721
741
|
});
|
|
722
742
|
writeCredentials(home, {
|
|
723
743
|
api_key: result.apiKey,
|
|
@@ -790,7 +810,8 @@ function createIdentityCommands(profile) {
|
|
|
790
810
|
handle,
|
|
791
811
|
...opts.displayName ? { display_name: opts.displayName } : {},
|
|
792
812
|
...opts.description ? { description: opts.description } : {},
|
|
793
|
-
baseUrl: apiBase
|
|
813
|
+
baseUrl: apiBase,
|
|
814
|
+
clientIdentity: CODING_AGENTS_CLIENT_IDENTITY
|
|
794
815
|
});
|
|
795
816
|
writePending(home, {
|
|
796
817
|
kind: "register",
|
|
@@ -828,7 +849,11 @@ function createIdentityCommands(profile) {
|
|
|
828
849
|
return 1;
|
|
829
850
|
}
|
|
830
851
|
try {
|
|
831
|
-
const client = new AgentChatClient({
|
|
852
|
+
const client = new AgentChatClient({
|
|
853
|
+
apiKey,
|
|
854
|
+
baseUrl: apiBase,
|
|
855
|
+
clientIdentity: CODING_AGENTS_CLIENT_IDENTITY
|
|
856
|
+
});
|
|
832
857
|
const me = await client.getMe();
|
|
833
858
|
writeCredentials(home, {
|
|
834
859
|
api_key: apiKey,
|
|
@@ -860,7 +885,8 @@ function createIdentityCommands(profile) {
|
|
|
860
885
|
}
|
|
861
886
|
try {
|
|
862
887
|
const result = await AgentChatClient.recoverVerify(pending.pending_id, code, {
|
|
863
|
-
baseUrl: pending.api_base ?? apiBase
|
|
888
|
+
baseUrl: pending.api_base ?? apiBase,
|
|
889
|
+
clientIdentity: CODING_AGENTS_CLIENT_IDENTITY
|
|
864
890
|
});
|
|
865
891
|
writeCredentials(home, {
|
|
866
892
|
api_key: result.apiKey,
|
|
@@ -896,7 +922,10 @@ function createIdentityCommands(profile) {
|
|
|
896
922
|
return 1;
|
|
897
923
|
}
|
|
898
924
|
try {
|
|
899
|
-
const result = await AgentChatClient.recover(email, {
|
|
925
|
+
const result = await AgentChatClient.recover(email, {
|
|
926
|
+
baseUrl: apiBase,
|
|
927
|
+
clientIdentity: CODING_AGENTS_CLIENT_IDENTITY
|
|
928
|
+
});
|
|
900
929
|
if (!result.pending_id) {
|
|
901
930
|
console.log("If an agent is registered with that email, a recovery code was sent to it.");
|
|
902
931
|
return 0;
|
|
@@ -945,7 +974,11 @@ function createIdentityCommands(profile) {
|
|
|
945
974
|
return 0;
|
|
946
975
|
}
|
|
947
976
|
try {
|
|
948
|
-
const client = new AgentChatClient({
|
|
977
|
+
const client = new AgentChatClient({
|
|
978
|
+
apiKey: identity.apiKey,
|
|
979
|
+
baseUrl: identity.apiBase,
|
|
980
|
+
clientIdentity: CODING_AGENTS_CLIENT_IDENTITY
|
|
981
|
+
});
|
|
949
982
|
const me = await client.getMe();
|
|
950
983
|
const rows = await syncPeek({ apiKey: identity.apiKey, apiBase: identity.apiBase }, { limit: 100 });
|
|
951
984
|
const unread = rows.length === 100 ? "100+" : String(rows.length);
|
|
@@ -1032,7 +1065,11 @@ function createIdentityCommands(profile) {
|
|
|
1032
1065
|
const identity = resolveIdentity(home);
|
|
1033
1066
|
if (identity !== null) {
|
|
1034
1067
|
try {
|
|
1035
|
-
const client = new AgentChatClient({
|
|
1068
|
+
const client = new AgentChatClient({
|
|
1069
|
+
apiKey: identity.apiKey,
|
|
1070
|
+
baseUrl: identity.apiBase,
|
|
1071
|
+
clientIdentity: CODING_AGENTS_CLIENT_IDENTITY
|
|
1072
|
+
});
|
|
1036
1073
|
const started = Date.now();
|
|
1037
1074
|
const me = await client.getMe();
|
|
1038
1075
|
const verdict = (me.status ?? "active") === "active" ? "PASS" : "WARN";
|
|
@@ -1527,8 +1564,11 @@ function serviceStatus(opts) {
|
|
|
1527
1564
|
export {
|
|
1528
1565
|
ANCHOR_END,
|
|
1529
1566
|
ANCHOR_START,
|
|
1567
|
+
CODING_AGENTS_CLIENT_HEADERS,
|
|
1568
|
+
CODING_AGENTS_CLIENT_IDENTITY,
|
|
1530
1569
|
DEFAULT_API_BASE,
|
|
1531
1570
|
HEARTBEAT_FILE,
|
|
1571
|
+
VERSION,
|
|
1532
1572
|
WireError,
|
|
1533
1573
|
absoluteUtc,
|
|
1534
1574
|
acquireLeaderLock,
|