@agentchatme/agent-core 0.0.1 → 0.0.12
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-4UOOWYCI.js → chunk-NEGTEBFK.js} +38 -5
- package/dist/{chunk-4UOOWYCI.js.map → chunk-NEGTEBFK.js.map} +1 -1
- package/dist/daemon-entry.d.ts +10 -5
- package/dist/daemon-entry.js +98 -27
- package/dist/daemon-entry.js.map +1 -1
- package/dist/index.d.ts +61 -5
- package/dist/index.js +171 -97
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/daemon-entry.d.ts
CHANGED
|
@@ -145,7 +145,7 @@ declare function describeConversation(ctx: TurnContext): string;
|
|
|
145
145
|
declare function describeSender(ctx: TurnContext): string;
|
|
146
146
|
|
|
147
147
|
interface RunDaemonOpts {
|
|
148
|
-
/** THE identity home for the agent this daemon
|
|
148
|
+
/** THE identity home for the agent this daemon serves. */
|
|
149
149
|
home: string;
|
|
150
150
|
/** How to spawn one headless turn of this integration's coding agent. */
|
|
151
151
|
adapter: RuntimeAdapter;
|
|
@@ -153,9 +153,8 @@ interface RunDaemonOpts {
|
|
|
153
153
|
workdir?: string;
|
|
154
154
|
}
|
|
155
155
|
/**
|
|
156
|
-
* Run the always-on daemon.
|
|
157
|
-
*
|
|
158
|
-
* lock, the runtime is not usable).
|
|
156
|
+
* Run the always-on daemon. Returns only on a condition that makes running
|
|
157
|
+
* pointless — namely another daemon already holding this home's lock.
|
|
159
158
|
*/
|
|
160
159
|
declare function runDaemon(opts: RunDaemonOpts): Promise<number>;
|
|
161
160
|
|
|
@@ -190,6 +189,9 @@ declare function resolveDaemonConfig(opts: ResolveDaemonOpts): Promise<DaemonCon
|
|
|
190
189
|
declare class Daemon {
|
|
191
190
|
private readonly cfg;
|
|
192
191
|
private readonly adapter;
|
|
192
|
+
/** Called when the socket gives up for good (auth refused). The supervisor
|
|
193
|
+
* above decides what happens next — this class does not end the process. */
|
|
194
|
+
private readonly onTerminal?;
|
|
193
195
|
private readonly ws;
|
|
194
196
|
private readonly coord;
|
|
195
197
|
private readonly seen;
|
|
@@ -198,7 +200,10 @@ declare class Daemon {
|
|
|
198
200
|
private readonly waiters;
|
|
199
201
|
private stopping;
|
|
200
202
|
private heartbeatTimer;
|
|
201
|
-
constructor(cfg: DaemonConfig, adapter: RuntimeAdapter, ws?: AgentWsClient
|
|
203
|
+
constructor(cfg: DaemonConfig, adapter: RuntimeAdapter, ws?: AgentWsClient, // injectable for tests; defaults to a real socket
|
|
204
|
+
/** Called when the socket gives up for good (auth refused). The supervisor
|
|
205
|
+
* above decides what happens next — this class does not end the process. */
|
|
206
|
+
onTerminal?: ((reason: string) => void) | undefined);
|
|
202
207
|
start(): Promise<void>;
|
|
203
208
|
stop(): void;
|
|
204
209
|
private onInbound;
|
package/dist/daemon-entry.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
acquireLeaderLock,
|
|
3
3
|
beat,
|
|
4
|
+
credentialsPath,
|
|
4
5
|
external_exports,
|
|
5
6
|
getMeLite,
|
|
7
|
+
idle,
|
|
6
8
|
log,
|
|
7
9
|
resolveIdentity
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-NEGTEBFK.js";
|
|
9
11
|
|
|
10
12
|
// src/daemon/ws-client.ts
|
|
11
13
|
import { WebSocket } from "ws";
|
|
@@ -260,6 +262,10 @@ function describeSender(ctx) {
|
|
|
260
262
|
return ctx.senderKind === "system" ? `${named}, a system agent` : named;
|
|
261
263
|
}
|
|
262
264
|
|
|
265
|
+
// src/daemon/run.ts
|
|
266
|
+
import * as path2 from "path";
|
|
267
|
+
import * as fs from "fs";
|
|
268
|
+
|
|
263
269
|
// src/daemon/config.ts
|
|
264
270
|
import * as path from "path";
|
|
265
271
|
function wsUrlFor(apiBase) {
|
|
@@ -299,9 +305,10 @@ var HEARTBEAT_MS = 3e4;
|
|
|
299
305
|
var YIELD_MS = Number(process.env["AGENTCHATD_YIELD_MS"] ?? 1e4);
|
|
300
306
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
301
307
|
var Daemon = class {
|
|
302
|
-
constructor(cfg, adapter, ws) {
|
|
308
|
+
constructor(cfg, adapter, ws, onTerminal) {
|
|
303
309
|
this.cfg = cfg;
|
|
304
310
|
this.adapter = adapter;
|
|
311
|
+
this.onTerminal = onTerminal;
|
|
305
312
|
this.coord = new ReplyCoord({
|
|
306
313
|
apiKey: cfg.apiKey,
|
|
307
314
|
apiBase: cfg.apiBase,
|
|
@@ -313,11 +320,12 @@ var Daemon = class {
|
|
|
313
320
|
this.ws.on("terminal", (reason) => {
|
|
314
321
|
log.error(`daemon terminal: ${reason}`);
|
|
315
322
|
this.stop();
|
|
316
|
-
|
|
323
|
+
this.onTerminal?.(reason);
|
|
317
324
|
});
|
|
318
325
|
}
|
|
319
326
|
cfg;
|
|
320
327
|
adapter;
|
|
328
|
+
onTerminal;
|
|
321
329
|
ws;
|
|
322
330
|
coord;
|
|
323
331
|
seen = /* @__PURE__ */ new Map();
|
|
@@ -412,7 +420,7 @@ var Daemon = class {
|
|
|
412
420
|
this.inFlight++;
|
|
413
421
|
return Promise.resolve();
|
|
414
422
|
}
|
|
415
|
-
return new Promise((
|
|
423
|
+
return new Promise((resolve3) => this.waiters.push(resolve3));
|
|
416
424
|
}
|
|
417
425
|
releaseSlot() {
|
|
418
426
|
const next = this.waiters.shift();
|
|
@@ -422,40 +430,103 @@ var Daemon = class {
|
|
|
422
430
|
};
|
|
423
431
|
|
|
424
432
|
// src/daemon/run.ts
|
|
433
|
+
var POLL_MS = 5e3;
|
|
434
|
+
var TICK_MS = 250;
|
|
435
|
+
var MAX_BACKOFF_MS2 = 5 * 6e4;
|
|
436
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
437
|
+
function fingerprint(home) {
|
|
438
|
+
const id = resolveIdentity(home);
|
|
439
|
+
return id === null ? null : `${id.apiKey}:${id.handle ?? ""}`;
|
|
440
|
+
}
|
|
425
441
|
async function runDaemon(opts) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
...opts.workdir !== void 0 ? { workdir: opts.workdir } : {}
|
|
431
|
-
});
|
|
432
|
-
} catch (err) {
|
|
433
|
-
console.error(String(err instanceof Error ? err.message : err));
|
|
434
|
-
return 1;
|
|
435
|
-
}
|
|
436
|
-
const lock = acquireLeaderLock(cfg.home);
|
|
442
|
+
const home = path2.resolve(opts.home);
|
|
443
|
+
const workdir = opts.workdir ?? path2.join(home, "daemon-workdir");
|
|
444
|
+
if (process.env["AGENTCHAT_LOG_LEVEL"] === void 0) process.env["AGENTCHAT_LOG_LEVEL"] = "info";
|
|
445
|
+
const lock = acquireLeaderLock(home);
|
|
437
446
|
if (lock === null) return 1;
|
|
438
|
-
|
|
439
|
-
let
|
|
447
|
+
let live = null;
|
|
448
|
+
let liveFingerprint = null;
|
|
449
|
+
let refused = null;
|
|
450
|
+
let failures = 0;
|
|
451
|
+
let lastFailure = null;
|
|
452
|
+
let shuttingDown = false;
|
|
453
|
+
const disconnect = (why) => {
|
|
454
|
+
if (live === null) return;
|
|
455
|
+
log.info(`${why} \u2014 disconnecting, staying resident`);
|
|
456
|
+
live.stop();
|
|
457
|
+
live = null;
|
|
458
|
+
liveFingerprint = null;
|
|
459
|
+
idle(home);
|
|
460
|
+
};
|
|
440
461
|
const shutdown = (sig) => {
|
|
441
|
-
if (
|
|
442
|
-
|
|
462
|
+
if (shuttingDown) return;
|
|
463
|
+
shuttingDown = true;
|
|
443
464
|
log.info(`${sig} \u2014 shutting down`);
|
|
444
|
-
|
|
465
|
+
live?.stop();
|
|
466
|
+
idle(home);
|
|
445
467
|
lock.release();
|
|
446
468
|
process.exit(0);
|
|
447
469
|
};
|
|
448
470
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
449
471
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
472
|
+
let nudged = false;
|
|
450
473
|
try {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
474
|
+
fs.mkdirSync(home, { recursive: true });
|
|
475
|
+
const watcher = fs.watch(home, (_event, filename) => {
|
|
476
|
+
if (filename === null || String(filename).startsWith("credentials")) nudged = true;
|
|
477
|
+
});
|
|
478
|
+
watcher.unref();
|
|
479
|
+
} catch {
|
|
480
|
+
}
|
|
481
|
+
log.info(`always-on resident for ${home} (${credentialsPath(home)})`);
|
|
482
|
+
idle(home);
|
|
483
|
+
for (; ; ) {
|
|
484
|
+
if (shuttingDown) break;
|
|
485
|
+
const fp = fingerprint(home);
|
|
486
|
+
if (fp === null) {
|
|
487
|
+
disconnect("signed out");
|
|
488
|
+
if (refused !== null) refused = null;
|
|
489
|
+
} else if (fp !== liveFingerprint) {
|
|
490
|
+
disconnect("identity changed");
|
|
491
|
+
failures = 0;
|
|
492
|
+
if (fp === refused) {
|
|
493
|
+
} else {
|
|
494
|
+
try {
|
|
495
|
+
const cfg = await resolveDaemonConfig({ home, workdir });
|
|
496
|
+
const candidate = new Daemon(cfg, opts.adapter, void 0, (reason) => {
|
|
497
|
+
log.warn(`credential refused (${reason}) \u2014 idling until it changes`);
|
|
498
|
+
refused = fp;
|
|
499
|
+
live = null;
|
|
500
|
+
liveFingerprint = null;
|
|
501
|
+
idle(home);
|
|
502
|
+
});
|
|
503
|
+
await candidate.start();
|
|
504
|
+
live = candidate;
|
|
505
|
+
liveFingerprint = fp;
|
|
506
|
+
failures = 0;
|
|
507
|
+
lastFailure = null;
|
|
508
|
+
} catch (err) {
|
|
509
|
+
const msg = String(err instanceof Error ? err.message : err);
|
|
510
|
+
if (msg !== lastFailure) {
|
|
511
|
+
log.warn(`not connecting yet: ${msg}`);
|
|
512
|
+
lastFailure = msg;
|
|
513
|
+
}
|
|
514
|
+
failures += 1;
|
|
515
|
+
live = null;
|
|
516
|
+
liveFingerprint = null;
|
|
517
|
+
idle(home);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const waitMs = failures === 0 ? POLL_MS : Math.min(POLL_MS * 2 ** Math.min(failures, 6), MAX_BACKOFF_MS2);
|
|
522
|
+
nudged = false;
|
|
523
|
+
const deadline = Date.now() + waitMs;
|
|
524
|
+
while (!nudged && !shuttingDown && Date.now() < deadline) {
|
|
525
|
+
await sleep(TICK_MS);
|
|
526
|
+
}
|
|
456
527
|
}
|
|
457
|
-
|
|
458
|
-
|
|
528
|
+
lock.release();
|
|
529
|
+
return 0;
|
|
459
530
|
}
|
|
460
531
|
export {
|
|
461
532
|
AgentWsClient,
|
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/config.ts","../src/daemon/loop.ts","../src/daemon/run.ts"],"sourcesContent":["import { WebSocket } from 'ws'\nimport { EventEmitter } from 'node:events'\nimport { log } from '../util/log.js'\nimport { parseInbound, type SyncRow } from './frames.js'\n\n// ─── Agent WebSocket client ─────────────────────────────────────────────────\n//\n// Connects to /v1/ws as the agent (Bearer auth). The server drains undelivered\n// as `message.new` frames on connect AND pushes them in real time. The `ws`\n// library auto-pongs the server's heartbeat pings, which keeps presence alive.\n// We add reconnect with exponential backoff + jitter, a liveness watchdog, and\n// a terminal state for auth failure so a bad key doesn't reconnect forever.\n\ntype State = 'connecting' | 'ready' | 'reconnecting' | 'terminal' | 'closed'\n\nconst BASE_BACKOFF_MS = 1_000\nconst MAX_BACKOFF_MS = 60_000\n// If no frame/ping arrives for this long, treat the socket as dead. The server\n// pings every 45s, so ~2 missed cycles.\nconst LIVENESS_MS = 100_000\n\nexport interface WsClientEvents {\n inbound: (row: SyncRow) => void\n ready: () => void\n terminal: (reason: string) => void\n}\n\nexport class AgentWsClient extends EventEmitter {\n private ws: WebSocket | null = null\n private state: State = 'closed'\n private attempt = 0\n private reconnectTimer: NodeJS.Timeout | null = null\n private livenessTimer: NodeJS.Timeout | null = null\n private stopped = false\n private ackMode = false\n\n constructor(\n private readonly url: string,\n private readonly apiKey: string,\n ) {\n super()\n }\n\n /** True only while the socket is live and ready. The heartbeat writer keys\n * off this, so a reconnecting/terminal daemon lets its heartbeat go stale\n * and the next session detects that always-on is actually down. */\n get connected(): boolean {\n return this.state === 'ready'\n }\n\n start(): void {\n this.stopped = false\n this.open()\n }\n\n stop(): void {\n this.stopped = true\n this.state = 'closed'\n this.clearTimers()\n if (this.ws) {\n try {\n this.ws.close(1000, 'daemon shutdown')\n } catch {\n /* already closed */\n }\n this.ws = null\n }\n }\n\n getState(): State {\n return this.state\n }\n\n /**\n * Confirm a message as handled: `{\"type\":\"ack\",\"message_id\":\"msg_...\"}`.\n * Fire-and-forget by design — a dropped ack is loss-free (the delivery\n * stays 'stored' and re-drains on the next reconnect, where dedup absorbs\n * the replay). Acking by message id (not delivery id) is what lets a\n * real-time push — which carries no delivery_id — be acked at all.\n */\n ack(messageId: string): void {\n if (this.state !== 'ready' || !this.ws) return\n try {\n this.ws.send(JSON.stringify({ type: 'ack', message_id: messageId }))\n } catch (err) {\n log.debug(`ack send failed for ${messageId} (will re-drain): ${String(err)}`)\n }\n }\n\n private open(): void {\n if (this.stopped) return\n this.state = this.attempt === 0 ? 'connecting' : 'reconnecting'\n log.info(`ws ${this.state} (attempt ${this.attempt + 1}) → ${this.url}`)\n\n const ws = new WebSocket(this.url, {\n headers: {\n authorization: `Bearer ${this.apiKey}`,\n // Opt into the delivery-ack protocol: the server then leaves each\n // delivery 'stored' until we ack it (by message id) instead of\n // marking it delivered the instant it hits the socket. A crash\n // mid-turn therefore re-drains on reconnect — at-least-once.\n 'x-agentchat-capabilities': 'ack',\n },\n })\n this.ws = ws\n\n ws.on('open', () => {\n this.attempt = 0\n this.state = 'ready'\n this.armLiveness()\n log.info('ws ready — draining + listening')\n this.emit('ready')\n })\n\n ws.on('message', (data) => {\n this.armLiveness()\n let frame: unknown\n try {\n frame = JSON.parse(data.toString())\n } catch {\n return // non-JSON frame — ignore\n }\n const f = frame as { type?: string; payload?: unknown; capabilities?: unknown }\n if (f?.type === 'message.new') {\n const row = parseInbound(f.payload)\n if (row) this.emit('inbound', row)\n else log.warn(`message.new payload failed to parse: ${JSON.stringify(f.payload).slice(0, 300)}`)\n } else if (f?.type === 'hello.ok') {\n const caps = Array.isArray(f.capabilities) ? (f.capabilities as string[]) : []\n this.ackMode = caps.includes('ack')\n log.info(`ws hello.ok — ack-mode ${this.ackMode ? 'ON' : 'OFF (legacy)'}`)\n } else {\n log.debug(`ws frame: ${f?.type}`)\n }\n // presence.update, typing.* etc. — not acted on here.\n })\n\n ws.on('ping', () => this.armLiveness()) // ws auto-pongs; just refresh liveness\n\n ws.on('unexpected-response', (_req, res) => {\n if (res.statusCode === 401 || res.statusCode === 403) {\n this.state = 'terminal'\n this.clearTimers()\n const reason = `auth rejected (${res.statusCode}) — check the agent's API key`\n log.error(`ws ${reason}`)\n this.emit('terminal', reason)\n return\n }\n log.warn(`ws unexpected response ${res.statusCode} — will reconnect`)\n })\n\n ws.on('error', (err) => {\n log.warn(`ws error: ${String(err)}`)\n // 'close' fires after 'error'; reconnect is scheduled there.\n })\n\n ws.on('close', (code) => {\n if (this.state === 'terminal' || this.stopped) return\n log.warn(`ws closed (${code}) — scheduling reconnect`)\n this.scheduleReconnect()\n })\n }\n\n private scheduleReconnect(): void {\n if (this.stopped || this.state === 'terminal') return\n this.state = 'reconnecting'\n this.clearTimers()\n const backoff = Math.min(BASE_BACKOFF_MS * 2 ** this.attempt, MAX_BACKOFF_MS)\n const jitter = backoff * (0.5 + Math.random() * 0.5) // 50–100% of backoff\n this.attempt++\n this.reconnectTimer = setTimeout(() => this.open(), jitter)\n }\n\n private armLiveness(): void {\n if (this.livenessTimer) clearTimeout(this.livenessTimer)\n this.livenessTimer = setTimeout(() => {\n log.warn('ws liveness timeout — forcing reconnect')\n try {\n this.ws?.terminate()\n } catch {\n /* ignore */\n }\n this.scheduleReconnect()\n }, LIVENESS_MS)\n }\n\n private clearTimers(): void {\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer)\n this.reconnectTimer = null\n }\n if (this.livenessTimer) {\n clearTimeout(this.livenessTimer)\n this.livenessTimer = null\n }\n }\n}\n","import { z } from 'zod'\nimport { log } from '../util/log.js'\n\n// ─── Wire shapes + HTTP fallback drain ──────────────────────────────────────\n//\n// The socket is ACK-CAPABLE (opted in via the `x-agentchat-capabilities: ack`\n// request header): the server leaves deliveries 'stored' until we ack, so a\n// crash mid-processing re-drains on reconnect (at-least-once). We ack over the\n// WS by MESSAGE id (`{\"type\":\"ack\",\"message_id\":\"msg_...\"}`) — the one field\n// present on BOTH real-time pushes and reconnect-drain frames. Real-time frames\n// carry NO delivery_id (that's a REST /sync concept), so the schema treats it\n// as optional. syncPeek/syncAck below are the belt-and-suspenders REST fallback\n// (that path always has delivery_id). Same bare-array / string-cursor wire the\n// coding-agents CLI uses (SDK still mis-types this path).\n\nexport interface WireConfig {\n apiKey: string\n apiBase: string\n timeoutMs?: number\n}\n\nconst SyncRowSchema = z\n .object({\n id: z.string(),\n conversation_id: z.string(),\n // Present on REST /sync + reconnect-drain rows; ABSENT on real-time pushes.\n delivery_id: z.string().nullish(),\n sender: z.string().optional(),\n sender_handle: z.string().optional(),\n type: z.string().optional(),\n content: z.record(z.unknown()).optional(),\n created_at: z.string().optional(),\n })\n .passthrough()\n\nexport type SyncRow = z.infer<typeof SyncRowSchema>\n\nasync function request(cfg: WireConfig, method: 'GET' | 'POST', pathname: string, body?: unknown): Promise<unknown> {\n const url = cfg.apiBase.replace(/\\/+$/, '') + pathname\n const res = await fetch(url, {\n method,\n headers: {\n authorization: `Bearer ${cfg.apiKey}`,\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n signal: AbortSignal.timeout(cfg.timeoutMs ?? 6000),\n })\n if (!res.ok) throw new Error(`AgentChat API ${res.status}: ${(await res.text().catch(() => '')).slice(0, 200)}`)\n return res.json()\n}\n\nexport function parseInbound(payload: unknown): SyncRow | null {\n const parsed = SyncRowSchema.safeParse(payload)\n return parsed.success ? parsed.data : null\n}\n\nexport function senderOf(row: SyncRow): string {\n return row.sender ?? row.sender_handle ?? 'unknown'\n}\n\n/** Platform-authored trusted context (server `message.context`) — resolved\n * sender identity, the conversation descriptor, and the parsed mention list.\n * Read defensively off the passthrough row; a message predating the server\n * enrichment yields all-null/empty and the caller degrades to bare handles. */\nexport interface MessageContext {\n senderDisplayName: string | null\n senderKind: 'agent' | 'system'\n groupName: string | null\n memberCount: number | null\n mentions: string[]\n}\n\nexport function contextOf(row: SyncRow): MessageContext {\n const raw = (row as { context?: unknown }).context\n const c = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n const sender = (c.sender && typeof c.sender === 'object' ? c.sender : {}) as Record<\n string,\n unknown\n >\n const conv = (c.conversation && typeof c.conversation === 'object'\n ? c.conversation\n : {}) as Record<string, unknown>\n return {\n senderDisplayName: typeof sender.display_name === 'string' ? sender.display_name : null,\n senderKind: sender.kind === 'system' ? 'system' : 'agent',\n groupName: typeof conv.group_name === 'string' ? conv.group_name : null,\n memberCount: typeof conv.member_count === 'number' ? conv.member_count : null,\n mentions: Array.isArray(c.mentions)\n ? c.mentions.filter((m): m is string => typeof m === 'string').map((m) => m.toLowerCase())\n : [],\n }\n}\n\n/** Commit deliveries at-or-before the cursor. Injection/handling = delivered. */\nexport async function syncAck(cfg: WireConfig, lastDeliveryId: string): Promise<number> {\n const data = await request(cfg, 'POST', '/v1/messages/sync/ack', { last_delivery_id: lastDeliveryId })\n const parsed = z.object({ acked: z.number() }).safeParse(data)\n return parsed.success ? parsed.data.acked : 0\n}\n\n/** Non-destructive peek — a fallback drain if the WS ever misses (belt-and-\n * suspenders; the WS already drains on connect). */\nexport async function syncPeek(cfg: WireConfig, after?: string): Promise<SyncRow[]> {\n const qs = after ? `?after=${encodeURIComponent(after)}&limit=200` : '?limit=200'\n const data = await request(cfg, 'GET', `/v1/messages/sync${qs}`)\n if (!Array.isArray(data)) {\n log.warn(`sync returned non-array (${typeof data})`)\n return []\n }\n const rows: SyncRow[] = []\n for (const item of data) {\n const p = SyncRowSchema.safeParse(item)\n if (p.success) rows.push(p.data)\n else break // never ack past an unparseable row\n }\n return rows\n}\n","import { log } from '../util/log.js'\n\n// ─── Reply-coordination client (/v1/reply) ───────────────────────────────────\n//\n// Lets this daemon agree with the agent's live coding session on ONE replier\n// per message, so a message is never answered twice when both are present.\n//\n// Design rule: EVERY call fails OPEN toward replying. A coordination outage\n// (Redis/API blip) must never make the daemon go silent — a missed reply is\n// worse than a rare double. So `claim` fails to TRUE (reply anyway) and\n// `isSessionActive` fails to FALSE (don't yield to a session we can't see).\n\nexport interface CoordConfig {\n apiKey: string\n apiBase: string\n /** Stable, replier-unique token, e.g. \"daemon:<host>\". Same token across a\n * restart on the same host so the daemon re-claims its own in-flight work. */\n holder: string\n timeoutMs?: number\n}\n\nexport class ReplyCoord {\n constructor(private readonly cfg: CoordConfig) {}\n\n private async req(method: 'GET' | 'POST', pathname: string, body?: unknown): Promise<unknown> {\n const url = this.cfg.apiBase.replace(/\\/+$/, '') + pathname\n const res = await fetch(url, {\n method,\n headers: {\n authorization: `Bearer ${this.cfg.apiKey}`,\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n signal: AbortSignal.timeout(this.cfg.timeoutMs ?? 5_000),\n })\n if (!res.ok) throw new Error(`reply-coord ${res.status}`)\n return res.json()\n }\n\n /** Is the agent's live coding session actively working? Fail-open → FALSE. */\n async isSessionActive(): Promise<boolean> {\n try {\n const d = (await this.req('GET', '/v1/reply/active')) as { active?: boolean }\n return d?.active === true\n } catch (err) {\n log.debug(`coord isSessionActive failed (assuming inactive): ${String(err)}`)\n return false\n }\n }\n\n /**\n * Claim the sole right to reply to a message. Returns true if THIS daemon is\n * the designated replier, false if a live session already owns it. Fail-open\n * → TRUE (reply anyway rather than drop).\n */\n async claim(messageId: string): Promise<boolean> {\n try {\n const d = (await this.req('POST', '/v1/reply/claim', {\n message_id: messageId,\n holder: this.cfg.holder,\n })) as { claimed?: boolean }\n return d?.claimed !== false\n } catch (err) {\n log.debug(`coord claim failed (proceeding): ${String(err)}`)\n return true\n }\n }\n}\n","import type { TurnContext } from './adapter-types.js'\n\n// Shared first-touch orientation fragments for the daemon adapters (claude +\n// codex render identical framing). Group labels keep the conversation id so the\n// agent can pass it straight to agentchat_get_conversation.\n\n/** \"the group \\\"Ops\\\" (grp_x)\", or a bare \"the group conversation grp_x\" when\n * the server supplied no name, or \"the direct conversation conv_x\". */\nexport function describeConversation(ctx: TurnContext): string {\n if (!ctx.conversationId.startsWith('grp_')) {\n return `the direct conversation ${ctx.conversationId}`\n }\n return ctx.groupName\n ? `the group \"${ctx.groupName}\" (${ctx.conversationId})`\n : `the group conversation ${ctx.conversationId}`\n}\n\n/** Resolved sender identity: \"Display Name (@handle)\" or \"@handle\", flagging a\n * system agent so the model weights its words as platform-authored. */\nexport function describeSender(ctx: TurnContext): string {\n const named = ctx.senderDisplayName\n ? `${ctx.senderDisplayName} (@${ctx.sender})`\n : `@${ctx.sender}`\n return ctx.senderKind === 'system' ? `${named}, a system agent` : named\n}\n","import * as path from 'node:path'\nimport { resolveIdentity } from '../identity/credentials.js'\nimport { getMeLite } from '../wire/index.js'\n\n// ─── Daemon identity resolution ─────────────────────────────────────────────\n//\n// The daemon runs AS one host agent — the same identity that agent's in-session\n// hooks use, never a separate account. It reads that credential from the home\n// it is GIVEN.\n//\n// The predecessor of this file mapped a `runtime` enum to a home\n// (`codex → ~/.codex/agentchat`, `claude-code → ~/.claude/agentchat`). That\n// mapping is exactly the \"a function that decides can decide wrong\" defect this\n// package exists to make unrepresentable, so it is gone: the caller passes its\n// own home and there is no enum to mis-set.\n\nexport interface DaemonConfig {\n apiKey: string\n handle: string\n apiBase: string\n wsUrl: string\n /** The identity home. Credentials, leader lock, and heartbeat all live here. */\n home: string\n /** Scratch dir for the adapter (spawned-turn cwd, generated MCP config). */\n workdir: string\n}\n\n/** `https://api.agentchat.me` → `wss://api.agentchat.me/v1/ws`. */\nexport function wsUrlFor(apiBase: string): string {\n return apiBase.replace(/^http/, 'ws').replace(/\\/+$/, '') + '/v1/ws'\n}\n\nexport interface ResolveDaemonOpts {\n /** THE identity home. Required — this module never derives one. */\n home: string\n workdir?: string\n}\n\n/**\n * Resolve the identity the daemon runs as.\n *\n * Async because the handle is load-bearing at runtime — it filters this agent's\n * own outbound echoed back by server fan-out, and decides whether a group\n * mention names it. An env-only identity (`AGENTCHAT_API_KEY`, no credentials\n * file) has no handle on disk, so we ask the server rather than starting up\n * blind and replying to ourselves.\n */\nexport async function resolveDaemonConfig(opts: ResolveDaemonOpts): Promise<DaemonConfig> {\n const home = path.resolve(opts.home)\n const id = resolveIdentity(home)\n if (id === null) {\n throw new Error(`no AgentChat identity in ${home} — register this agent first`)\n }\n\n let handle = id.handle\n if (handle === null) {\n const me = await getMeLite({ apiKey: id.apiKey, apiBase: id.apiBase })\n if (me === null) {\n throw new Error(\n 'could not determine this agent’s handle (no credentials file, and /v1/agents/me did not answer)',\n )\n }\n handle = me.handle\n }\n\n return {\n apiKey: id.apiKey,\n handle,\n apiBase: id.apiBase,\n wsUrl: wsUrlFor(id.apiBase),\n home,\n workdir: opts.workdir ?? path.join(home, 'daemon-workdir'),\n }\n}\n","import * as os from 'node:os'\nimport { log } from '../util/log.js'\nimport type { DaemonConfig } from './config.js'\nimport { AgentWsClient } from './ws-client.js'\nimport { ReplyCoord } from './coord.js'\nimport { beat } from './health.js'\nimport { contextOf, senderOf, type SyncRow } from './frames.js'\nimport type { RuntimeAdapter } from './adapter-types.js'\n\n// ─── The core loop ──────────────────────────────────────────────────────────\n//\n// WS pushes message.new → dedup → coexistence check (yield to a live session,\n// then claim the sole right to reply) → (per-conversation serialized, globally\n// capped) run one runtime turn → ack on success. Not acking on failure means\n// the server re-drains the message on the next reconnect (at-least-once); a\n// per-message attempt cap drops poison after N tries so it can't loop forever.\n//\n// Host-agnostic by construction: everything it knows about the agent arrives in\n// `DaemonConfig`, and everything it knows about the coding agent arrives as a\n// `RuntimeAdapter`. It cannot name a host, so it cannot act on the wrong one.\n\nconst MAX_CONCURRENT_TURNS = 3\nconst MAX_ATTEMPTS = 3\nconst HEARTBEAT_MS = 30_000\n// When the agent's live coding session is actively working, wait this long\n// before claiming — a head start so the human-driven session (priority) can\n// grab the message first. Only applies while a session is active; the common\n// \"no session, daemon only\" path has zero added latency. Tunable for testing.\nconst YIELD_MS = Number(process.env['AGENTCHATD_YIELD_MS'] ?? 10_000)\n\nconst delay = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms))\n\nexport class Daemon {\n private readonly ws: AgentWsClient\n private readonly coord: ReplyCoord\n private readonly seen = new Map<string, number>() // message id → attempts\n private readonly convChains = new Map<string, Promise<void>>()\n private inFlight = 0\n private readonly waiters: Array<() => void> = []\n private stopping = false\n private heartbeatTimer: NodeJS.Timeout | null = null\n\n constructor(\n private readonly cfg: DaemonConfig,\n private readonly adapter: RuntimeAdapter,\n ws?: AgentWsClient, // injectable for tests; defaults to a real socket\n ) {\n // Stable holder token: the same across a restart on THIS host, so a\n // restarted daemon re-claims its own in-flight messages instead of being\n // locked out by its own prior claim. (Two daemons per agent on one host\n // are already prevented by the leader lock.)\n this.coord = new ReplyCoord({\n apiKey: cfg.apiKey,\n apiBase: cfg.apiBase,\n holder: `daemon:${os.hostname()}`,\n })\n this.ws = ws ?? new AgentWsClient(cfg.wsUrl, cfg.apiKey)\n this.ws.on('inbound', (row: SyncRow) => this.onInbound(row))\n // Every fresh connection stamps the beacon immediately (don't wait up to 30s\n // for the first interval tick to prove we're live).\n this.ws.on('ready', () => beat(this.cfg.home))\n this.ws.on('terminal', (reason: string) => {\n log.error(`daemon terminal: ${reason}`)\n this.stop()\n process.exitCode = 1\n })\n }\n\n async start(): Promise<void> {\n const pre = await this.adapter.preflight()\n if (!pre.ok) {\n throw new Error(`runtime (${this.adapter.name}) not ready: ${pre.detail}`)\n }\n log.info(`agentchat daemon up as @${this.cfg.handle} via ${this.adapter.name}; holding the wire`)\n this.ws.start()\n // Keep the beacon fresh while connected. unref so it never by itself keeps\n // the process alive.\n this.heartbeatTimer = setInterval(() => {\n if (this.ws.connected) beat(this.cfg.home)\n }, HEARTBEAT_MS)\n this.heartbeatTimer.unref()\n }\n\n stop(): void {\n this.stopping = true\n if (this.heartbeatTimer) clearInterval(this.heartbeatTimer)\n this.ws.stop()\n }\n\n private onInbound(row: SyncRow): void {\n // Ignore our own outbound echoed back by server fan-out.\n if (senderOf(row) === this.cfg.handle) return\n if (this.seen.has(row.id)) return // dedup (reconnect replay)\n this.seen.set(row.id, 0)\n this.enqueue(row)\n }\n\n /** Serialize turns within a conversation; the global semaphore caps total. */\n private enqueue(row: SyncRow): void {\n const prev = this.convChains.get(row.conversation_id) ?? Promise.resolve()\n const next = prev\n .then(() => this.handle(row))\n .catch((err) => {\n log.warn(`unhandled in conv ${row.conversation_id}: ${String(err)}`)\n })\n this.convChains.set(row.conversation_id, next)\n // Prune the chain entry once it settles (avoid unbounded map growth).\n void next.then(() => {\n if (this.convChains.get(row.conversation_id) === next) this.convChains.delete(row.conversation_id)\n })\n }\n\n private async handle(row: SyncRow): Promise<void> {\n if (this.stopping) return\n\n // ── Coexistence: agree on exactly one replier ──\n // If the agent's live coding session is actively working, yield briefly so\n // its hook can claim + handle this first (the human-driven session has\n // priority). Then claim the sole right to reply; whoever wins is it.\n if (await this.coord.isSessionActive()) {\n log.info(`msg ${row.id}: live session active — yielding for ${YIELD_MS}ms`)\n await delay(YIELD_MS)\n if (this.stopping) return\n }\n if (!(await this.coord.claim(row.id))) {\n // A live session owns this one. Do NOT ack — leave it 'stored' so the\n // session's sync-peek still sees it and marks it delivered on handling.\n log.info(`msg ${row.id}: claimed by the live session — standing down`)\n return\n }\n\n await this.acquireSlot()\n try {\n const attempts = (this.seen.get(row.id) ?? 0) + 1\n this.seen.set(row.id, attempts)\n log.info(`turn for msg ${row.id} from @${senderOf(row)} (attempt ${attempts})`)\n\n const ctx = contextOf(row)\n const result = await this.adapter.runTurn({\n conversationId: row.conversation_id,\n sender: senderOf(row),\n text: typeof row.content?.['text'] === 'string' ? (row.content['text'] as string) : '',\n createdAt: typeof row.created_at === 'string' ? row.created_at : undefined,\n type: typeof row.type === 'string' ? row.type : undefined,\n senderDisplayName: ctx.senderDisplayName,\n senderKind: ctx.senderKind,\n groupName: ctx.groupName,\n mentioned: ctx.mentions.includes(this.cfg.handle.toLowerCase()),\n })\n\n if (result.ok) {\n this.ws.ack(row.id)\n } else if (result.fatal) {\n log.error(`fatal turn error: ${result.detail} — not acking (will re-drain)`)\n } else if (attempts >= MAX_ATTEMPTS) {\n log.warn(`msg ${row.id} failed ${attempts}× (${result.detail}); acking to drop (poison guard)`)\n this.ws.ack(row.id)\n } else {\n log.warn(`turn failed for ${row.id}: ${result.detail}; leaving unacked for re-drain`)\n }\n } finally {\n this.releaseSlot()\n }\n }\n\n // ─── global concurrency semaphore ─────────────────────────────────────────\n // A waiter inherits the releaser's slot directly (inFlight unchanged on\n // hand-off) — no decrement-then-reincrement window that could momentarily\n // exceed the cap.\n private acquireSlot(): Promise<void> {\n if (this.inFlight < MAX_CONCURRENT_TURNS) {\n this.inFlight++\n return Promise.resolve()\n }\n return new Promise<void>((resolve) => this.waiters.push(resolve))\n }\n\n private releaseSlot(): void {\n const next = this.waiters.shift()\n if (next) next() // pass the slot on; inFlight stays at the cap\n else this.inFlight--\n }\n}\n","import { log } from '../util/log.js'\nimport { acquireLeaderLock } from './leader-lock.js'\nimport { resolveDaemonConfig } from './config.js'\nimport { Daemon } from './loop.js'\nimport type { RuntimeAdapter } from './adapter-types.js'\n\n// ─── The always-on entrypoint ───────────────────────────────────────────────\n//\n// One call an integration's daemon binary makes. It owns everything that is\n// the same for every coding agent — identity resolution, the single-daemon\n// leader lock, signal handling, holding the process open — and nothing that\n// differs. The one thing that differs, \"how do I spawn a headless turn of my\n// runtime\", arrives as the `adapter` argument.\n//\n// Never returns while healthy: a daemon that returns is a daemon that stopped\n// answering, and the service manager would just restart it.\n\nexport interface RunDaemonOpts {\n /** THE identity home for the agent this daemon runs as. */\n home: string\n /** How to spawn one headless turn of this integration's coding agent. */\n adapter: RuntimeAdapter\n /** Scratch dir override; defaults to `<home>/daemon-workdir`. */\n workdir?: string\n}\n\n/**\n * Run the always-on daemon. Resolves to a process exit code only on a failure\n * that makes running pointless (no identity, another daemon already holds the\n * lock, the runtime is not usable).\n */\nexport async function runDaemon(opts: RunDaemonOpts): Promise<number> {\n let cfg\n try {\n cfg = await resolveDaemonConfig({\n home: opts.home,\n ...(opts.workdir !== undefined ? { workdir: opts.workdir } : {}),\n })\n } catch (err) {\n console.error(String(err instanceof Error ? err.message : err))\n return 1\n }\n\n // One daemon per identity on this box.\n const lock = acquireLeaderLock(cfg.home)\n if (lock === null) return 1\n\n const daemon = new Daemon(cfg, opts.adapter)\n let releasing = false\n const shutdown = (sig: string): void => {\n if (releasing) return\n releasing = true\n log.info(`${sig} — shutting down`)\n daemon.stop()\n lock.release()\n process.exit(0)\n }\n process.on('SIGINT', () => shutdown('SIGINT'))\n process.on('SIGTERM', () => shutdown('SIGTERM'))\n\n try {\n await daemon.start()\n } catch (err) {\n console.error(String(err instanceof Error ? err.message : err))\n lock.release()\n return 1\n }\n\n // Hold the process open; the WS client keeps event-loop work alive.\n return await new Promise<number>(() => {})\n}\n"],"mappings":"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","names":["resolve"]}
|
|
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"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -168,6 +168,7 @@ declare const StateSchema: z.ZodObject<{
|
|
|
168
168
|
pending_ack?: string | undefined;
|
|
169
169
|
}>>>;
|
|
170
170
|
last_offer_at: z.ZodOptional<z.ZodString>;
|
|
171
|
+
offer_declined_at: z.ZodOptional<z.ZodString>;
|
|
171
172
|
}, "strip", z.ZodTypeAny, {
|
|
172
173
|
sessions: Record<string, {
|
|
173
174
|
continuations: number;
|
|
@@ -175,6 +176,7 @@ declare const StateSchema: z.ZodObject<{
|
|
|
175
176
|
pending_ack?: string | undefined;
|
|
176
177
|
}>;
|
|
177
178
|
last_offer_at?: string | undefined;
|
|
179
|
+
offer_declined_at?: string | undefined;
|
|
178
180
|
}, {
|
|
179
181
|
sessions?: Record<string, {
|
|
180
182
|
continuations: number;
|
|
@@ -182,6 +184,7 @@ declare const StateSchema: z.ZodObject<{
|
|
|
182
184
|
pending_ack?: string | undefined;
|
|
183
185
|
}> | undefined;
|
|
184
186
|
last_offer_at?: string | undefined;
|
|
187
|
+
offer_declined_at?: string | undefined;
|
|
185
188
|
}>;
|
|
186
189
|
type HookState = z.infer<typeof StateSchema>;
|
|
187
190
|
declare function readState(home: string): HookState;
|
|
@@ -199,6 +202,19 @@ declare function setPendingAck(home: string, sessionKey: string, cursor: string,
|
|
|
199
202
|
declare function takePendingAck(home: string, sessionKey: string, now?: Date): string | null;
|
|
200
203
|
declare function shouldOfferRegistration(home: string, now?: Date): boolean;
|
|
201
204
|
declare function recordRegistrationOffer(home: string, now?: Date): void;
|
|
205
|
+
/**
|
|
206
|
+
* The user said "not now".
|
|
207
|
+
*
|
|
208
|
+
* A hook-injected offer could rely on a 24-hour cooldown, because the hook
|
|
209
|
+
* re-runs and can consult state. A prompt written into the always-loaded
|
|
210
|
+
* instruction file cannot: it is static text the agent re-reads every session,
|
|
211
|
+
* with no memory that it was already declined. So the decline has to be
|
|
212
|
+
* recorded here, and the instruction file rewritten to stop asking.
|
|
213
|
+
*/
|
|
214
|
+
declare function recordOfferDeclined(home: string, now?: Date): void;
|
|
215
|
+
declare function offerDeclined(home: string): boolean;
|
|
216
|
+
/** Cleared when an identity is established, so a later logout asks again. */
|
|
217
|
+
declare function clearOfferDeclined(home: string): void;
|
|
202
218
|
|
|
203
219
|
declare function formatSessionStart(handle: string | null, rows: SyncRow[]): string;
|
|
204
220
|
declare function formatStopPickup(handle: string | null, rows: SyncRow[]): string;
|
|
@@ -227,6 +243,22 @@ interface HostCopy {
|
|
|
227
243
|
label: string;
|
|
228
244
|
}
|
|
229
245
|
declare function formatRegistrationOffer(copy: HostCopy): string;
|
|
246
|
+
/**
|
|
247
|
+
* "You have AgentChat but no handle — offer to set one up."
|
|
248
|
+
*
|
|
249
|
+
* Deliberately bounded: static text is re-read every session, so without an
|
|
250
|
+
* explicit stop condition an agent would raise it forever. `--not-now` records
|
|
251
|
+
* the decline and rewrites this block to the silent variant below.
|
|
252
|
+
*/
|
|
253
|
+
declare function renderUnregisteredBlock(copy: HostCopy): string;
|
|
254
|
+
/**
|
|
255
|
+
* The silent variant, written after `--not-now`.
|
|
256
|
+
*
|
|
257
|
+
* Still states the fact — an agent asked "am I on AgentChat?" should be able to
|
|
258
|
+
* answer, and a user who changes their mind should find the command — but it
|
|
259
|
+
* gives no instruction to act on, so there is nothing to nag with.
|
|
260
|
+
*/
|
|
261
|
+
declare function renderDeclinedBlock(copy: HostCopy): string;
|
|
230
262
|
|
|
231
263
|
declare const ANCHOR_START = "<!-- agentchat:start -->";
|
|
232
264
|
declare const ANCHOR_END = "<!-- agentchat:end -->";
|
|
@@ -417,13 +449,37 @@ declare function markAlwaysOnWanted(home: string): void;
|
|
|
417
449
|
/** Forget the intent (user chose session-only, or uninstalled). */
|
|
418
450
|
declare function clearAlwaysOnWanted(home: string): void;
|
|
419
451
|
declare function alwaysOnWanted(home: string): boolean;
|
|
452
|
+
/** Remember that the user switched always-on off. Survives re-install. */
|
|
453
|
+
declare function markAlwaysOnOptOut(home: string): void;
|
|
454
|
+
/** Cleared only by an explicit `daemon install` — never implicitly. */
|
|
455
|
+
declare function clearAlwaysOnOptOut(home: string): void;
|
|
456
|
+
declare function alwaysOnOptedOut(home: string): boolean;
|
|
420
457
|
/** Touch the liveness beacon. Called by the running daemon. */
|
|
421
458
|
declare function beat(home: string): void;
|
|
459
|
+
/** Clear the beacon. The daemon calls this whenever it is resident but NOT
|
|
460
|
+
* connected, so "idle" is never mistaken for "beating". */
|
|
461
|
+
declare function idle(home: string): void;
|
|
462
|
+
/**
|
|
463
|
+
* Always-on has THREE states, not two.
|
|
464
|
+
*
|
|
465
|
+
* It used to be a boolean pair, which could not tell "idle because nobody is
|
|
466
|
+
* signed in" apart from "installed and broken" — so a signed-out user would be
|
|
467
|
+
* nagged every session about a daemon that was behaving exactly as intended.
|
|
468
|
+
*
|
|
469
|
+
* off — the service is not installed (or was explicitly disabled).
|
|
470
|
+
* idle — installed and resident, but there is no identity to serve.
|
|
471
|
+
* Correct and quiet: the daemon is waiting for a sign-in.
|
|
472
|
+
* connected — holding the wire; the beacon is fresh.
|
|
473
|
+
* down — there IS an identity and the service is installed, but nothing
|
|
474
|
+
* is beating. The only state worth telling a session about.
|
|
475
|
+
*
|
|
476
|
+
* Pure reads, no subprocess, never throws.
|
|
477
|
+
*/
|
|
478
|
+
type AlwaysOnState = 'off' | 'idle' | 'connected' | 'down';
|
|
479
|
+
declare function alwaysOnState(home: string): AlwaysOnState;
|
|
422
480
|
/**
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
* `wanted:true, healthy:false` means always-on was set up but the daemon isn't
|
|
426
|
-
* beating → the hook warns. Pure reads (two stats), no subprocess, never throws.
|
|
481
|
+
* Back-compatible view for callers that only need "should I warn?".
|
|
482
|
+
* `healthy` is false ONLY in the `down` state — an idle daemon is healthy.
|
|
427
483
|
*/
|
|
428
484
|
declare function alwaysOnHealth(home: string): {
|
|
429
485
|
wanted: boolean;
|
|
@@ -509,4 +565,4 @@ declare function formatWhen(createdAt: string | undefined, now?: number): string
|
|
|
509
565
|
declare function atomicWriteFile(filePath: string, data: string, mode?: number): void;
|
|
510
566
|
declare function readJsonFile<T>(filePath: string): T | null;
|
|
511
567
|
|
|
512
|
-
export { ANCHOR_END, ANCHOR_START, type AnchorAction, type Credentials, DEFAULT_API_BASE, type DoctorCheck, type DoctorOpts, HEARTBEAT_FILE, type HookContext, type HookDialect, type HookInput, type HookRunners, type HookState, type HostCopy, type HostProfile, type IdentityCommands, type LockHandle, type MessageContext, type PendingRegistration, type RegisterOpts, type ResolvedIdentity, type ServiceOpts, type ServiceRef, type SessionStartResult, type StopResult, type SyncRow, type Verdict, type WireConfig, WireError, absoluteUtc, acquireLeaderLock, alwaysOnHealth, alwaysOnWanted, anchorLabelOf, atomicWriteFile, beat, claimReply, clearAlwaysOnWanted, clearCredentials, clearPending, clearSessionActive, contextOf, createHookRunners, createIdentityCommands, credentialsPath, formatAlwaysOnDown, formatRegistrationOffer, formatSessionStart, formatStopPickup, formatWhen, getContinuations, getMeLite, hasAnchorAt, hooksDisabled, installService, lastDeliveryId, log, markAlwaysOnWanted, markSessionActive, pendingPath, planForTest, readAnchorHandleAt, readAnchorHandleFrom, readCredentials, readHookInput, readJsonFile, readPending, readState, recordContinuation, recordRegistrationOffer, relativeAge, relativeWhen, removeAnchorAt, renderAnchorBlock, resetSession, resolveIdentity, serviceStatus, sessionStart, setPendingAck, shouldOfferRegistration, statePath, stop, stripAnchorBlock, syncAck, syncPeek, takePendingAck, uninstallService, upsertAnchorBlock, userPrompt, writeAnchor, writeCredentials, writePending, writeState };
|
|
568
|
+
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 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, renderUnregisteredBlock, resetSession, resolveIdentity, serviceStatus, sessionStart, setPendingAck, shouldOfferRegistration, statePath, stop, stripAnchorBlock, syncAck, syncPeek, takePendingAck, uninstallService, upsertAnchorBlock, userPrompt, writeAnchor, writeCredentials, writePending, writeState };
|