@agentchatme/agent-core 0.0.131 → 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-XLRPNQ7G.js → chunk-ER4AFPH7.js} +18 -1
- 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 +12 -1
- package/dist/index.js +32 -8
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-XLRPNQ7G.js.map +0 -1
package/dist/daemon-entry.js
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;
|
|
@@ -603,4 +614,4 @@ declare function formatWhen(createdAt: string | undefined, now?: number): string
|
|
|
603
614
|
declare function atomicWriteFile(filePath: string, data: string, mode?: number): void;
|
|
604
615
|
declare function readJsonFile<T>(filePath: string): T | null;
|
|
605
616
|
|
|
606
|
-
export { ANCHOR_END, ANCHOR_START, type AlwaysOnState, type AnchorAction, type Credentials, DEFAULT_API_BASE, type DoctorCheck, type DoctorOpts, HEARTBEAT_FILE, type HookContext, type HookDialect, type HookInput, type HookRunners, type HookState, type HostCopy, type HostProfile, type IdentityCommands, type LockHandle, type ManualCopy, type MessageContext, type PendingRegistration, type RegisterOpts, type ResolvedIdentity, type ServiceOpts, type ServiceRef, type SessionStartResult, type StopResult, type SyncRow, type Verdict, type WireConfig, WireError, absoluteUtc, acquireLeaderLock, alwaysOnHealth, alwaysOnOptedOut, alwaysOnState, alwaysOnWanted, anchorLabelOf, atomicWriteFile, beat, claimReply, clearAlwaysOnOptOut, clearAlwaysOnWanted, clearCredentials, clearOfferDeclined, clearPending, clearSessionActive, contextOf, createHookRunners, createIdentityCommands, credentialsPath, formatAlwaysOnDown, formatRegistrationOffer, formatSessionStart, formatStopPickup, formatWhen, getContinuations, getMeLite, hasAnchorAt, hooksDisabled, idle, installService, lastDeliveryId, log, markAlwaysOnOptOut, markAlwaysOnWanted, markSessionActive, offerDeclined, pendingPath, planForTest, readAnchorHandleAt, readAnchorHandleFrom, readCredentials, readHookInput, readJsonFile, readPending, readState, recordContinuation, recordOfferDeclined, recordRegistrationOffer, relativeAge, relativeWhen, removeAnchorAt, renderAnchorBlock, renderDeclinedBlock, renderManual, renderUnregisteredBlock, resetSession, resolveIdentity, serviceStatus, sessionStart, setPendingAck, shouldOfferRegistration, statePath, stop, stripAnchorBlock, syncAck, syncPeek, takePendingAck, uninstallService, upsertAnchorBlock, userPrompt, writeAnchor, writeCredentials, writePending, writeState };
|
|
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;
|
|
@@ -733,7 +736,8 @@ function createIdentityCommands(profile) {
|
|
|
733
736
|
}
|
|
734
737
|
try {
|
|
735
738
|
const result = await AgentChatClient.verify(pending.pending_id, code, {
|
|
736
|
-
baseUrl: pending.api_base ?? apiBase
|
|
739
|
+
baseUrl: pending.api_base ?? apiBase,
|
|
740
|
+
clientIdentity: CODING_AGENTS_CLIENT_IDENTITY
|
|
737
741
|
});
|
|
738
742
|
writeCredentials(home, {
|
|
739
743
|
api_key: result.apiKey,
|
|
@@ -806,7 +810,8 @@ function createIdentityCommands(profile) {
|
|
|
806
810
|
handle,
|
|
807
811
|
...opts.displayName ? { display_name: opts.displayName } : {},
|
|
808
812
|
...opts.description ? { description: opts.description } : {},
|
|
809
|
-
baseUrl: apiBase
|
|
813
|
+
baseUrl: apiBase,
|
|
814
|
+
clientIdentity: CODING_AGENTS_CLIENT_IDENTITY
|
|
810
815
|
});
|
|
811
816
|
writePending(home, {
|
|
812
817
|
kind: "register",
|
|
@@ -844,7 +849,11 @@ function createIdentityCommands(profile) {
|
|
|
844
849
|
return 1;
|
|
845
850
|
}
|
|
846
851
|
try {
|
|
847
|
-
const client = new AgentChatClient({
|
|
852
|
+
const client = new AgentChatClient({
|
|
853
|
+
apiKey,
|
|
854
|
+
baseUrl: apiBase,
|
|
855
|
+
clientIdentity: CODING_AGENTS_CLIENT_IDENTITY
|
|
856
|
+
});
|
|
848
857
|
const me = await client.getMe();
|
|
849
858
|
writeCredentials(home, {
|
|
850
859
|
api_key: apiKey,
|
|
@@ -876,7 +885,8 @@ function createIdentityCommands(profile) {
|
|
|
876
885
|
}
|
|
877
886
|
try {
|
|
878
887
|
const result = await AgentChatClient.recoverVerify(pending.pending_id, code, {
|
|
879
|
-
baseUrl: pending.api_base ?? apiBase
|
|
888
|
+
baseUrl: pending.api_base ?? apiBase,
|
|
889
|
+
clientIdentity: CODING_AGENTS_CLIENT_IDENTITY
|
|
880
890
|
});
|
|
881
891
|
writeCredentials(home, {
|
|
882
892
|
api_key: result.apiKey,
|
|
@@ -912,7 +922,10 @@ function createIdentityCommands(profile) {
|
|
|
912
922
|
return 1;
|
|
913
923
|
}
|
|
914
924
|
try {
|
|
915
|
-
const result = await AgentChatClient.recover(email, {
|
|
925
|
+
const result = await AgentChatClient.recover(email, {
|
|
926
|
+
baseUrl: apiBase,
|
|
927
|
+
clientIdentity: CODING_AGENTS_CLIENT_IDENTITY
|
|
928
|
+
});
|
|
916
929
|
if (!result.pending_id) {
|
|
917
930
|
console.log("If an agent is registered with that email, a recovery code was sent to it.");
|
|
918
931
|
return 0;
|
|
@@ -961,7 +974,11 @@ function createIdentityCommands(profile) {
|
|
|
961
974
|
return 0;
|
|
962
975
|
}
|
|
963
976
|
try {
|
|
964
|
-
const client = new AgentChatClient({
|
|
977
|
+
const client = new AgentChatClient({
|
|
978
|
+
apiKey: identity.apiKey,
|
|
979
|
+
baseUrl: identity.apiBase,
|
|
980
|
+
clientIdentity: CODING_AGENTS_CLIENT_IDENTITY
|
|
981
|
+
});
|
|
965
982
|
const me = await client.getMe();
|
|
966
983
|
const rows = await syncPeek({ apiKey: identity.apiKey, apiBase: identity.apiBase }, { limit: 100 });
|
|
967
984
|
const unread = rows.length === 100 ? "100+" : String(rows.length);
|
|
@@ -1048,7 +1065,11 @@ function createIdentityCommands(profile) {
|
|
|
1048
1065
|
const identity = resolveIdentity(home);
|
|
1049
1066
|
if (identity !== null) {
|
|
1050
1067
|
try {
|
|
1051
|
-
const client = new AgentChatClient({
|
|
1068
|
+
const client = new AgentChatClient({
|
|
1069
|
+
apiKey: identity.apiKey,
|
|
1070
|
+
baseUrl: identity.apiBase,
|
|
1071
|
+
clientIdentity: CODING_AGENTS_CLIENT_IDENTITY
|
|
1072
|
+
});
|
|
1052
1073
|
const started = Date.now();
|
|
1053
1074
|
const me = await client.getMe();
|
|
1054
1075
|
const verdict = (me.status ?? "active") === "active" ? "PASS" : "WARN";
|
|
@@ -1543,8 +1564,11 @@ function serviceStatus(opts) {
|
|
|
1543
1564
|
export {
|
|
1544
1565
|
ANCHOR_END,
|
|
1545
1566
|
ANCHOR_START,
|
|
1567
|
+
CODING_AGENTS_CLIENT_HEADERS,
|
|
1568
|
+
CODING_AGENTS_CLIENT_IDENTITY,
|
|
1546
1569
|
DEFAULT_API_BASE,
|
|
1547
1570
|
HEARTBEAT_FILE,
|
|
1571
|
+
VERSION,
|
|
1548
1572
|
WireError,
|
|
1549
1573
|
absoluteUtc,
|
|
1550
1574
|
acquireLeaderLock,
|