@agentconnect.md/daemon 0.0.0

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/index.js ADDED
@@ -0,0 +1,2478 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/daemon.ts
7
+ import { randomUUID as randomUUID2 } from "crypto";
8
+ import { hostname, loadavg, freemem, totalmem } from "os";
9
+ import chokidar from "chokidar";
10
+
11
+ // src/config/load-config.ts
12
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
13
+ import { dirname } from "path";
14
+
15
+ // src/config/config-schema.ts
16
+ import { z } from "zod";
17
+ var RuntimeDefSchema = z.object({
18
+ command: z.string(),
19
+ args: z.array(z.string()).default([]),
20
+ env: z.array(z.object({ name: z.string(), value: z.string() })).default([])
21
+ });
22
+ var ConfigSchema = z.object({
23
+ version: z.literal(1),
24
+ daemonId: z.string().optional(),
25
+ controlPlane: z.object({
26
+ enabled: z.boolean().default(true),
27
+ url: z.string().optional(),
28
+ token: z.string().optional(),
29
+ heartbeatMs: z.number().int().default(15e3)
30
+ }).default({ enabled: false, heartbeatMs: 15e3 }),
31
+ agentsDir: z.string().optional(),
32
+ // resolved against root if absent
33
+ runtimes: z.record(z.string(), RuntimeDefSchema).optional(),
34
+ logging: z.object({ level: z.enum(["trace", "debug", "info", "warn", "error"]).default("info") }).default({ level: "info" }),
35
+ limits: z.object({
36
+ maxAgents: z.number().int().default(8),
37
+ maxConcurrentSessions: z.number().int().default(32),
38
+ agentIdleTimeoutMs: z.number().int().default(9e5)
39
+ }).default({ maxAgents: 8, maxConcurrentSessions: 32, agentIdleTimeoutMs: 9e5 })
40
+ });
41
+
42
+ // src/paths.ts
43
+ import { homedir } from "os";
44
+ import { join, resolve } from "path";
45
+ function resolveRoot(root) {
46
+ const r = root ?? process.env.AGENTCONNECT_ROOT ?? join(homedir(), ".agentconnect");
47
+ return resolve(r.replace(/^~(?=$|\/)/, homedir()));
48
+ }
49
+ function configPath(root) {
50
+ return join(root, "config.json");
51
+ }
52
+ function statePath(root) {
53
+ return join(root, "state", "local.sqlite");
54
+ }
55
+ function defaultAgentsDir(root) {
56
+ return join(root, "agents");
57
+ }
58
+ function registryPath(root) {
59
+ return join(root, "acp_registry.json");
60
+ }
61
+ function registryCachePath(root) {
62
+ return join(root, "acp_registry.cache.json");
63
+ }
64
+
65
+ // src/config/load-config.ts
66
+ function loadConfig(opts = {}) {
67
+ const root = resolveRoot(opts.root);
68
+ const file = opts.configPath ?? configPath(root);
69
+ let raw;
70
+ if (existsSync(file)) {
71
+ raw = JSON.parse(readFileSync(file, "utf8"));
72
+ } else if (opts.autoCreate) {
73
+ raw = { version: 1 };
74
+ mkdirSync(dirname(file), { recursive: true });
75
+ writeFileSync(file, JSON.stringify(raw, null, 2) + "\n");
76
+ } else if (opts.optional) {
77
+ raw = { version: 1 };
78
+ } else {
79
+ throw new Error(`config not found: ${file} (create it, pass --config, or run \`agentconnect login\`)`);
80
+ }
81
+ const cfg = ConfigSchema.parse(raw);
82
+ const o = opts.overrides ?? {};
83
+ if (o.daemonId) cfg.daemonId = o.daemonId;
84
+ if (o.logLevel) cfg.logging.level = o.logLevel;
85
+ if (o.maxAgents !== void 0) cfg.limits.maxAgents = o.maxAgents;
86
+ if (o.cpUrl) cfg.controlPlane.url = o.cpUrl;
87
+ if (o.cpToken) cfg.controlPlane.token = o.cpToken;
88
+ if (o.cpUrl || o.cpToken) cfg.controlPlane.enabled = true;
89
+ if (o.noCp) cfg.controlPlane.enabled = false;
90
+ cfg.agentsDir = o.agentsDir ?? cfg.agentsDir ?? defaultAgentsDir(root);
91
+ return cfg;
92
+ }
93
+ function persistDaemonId(root, daemonId) {
94
+ try {
95
+ const file = configPath(resolveRoot(root));
96
+ const raw = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : { version: 1 };
97
+ raw.daemonId = daemonId;
98
+ mkdirSync(dirname(file), { recursive: true });
99
+ writeFileSync(file, JSON.stringify(raw, null, 2) + "\n");
100
+ } catch {
101
+ }
102
+ }
103
+
104
+ // src/agents/load-agents.ts
105
+ import { readFileSync as readFileSync3, readdirSync, existsSync as existsSync3 } from "fs";
106
+ import { join as join3, resolve as resolve2, isAbsolute, dirname as dirname2 } from "path";
107
+
108
+ // src/agents/agent-schema.ts
109
+ import { z as z2 } from "zod";
110
+ var BindMatchSchema = z2.discriminatedUnion("kind", [
111
+ z2.object({ kind: z2.literal("mention") }),
112
+ z2.object({ kind: z2.literal("dm") }),
113
+ z2.object({ kind: z2.literal("keyword"), value: z2.string() }),
114
+ z2.object({ kind: z2.literal("auto") })
115
+ ]);
116
+ var BindRuleConfigSchema = z2.object({
117
+ channel: z2.string().optional(),
118
+ // absent = any channel
119
+ thread: z2.string().optional(),
120
+ match: BindMatchSchema
121
+ });
122
+ var SlackConfigSchema = z2.object({
123
+ botToken: z2.string(),
124
+ appToken: z2.string(),
125
+ signingSecret: z2.string().optional(),
126
+ botUserId: z2.string().optional(),
127
+ // filled at connect via auth.test if absent
128
+ allowedUserIds: z2.array(z2.string()).default([]),
129
+ notificationChannelId: z2.string().optional(),
130
+ bindRules: z2.array(BindRuleConfigSchema).default([])
131
+ });
132
+ var IntegrationSchema = z2.object({
133
+ id: z2.string(),
134
+ platform: z2.literal("slack"),
135
+ slack: SlackConfigSchema
136
+ });
137
+ var CronDefSchema = z2.object({
138
+ id: z2.string(),
139
+ schedule: z2.string(),
140
+ target: z2.object({ platform: z2.literal("slack"), channel: z2.string() }),
141
+ trigger: z2.string()
142
+ });
143
+ var AgentSchema = z2.object({
144
+ id: z2.string(),
145
+ name: z2.string(),
146
+ status: z2.enum(["active", "inactive", "paused"]).default("active"),
147
+ runtime: z2.string(),
148
+ runtimeOverrides: z2.object({
149
+ model: z2.string().optional(),
150
+ env: z2.array(z2.object({ name: z2.string(), value: z2.string() })).default([])
151
+ }).optional(),
152
+ workspace: z2.object({
153
+ mode: z2.enum(["git-repo", "from-scratch"]),
154
+ path: z2.string(),
155
+ gitRepo: z2.string().optional(),
156
+ gitBranch: z2.string().default("main"),
157
+ pullOnNewSession: z2.boolean().default(true),
158
+ skills: z2.array(z2.string()).default([])
159
+ }),
160
+ integrations: z2.array(IntegrationSchema).default([]),
161
+ // zod 4: nested .default({}) does not apply inner field defaults — use explicit full literal
162
+ output: z2.object({ mode: z2.enum(["low", "medium", "high"]).default("low") }).default({ mode: "low" }),
163
+ permissions: z2.object({ policy: z2.enum(["ask", "auto"]).default("ask"), autoApprove: z2.array(z2.string()).default([]) }).default({ policy: "ask", autoApprove: [] }),
164
+ crons: z2.array(CronDefSchema).default([])
165
+ });
166
+
167
+ // src/agents/agent-env.ts
168
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
169
+ import { join as join2 } from "path";
170
+ function parseDotenv(text) {
171
+ const out = {};
172
+ for (const rawLine of text.split("\n")) {
173
+ const line = rawLine.replace(/\r$/, "").trim();
174
+ if (!line || line.startsWith("#")) continue;
175
+ const body = line.startsWith("export ") ? line.slice(7).trim() : line;
176
+ const eq = body.indexOf("=");
177
+ if (eq === -1) continue;
178
+ const key = body.slice(0, eq).trim();
179
+ if (!key) continue;
180
+ let value = body.slice(eq + 1).trim();
181
+ const q = value[0];
182
+ if (value.length >= 2 && (q === '"' || q === "'") && value.at(-1) === q) {
183
+ value = value.slice(1, -1);
184
+ }
185
+ out[key] = value;
186
+ }
187
+ return out;
188
+ }
189
+ function loadDotenv(dir) {
190
+ const file = join2(dir, ".env");
191
+ if (!existsSync2(file)) return {};
192
+ return parseDotenv(readFileSync2(file, "utf8"));
193
+ }
194
+ var VAR_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
195
+ function interpolateString(value, source, path, ctx) {
196
+ return value.replace(VAR_RE, (_m, name) => {
197
+ const resolved = source[name];
198
+ if (resolved === void 0) {
199
+ throw new Error(`agent "${ctx.id}": unresolved \${${name}} in ${path} (.env: ${ctx.envPath})`);
200
+ }
201
+ return resolved;
202
+ });
203
+ }
204
+ function walk(node, source, path, ctx) {
205
+ if (typeof node === "string") return interpolateString(node, source, path, ctx);
206
+ if (Array.isArray(node)) {
207
+ for (let i = 0; i < node.length; i++) node[i] = walk(node[i], source, `${path}[${i}]`, ctx);
208
+ return node;
209
+ }
210
+ if (node && typeof node === "object") {
211
+ const obj = node;
212
+ for (const key of Object.keys(obj)) {
213
+ obj[key] = walk(obj[key], source, path ? `${path}.${key}` : key, ctx);
214
+ }
215
+ return node;
216
+ }
217
+ return node;
218
+ }
219
+ function interpolateAgent(agent2, source, ctx) {
220
+ walk(agent2, source, "", ctx);
221
+ }
222
+ function agentChildEnv(agent2) {
223
+ return {
224
+ ...agent2.env,
225
+ ...Object.fromEntries((agent2.runtimeOverrides?.env ?? []).map((e) => [e.name, e.value]))
226
+ };
227
+ }
228
+
229
+ // src/agents/load-agents.ts
230
+ var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
231
+ var MAX_DEPTH = 4;
232
+ function parseAgentFile(file) {
233
+ const dir = dirname2(file);
234
+ const env = loadDotenv(dir);
235
+ const agent2 = AgentSchema.parse(JSON.parse(readFileSync3(file, "utf8")));
236
+ interpolateAgent(agent2, { ...process.env, ...env }, { id: agent2.id, envPath: join3(dir, ".env") });
237
+ if (!isAbsolute(agent2.workspace.path)) {
238
+ agent2.workspace.path = resolve2(dir, agent2.workspace.path);
239
+ }
240
+ return { ...agent2, dir, env };
241
+ }
242
+ function findAgentFiles(dir, depth = 0) {
243
+ if (depth > MAX_DEPTH || !existsSync3(dir)) return [];
244
+ let entries;
245
+ try {
246
+ entries = readdirSync(dir, { withFileTypes: true });
247
+ } catch {
248
+ return [];
249
+ }
250
+ if (entries.some((e) => e.isFile() && e.name === "agent.json")) {
251
+ return [join3(dir, "agent.json")];
252
+ }
253
+ const out = [];
254
+ for (const entry of entries) {
255
+ if (!entry.isDirectory()) continue;
256
+ if (IGNORED_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
257
+ out.push(...findAgentFiles(join3(dir, entry.name), depth + 1));
258
+ }
259
+ return out;
260
+ }
261
+ function discoverAgents(agentsDir) {
262
+ return findAgentFiles(agentsDir).map((file) => {
263
+ try {
264
+ return { agent: parseAgentFile(file), dir: dirname2(file) };
265
+ } catch (err) {
266
+ throw new Error(`invalid agent.json at ${file}: ${err.message}`);
267
+ }
268
+ });
269
+ }
270
+ function loadAgents(agentsDir) {
271
+ return discoverAgents(agentsDir).map((d) => d.agent).filter((a) => a.status === "active");
272
+ }
273
+ function selectAgent(agentsDir, name) {
274
+ const agents = discoverAgents(agentsDir).map((d) => d.agent);
275
+ if (name) {
276
+ const match = agents.find((a) => a.id === name);
277
+ if (!match) {
278
+ const available = agents.map((a) => a.id).sort().join(", ") || "(none)";
279
+ throw new Error(`agent "${name}" not found in ${agentsDir}. Available: ${available}`);
280
+ }
281
+ return match;
282
+ }
283
+ if (agents.length === 0) throw new Error(`no agent.json found in ${agentsDir}`);
284
+ if (agents.length > 1) {
285
+ const ids = agents.map((a) => a.id).sort().join(", ");
286
+ throw new Error(`multiple agents found in ${agentsDir}: ${ids}; use --agent <name> to specify one`);
287
+ }
288
+ return agents[0];
289
+ }
290
+
291
+ // src/reconciler/reconciler.ts
292
+ function diffAgents(desired, actualIds) {
293
+ const desiredIds = new Set(desired.map((a) => a.id));
294
+ const toStart = desired.filter((a) => !actualIds.includes(a.id));
295
+ const toStop = actualIds.filter((id) => !desiredIds.has(id));
296
+ return { toStart, toStop };
297
+ }
298
+
299
+ // src/store/local-store.ts
300
+ import Database from "better-sqlite3";
301
+ import { mkdirSync as mkdirSync2 } from "fs";
302
+ import { dirname as dirname3 } from "path";
303
+ function sessionKey(platform, channel, thread, agentId) {
304
+ return `${platform}:${channel}:${thread}:${agentId}`;
305
+ }
306
+ var LocalStore = class {
307
+ db;
308
+ constructor(dbPath) {
309
+ mkdirSync2(dirname3(dbPath), { recursive: true });
310
+ this.db = new Database(dbPath);
311
+ this.db.pragma("journal_mode = WAL");
312
+ this.db.exec(`
313
+ CREATE TABLE IF NOT EXISTS sessions (
314
+ key TEXT PRIMARY KEY, agentId TEXT, platform TEXT, channel TEXT, thread TEXT,
315
+ acpSessionId TEXT, state TEXT, lastDeliveredTs TEXT, updatedAt INTEGER
316
+ );
317
+ CREATE TABLE IF NOT EXISTS transcript (
318
+ channel TEXT, thread TEXT, ts TEXT, sender TEXT, text TEXT,
319
+ PRIMARY KEY (channel, thread, ts)
320
+ );
321
+ CREATE TABLE IF NOT EXISTS cp_routing (
322
+ id INTEGER PRIMARY KEY CHECK (id = 1),
323
+ routingEpoch INTEGER, assignments TEXT, globalRules TEXT
324
+ );
325
+ `);
326
+ }
327
+ getSession(key) {
328
+ return this.db.prepare("SELECT * FROM sessions WHERE key = ?").get(key);
329
+ }
330
+ upsertSession(rec) {
331
+ this.db.prepare(
332
+ `INSERT INTO sessions (key, agentId, platform, channel, thread, acpSessionId, state, lastDeliveredTs, updatedAt)
333
+ VALUES (@key, @agentId, @platform, @channel, @thread, @acpSessionId, @state, @lastDeliveredTs, @updatedAt)
334
+ ON CONFLICT(key) DO UPDATE SET
335
+ acpSessionId=excluded.acpSessionId, state=excluded.state,
336
+ lastDeliveredTs=excluded.lastDeliveredTs, updatedAt=excluded.updatedAt`
337
+ ).run(rec);
338
+ }
339
+ appendTranscript(e) {
340
+ this.db.prepare(
341
+ "INSERT OR IGNORE INTO transcript (channel, thread, ts, sender, text) VALUES (@channel, @thread, @ts, @sender, @text)"
342
+ ).run(e);
343
+ }
344
+ transcriptSince(channel, thread, sinceTs) {
345
+ if (sinceTs === null) {
346
+ return this.db.prepare("SELECT * FROM transcript WHERE channel = ? AND thread = ? ORDER BY ts ASC").all(channel, thread);
347
+ }
348
+ return this.db.prepare("SELECT * FROM transcript WHERE channel = ? AND thread = ? AND ts > ? ORDER BY ts ASC").all(channel, thread, sinceTs);
349
+ }
350
+ openSessionAgents(channel, thread) {
351
+ return this.db.prepare("SELECT agentId FROM sessions WHERE channel = ? AND thread = ? AND state != 'closed'").all(channel, thread).map((r) => r.agentId);
352
+ }
353
+ getCpRouting() {
354
+ return this.db.prepare("SELECT routingEpoch, assignments, globalRules FROM cp_routing WHERE id = 1").get();
355
+ }
356
+ setCpRouting(routingEpoch, assignments, globalRules) {
357
+ this.db.prepare(
358
+ `INSERT INTO cp_routing (id, routingEpoch, assignments, globalRules) VALUES (1, @routingEpoch, @assignments, @globalRules)
359
+ ON CONFLICT(id) DO UPDATE SET routingEpoch=excluded.routingEpoch, assignments=excluded.assignments, globalRules=excluded.globalRules`
360
+ ).run({ routingEpoch, assignments, globalRules });
361
+ }
362
+ close() {
363
+ this.db.close();
364
+ }
365
+ };
366
+
367
+ // src/acp/acp-host.ts
368
+ import { spawn } from "child_process";
369
+ import { Readable, Writable } from "stream";
370
+ import {
371
+ ClientSideConnection,
372
+ ndJsonStream
373
+ } from "@agentclientprotocol/sdk";
374
+ var PROTOCOL_VERSION = 1;
375
+ var AcpHost = class {
376
+ constructor(runtime, opts) {
377
+ this.runtime = runtime;
378
+ this.opts = opts;
379
+ }
380
+ runtime;
381
+ opts;
382
+ child;
383
+ conn;
384
+ async start() {
385
+ if (this.child) throw new Error("AcpHost: already started");
386
+ const child = spawn(this.runtime.command, this.runtime.args, {
387
+ stdio: ["pipe", "pipe", "inherit"],
388
+ env: {
389
+ ...process.env,
390
+ ...Object.fromEntries(this.runtime.env.map((e) => [e.name, e.value])),
391
+ ...this.opts.env ?? {}
392
+ }
393
+ });
394
+ this.child = child;
395
+ if (!child.stdin || !child.stdout) {
396
+ throw new Error("AcpHost: subprocess stdin/stdout are not piped");
397
+ }
398
+ const toAgent = Writable.toWeb(child.stdin);
399
+ const fromAgent = Readable.toWeb(child.stdout);
400
+ const stream = ndJsonStream(toAgent, fromAgent);
401
+ const self = this;
402
+ this.conn = new ClientSideConnection((_agent) => {
403
+ return {
404
+ async sessionUpdate(params) {
405
+ self.opts.onUpdate(params.sessionId, params.update);
406
+ },
407
+ async requestPermission(params) {
408
+ const allow = params.options.find((o) => o.kind === "allow_once" || o.kind === "allow_always");
409
+ const optionId = (allow ?? params.options[0])?.optionId;
410
+ return {
411
+ outcome: optionId ? { outcome: "selected", optionId } : { outcome: "cancelled" }
412
+ };
413
+ },
414
+ async readTextFile() {
415
+ throw new Error("fs.readTextFile not supported in MVP");
416
+ },
417
+ async writeTextFile() {
418
+ throw new Error("fs.writeTextFile not supported in MVP");
419
+ }
420
+ };
421
+ }, stream);
422
+ await this.conn.initialize({
423
+ protocolVersion: PROTOCOL_VERSION,
424
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } }
425
+ });
426
+ }
427
+ async newSession(cwd) {
428
+ const res = await this.conn.newSession({ cwd, mcpServers: [] });
429
+ return res.sessionId;
430
+ }
431
+ async prompt(sessionId, blocks) {
432
+ const res = await this.conn.prompt({ sessionId, prompt: blocks });
433
+ return res.stopReason;
434
+ }
435
+ async cancel(sessionId) {
436
+ await this.conn.cancel({ sessionId });
437
+ }
438
+ async stop() {
439
+ if (!this.child) return;
440
+ this.child.kill("SIGTERM");
441
+ await new Promise((resolve3) => this.child.once("exit", () => resolve3()));
442
+ }
443
+ };
444
+
445
+ // src/workspace/workspace-manager.ts
446
+ import { mkdirSync as mkdirSync3, existsSync as existsSync4, writeFileSync as writeFileSync2 } from "fs";
447
+ import { join as join4 } from "path";
448
+ import { simpleGit } from "simple-git";
449
+ var PULL_TIMEOUT_MS = 4500;
450
+ async function prepareWorkspace(agent2) {
451
+ const cwd = agent2.workspace.path;
452
+ mkdirSync3(cwd, { recursive: true });
453
+ if (agent2.workspace.mode === "from-scratch") {
454
+ const mem = join4(cwd, "memory.md");
455
+ if (!existsSync4(mem)) writeFileSync2(mem, `# ${agent2.name} memory
456
+ `);
457
+ return cwd;
458
+ }
459
+ if (agent2.workspace.pullOnNewSession && existsSync4(join4(cwd, ".git"))) {
460
+ try {
461
+ await Promise.race([
462
+ simpleGit(cwd).pull(["--ff-only"]),
463
+ new Promise((_, rej) => setTimeout(() => rej(new Error("pull timeout")), PULL_TIMEOUT_MS))
464
+ ]);
465
+ } catch {
466
+ }
467
+ }
468
+ return cwd;
469
+ }
470
+
471
+ // src/session/session-manager.ts
472
+ var SessionManager = class {
473
+ constructor(deps) {
474
+ this.deps = deps;
475
+ }
476
+ deps;
477
+ threadOwner(channel, thread) {
478
+ const owners = this.deps.store.openSessionAgents(channel, thread);
479
+ return owners.length === 1 ? owners[0] : null;
480
+ }
481
+ async handle(agentId, msg) {
482
+ const agent2 = this.deps.agentById(agentId);
483
+ if (!agent2) throw new Error(`unknown agent ${agentId}`);
484
+ const thread = msg.thread ?? msg.msgId;
485
+ const key = sessionKey(msg.platform, msg.channel, thread, agentId);
486
+ this.deps.store.appendTranscript({
487
+ channel: msg.channel,
488
+ thread,
489
+ ts: tsOf(msg),
490
+ sender: msg.sender.id,
491
+ text: msg.text
492
+ });
493
+ let rec = this.deps.store.getSession(key);
494
+ const host = await this.deps.hostFor(agentId);
495
+ if (!rec || !rec.acpSessionId) {
496
+ const cwd = await prepareWorkspace(agent2);
497
+ const acpSessionId = await host.newSession(cwd);
498
+ rec = {
499
+ key,
500
+ agentId,
501
+ platform: msg.platform,
502
+ channel: msg.channel,
503
+ thread,
504
+ acpSessionId,
505
+ state: "idle",
506
+ lastDeliveredTs: null,
507
+ updatedAt: Date.now()
508
+ };
509
+ this.deps.store.upsertSession(rec);
510
+ }
511
+ const gap = this.deps.store.transcriptSince(msg.channel, thread, rec.lastDeliveredTs);
512
+ const blocks = [];
513
+ const context = gap.slice(0, -1);
514
+ if (context.length > 0) {
515
+ const ctxText = context.map((e) => `[${e.sender}] ${e.text}`).join("\n");
516
+ blocks.push({ type: "text", text: `(thread context you may have missed)
517
+ ${ctxText}` });
518
+ }
519
+ blocks.push({ type: "text", text: msg.text });
520
+ rec.lastDeliveredTs = tsOf(msg);
521
+ rec.state = "prompting";
522
+ rec.updatedAt = Date.now();
523
+ this.deps.store.upsertSession(rec);
524
+ return { sessionId: rec.acpSessionId, blocks };
525
+ }
526
+ };
527
+ function tsOf(msg) {
528
+ const parts = msg.msgId.split(":");
529
+ return parts[parts.length - 1] ?? "0";
530
+ }
531
+
532
+ // src/router/routing-table.ts
533
+ var KIND_ORDER = ["mention", "dm", "keyword", "auto"];
534
+ function scopeMatches(r, msg) {
535
+ if (r.scope.channel !== void 0 && r.scope.channel !== msg.channel) return false;
536
+ if (r.scope.thread !== void 0 && r.scope.thread !== msg.thread) return false;
537
+ return true;
538
+ }
539
+ function kindMatches(r, msg) {
540
+ switch (r.match.kind) {
541
+ case "mention":
542
+ return r.botUserId !== "" && msg.mentionedBots.includes(r.botUserId);
543
+ case "dm":
544
+ return msg.isDm;
545
+ case "keyword":
546
+ return msg.text.toLowerCase().includes(r.match.value.toLowerCase());
547
+ case "auto":
548
+ return true;
549
+ }
550
+ }
551
+ var pickRule = (r) => ({ agentId: r.agentId, integrationId: r.integrationId });
552
+ function routeRules(msg, rules, threadOwner) {
553
+ if (msg.sender.isBot) return null;
554
+ const authz = (r) => !r.allowedUserIds || r.allowedUserIds.length === 0 || r.allowedUserIds.includes(msg.sender.id);
555
+ const scopeCandidates = rules.filter((r) => scopeMatches(r, msg) && authz(r));
556
+ const kindCandidates = scopeCandidates.filter((r) => kindMatches(r, msg));
557
+ const mention = kindCandidates.find((r) => r.match.kind === "mention");
558
+ if (mention) return pickRule(mention);
559
+ if (msg.thread) {
560
+ const owner = threadOwner(msg.channel, msg.thread);
561
+ if (owner) {
562
+ const reachable = new Set(scopeCandidates.map((r) => r.agentId));
563
+ if (reachable.has(owner)) {
564
+ if (reachable.size === 1) {
565
+ const r = scopeCandidates.find((x) => x.agentId === owner);
566
+ return pickRule(r);
567
+ }
568
+ return null;
569
+ }
570
+ }
571
+ }
572
+ const cpInChannel = kindCandidates.some(
573
+ (r) => r.source === "cp" && r.scope.channel === msg.channel && (r.scope.thread === void 0 || r.scope.thread === msg.thread)
574
+ );
575
+ const layer = cpInChannel ? kindCandidates.filter((r) => r.source === "cp") : kindCandidates;
576
+ for (const kind of KIND_ORDER) {
577
+ if (kind === "mention") continue;
578
+ const r = layer.find((x) => x.match.kind === kind);
579
+ if (r) return pickRule(r);
580
+ }
581
+ return null;
582
+ }
583
+
584
+ // src/router/routing-rule.ts
585
+ function resolveAgentIntegration(agent2, botUserIds) {
586
+ const int = agent2?.integrations.find((i) => i.platform === "slack");
587
+ if (!int) return null;
588
+ return { integrationId: int.id, botUserId: botUserIds[int.id] ?? int.slack.botUserId ?? "" };
589
+ }
590
+ function rulesFromAgent(agent2, botUserIds) {
591
+ const out = [];
592
+ for (const int of agent2.integrations) {
593
+ if (int.platform !== "slack") continue;
594
+ const botUserId = botUserIds[int.id] ?? int.slack.botUserId ?? "";
595
+ for (const br of int.slack.bindRules) {
596
+ out.push({
597
+ agentId: agent2.id,
598
+ integrationId: int.id,
599
+ botUserId,
600
+ scope: { ...br.channel ? { channel: br.channel } : {}, ...br.thread ? { thread: br.thread } : {} },
601
+ match: br.match,
602
+ allowedUserIds: int.slack.allowedUserIds,
603
+ source: "config"
604
+ });
605
+ }
606
+ }
607
+ return out;
608
+ }
609
+ function resolveCpRule(cp, resolve3) {
610
+ const r = resolve3(cp.agentId);
611
+ if (!r) return null;
612
+ return {
613
+ agentId: cp.agentId,
614
+ integrationId: r.integrationId,
615
+ botUserId: r.botUserId,
616
+ scope: cp.scope,
617
+ match: cp.match,
618
+ source: "cp",
619
+ ...cp.epoch !== void 0 ? { epoch: cp.epoch } : {}
620
+ };
621
+ }
622
+ function sessionKeyStr(sk) {
623
+ return `${sk.platform}:${sk.channel}:${sk.thread ?? "-"}`;
624
+ }
625
+ function cpRulesFromAssign(a, epoch) {
626
+ return a.bindRules.map((br) => ({
627
+ agentId: a.agentId,
628
+ scope: { channel: a.sessionKey.channel, ...a.sessionKey.thread ? { thread: a.sessionKey.thread } : {} },
629
+ match: br.match,
630
+ ...epoch !== void 0 ? { epoch } : {}
631
+ }));
632
+ }
633
+ function cpRulesFromUpdate(u) {
634
+ const out = [];
635
+ for (const r of u.rules) {
636
+ const m = r.match;
637
+ if (m?.kind === "mention" || m?.kind === "dm" || m?.kind === "auto") {
638
+ out.push({ agentId: r.agentId, scope: {}, match: { kind: m.kind }, epoch: u.routingEpoch });
639
+ } else if (m?.kind === "keyword" && typeof m.value === "string") {
640
+ out.push({ agentId: r.agentId, scope: {}, match: { kind: "keyword", value: m.value }, epoch: u.routingEpoch });
641
+ }
642
+ }
643
+ return out;
644
+ }
645
+
646
+ // src/router/cp-routing-layer.ts
647
+ var CpRoutingLayer = class {
648
+ constructor(io) {
649
+ this.io = io;
650
+ const s = io.load();
651
+ if (s) {
652
+ this.routingEpoch = s.routingEpoch;
653
+ this.assignments = new Map(Object.entries(s.assignments));
654
+ this.globalRules = s.globalRules;
655
+ }
656
+ }
657
+ io;
658
+ routingEpoch = 0;
659
+ assignments = /* @__PURE__ */ new Map();
660
+ globalRules = [];
661
+ upsertAssign(a) {
662
+ this.assignments.set(sessionKeyStr(a.sessionKey), cpRulesFromAssign(a, this.routingEpoch));
663
+ this.persist();
664
+ }
665
+ applyUpdate(u) {
666
+ if (u.routingEpoch < this.routingEpoch) return;
667
+ this.routingEpoch = u.routingEpoch;
668
+ this.globalRules = cpRulesFromUpdate(u);
669
+ this.persist();
670
+ }
671
+ // Intentionally converges ONLY `assignments`: the register/ok reconcile snapshot carries no
672
+ // route/update global rules — those have their own epoch lifecycle via `applyUpdate`.
673
+ converge(snap) {
674
+ this.routingEpoch = snap.routingEpoch;
675
+ this.assignments = new Map(
676
+ snap.assignments.map((a) => [sessionKeyStr(a.sessionKey), cpRulesFromAssign(a, snap.routingEpoch)])
677
+ );
678
+ for (const k of snap.drop.assignments) this.assignments.delete(k);
679
+ this.persist();
680
+ }
681
+ effectiveRules() {
682
+ return [...[...this.assignments.values()].flat(), ...this.globalRules];
683
+ }
684
+ persist() {
685
+ this.io.save({
686
+ routingEpoch: this.routingEpoch,
687
+ assignments: Object.fromEntries(this.assignments),
688
+ globalRules: this.globalRules
689
+ });
690
+ }
691
+ };
692
+
693
+ // src/slack/connection.ts
694
+ import pkg from "@slack/bolt";
695
+
696
+ // src/slack/normalize.ts
697
+ var MENTION_RE = /<@([A-Z0-9]+)>/g;
698
+ function normalizeSlackEvent(event, ctx) {
699
+ const text = event.text ?? "";
700
+ const mentionedBots = [...text.matchAll(MENTION_RE)].map((m) => m[1]);
701
+ return {
702
+ msgId: `slack:${event.channel}:${event.ts}`,
703
+ traceId: ctx.traceId,
704
+ source: "user",
705
+ platform: "slack",
706
+ channel: event.channel,
707
+ thread: event.thread_ts ?? event.ts,
708
+ sender: { id: event.user ?? event.bot_id ?? "unknown", isBot: Boolean(event.bot_id) },
709
+ text,
710
+ mentionedBots,
711
+ isDm: event.channel_type === "im"
712
+ };
713
+ }
714
+
715
+ // src/slack/connection.ts
716
+ var { App, LogLevel } = pkg;
717
+ function consolidate(agents) {
718
+ const groups = /* @__PURE__ */ new Map();
719
+ for (const a of agents) {
720
+ for (const int of a.integrations) {
721
+ if (int.platform !== "slack") continue;
722
+ const k = int.slack.appToken;
723
+ const g = groups.get(k) ?? { appToken: k, botToken: int.slack.botToken, integrations: [] };
724
+ g.integrations.push({ agentId: a.id, integrationId: int.id });
725
+ groups.set(k, g);
726
+ }
727
+ }
728
+ return groups;
729
+ }
730
+ var SlackConnection = class {
731
+ constructor(deps, factory = (o) => new App({
732
+ token: o.token,
733
+ appToken: o.appToken,
734
+ socketMode: true,
735
+ ...deps.boltDebug ? { logLevel: LogLevel.DEBUG } : {}
736
+ })) {
737
+ this.deps = deps;
738
+ this.app = factory({ token: deps.group.botToken, appToken: deps.group.appToken });
739
+ }
740
+ deps;
741
+ app;
742
+ botUserId = "";
743
+ async start() {
744
+ const log = this.deps.log;
745
+ log?.debug("slack: auth.test \u2192 resolving bot identity (HTTPS)\u2026");
746
+ const auth = await this.app.client.auth.test();
747
+ this.botUserId = auth.user_id ?? "";
748
+ log?.debug(`slack: auth.test ok \u2192 bot user ${this.botUserId}`);
749
+ const deliver = (ev, kind) => {
750
+ const msg = normalizeSlackEvent(ev, { traceId: this.deps.newTraceId() });
751
+ log?.debug(
752
+ `slack: inbound ${kind} ch=${msg.channel} user=${msg.sender.id} isBot=${msg.sender.isBot} isDm=${msg.isDm} mentions=[${msg.mentionedBots.join(",")}] text=${JSON.stringify(msg.text.slice(0, 80))}`
753
+ );
754
+ this.deps.onMessage(msg);
755
+ };
756
+ this.app.message(async ({ message }) => {
757
+ const ev = message;
758
+ if (ev.type !== "message" || !ev.channel) {
759
+ log?.debug(`slack: inbound event ignored (type=${ev.type}, channel=${ev.channel ?? "none"})`);
760
+ return;
761
+ }
762
+ deliver(ev, "message");
763
+ });
764
+ this.app.event("app_mention", async ({ event }) => {
765
+ const ev = event;
766
+ if (!ev.channel) return;
767
+ deliver(ev, "app_mention");
768
+ });
769
+ log?.debug("slack: app.start \u2192 opening Socket Mode WebSocket (wss://\u2026slack.com)\u2026");
770
+ await this.app.start();
771
+ log?.debug("slack: app.start resolved \u2192 socket established");
772
+ }
773
+ async postMessage(channel, text, threadTs) {
774
+ await this.app.client.chat.postMessage({ channel, text, thread_ts: threadTs });
775
+ }
776
+ /**
777
+ * Best-effort assistant loading status (assistant.threads.setStatus).
778
+ * Works in channels/DMs/assistant panel under chat:write (post Mar 2026).
779
+ * Pass status='' to clear. Never throws into dispatch.
780
+ */
781
+ async setStatus(channel, threadTs, status, loadingMessages) {
782
+ try {
783
+ await this.app.client.assistant.threads.setStatus({
784
+ channel_id: channel,
785
+ thread_ts: threadTs,
786
+ status,
787
+ ...loadingMessages ? { loading_messages: loadingMessages } : {}
788
+ });
789
+ } catch (err) {
790
+ this.deps.log?.debug(`slack: setStatus failed (ch=${channel} thread=${threadTs}): ${err.message}`);
791
+ }
792
+ }
793
+ async stop() {
794
+ await this.app.stop();
795
+ }
796
+ };
797
+
798
+ // src/slack/render.ts
799
+ var OutputConverger = class {
800
+ constructor(mode) {
801
+ this.mode = mode;
802
+ }
803
+ mode;
804
+ buf = "";
805
+ flush() {
806
+ if (!this.buf.trim()) {
807
+ this.buf = "";
808
+ return [];
809
+ }
810
+ const text = this.buf;
811
+ this.buf = "";
812
+ return [{ kind: "post", text }];
813
+ }
814
+ onUpdate(update) {
815
+ switch (update.sessionUpdate) {
816
+ case "agent_message_chunk": {
817
+ const content = update.content;
818
+ if (content?.type === "text") this.buf += content.text ?? "";
819
+ return [];
820
+ }
821
+ case "agent_thought_chunk": {
822
+ if (this.mode === "low") return [...this.flush(), { kind: "set-status", text: "is thinking\u2026" }];
823
+ if (this.mode !== "high") return [];
824
+ const content = update.content;
825
+ return [...this.flush(), { kind: "update-main", text: `_thinking: ${content?.text ?? ""}_` }];
826
+ }
827
+ case "tool_call":
828
+ case "tool_call_update": {
829
+ const title = update.title ?? update.toolCallId ?? "tool";
830
+ if (this.mode === "low") return [...this.flush(), { kind: "set-status", text: title }];
831
+ return [...this.flush(), { kind: "update-main", text: `:hammer_and_wrench: ${title}` }];
832
+ }
833
+ case "usage_update":
834
+ return [];
835
+ // dropped (goes to telemetry, not the channel)
836
+ default:
837
+ return [];
838
+ }
839
+ }
840
+ onFinal(link) {
841
+ if (this.mode === "low") return [...this.flush(), { kind: "set-status", text: "" }];
842
+ return [...this.flush(), { kind: "post", text: `:white_check_mark: done \u2014 <${link}|details>` }];
843
+ }
844
+ };
845
+
846
+ // src/scheduler/scheduler.ts
847
+ import { Cron } from "croner";
848
+ function buildSyntheticMessage(agentId, cron, traceId) {
849
+ const msg = {
850
+ msgId: `cron:${cron.id}:${traceId}`,
851
+ traceId,
852
+ source: "cron",
853
+ platform: "slack",
854
+ channel: cron.target.channel,
855
+ thread: `cron:${cron.id}:${traceId}`,
856
+ // fresh thread per fire
857
+ sender: { id: `cron:${cron.id}`, isBot: false },
858
+ text: cron.trigger,
859
+ mentionedBots: [],
860
+ isDm: false,
861
+ trigger: "cron"
862
+ };
863
+ return { agentId, msg };
864
+ }
865
+ var Scheduler = class {
866
+ constructor(deps) {
867
+ this.deps = deps;
868
+ }
869
+ deps;
870
+ jobs = [];
871
+ register(agentId, cron) {
872
+ this.jobs.push(
873
+ new Cron(cron.schedule, () => {
874
+ const { msg } = buildSyntheticMessage(agentId, cron, this.deps.newTraceId());
875
+ this.deps.onFire(agentId, msg);
876
+ })
877
+ );
878
+ }
879
+ stop() {
880
+ for (const j of this.jobs) j.stop();
881
+ this.jobs = [];
882
+ }
883
+ };
884
+
885
+ // src/runtimes/registry.ts
886
+ import { z as z3 } from "zod";
887
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "fs";
888
+ import { dirname as dirname4 } from "path";
889
+ var PackageDistSchema = z3.object({ package: z3.string(), args: z3.array(z3.string()).default([]) });
890
+ var BinaryPlatformSchema = z3.object({
891
+ archive: z3.string().optional(),
892
+ cmd: z3.string(),
893
+ args: z3.array(z3.string()).default([]),
894
+ env: z3.record(z3.string(), z3.string()).default({})
895
+ });
896
+ var DistributionSchema = z3.object({
897
+ npx: PackageDistSchema.optional(),
898
+ uvx: PackageDistSchema.optional(),
899
+ binary: z3.record(z3.string(), BinaryPlatformSchema).optional()
900
+ });
901
+ var RegistryEntrySchema = z3.object({
902
+ id: z3.string(),
903
+ name: z3.string().default(""),
904
+ version: z3.string().default(""),
905
+ distribution: DistributionSchema
906
+ });
907
+ var RegistryDocSchema = z3.object({ agents: z3.union([z3.array(RegistryEntrySchema), z3.record(z3.string(), RegistryEntrySchema)]) }).transform((d) => ({
908
+ agents: Array.isArray(d.agents) ? Object.fromEntries(d.agents.map((a) => [a.id, a])) : d.agents
909
+ }));
910
+ function platformKey() {
911
+ const os = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : null;
912
+ const arch = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : null;
913
+ if (!os || !arch) return null;
914
+ return `${os}-${arch}`;
915
+ }
916
+ function toRuntimeDef(entry) {
917
+ const d = entry.distribution;
918
+ if (d.npx) return { command: "npx", args: ["-y", d.npx.package, ...d.npx.args], env: [] };
919
+ if (d.uvx) return { command: "uvx", args: [d.uvx.package, ...d.uvx.args], env: [] };
920
+ if (d.binary) {
921
+ const key = platformKey();
922
+ const bin = key ? d.binary[key] : void 0;
923
+ if (!bin) return null;
924
+ return { command: bin.cmd, args: bin.args, env: Object.entries(bin.env).map(([name, value]) => ({ name, value })) };
925
+ }
926
+ return null;
927
+ }
928
+ var REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
929
+ var DEFAULT_TIMEOUT_MS = 4500;
930
+ function readCachedDoc(root) {
931
+ const file = registryPath(root);
932
+ if (!existsSync5(file)) return null;
933
+ try {
934
+ return RegistryDocSchema.parse(JSON.parse(readFileSync4(file, "utf8")));
935
+ } catch {
936
+ return null;
937
+ }
938
+ }
939
+ function readCacheMeta(root) {
940
+ const file = registryCachePath(root);
941
+ if (!existsSync5(file)) return {};
942
+ try {
943
+ return JSON.parse(readFileSync4(file, "utf8"));
944
+ } catch {
945
+ return {};
946
+ }
947
+ }
948
+ async function fetchRegistry(root, opts = {}) {
949
+ const doFetch = opts.fetchImpl ?? fetch;
950
+ const meta = readCacheMeta(root);
951
+ const headers = {};
952
+ if (meta.etag) headers["If-None-Match"] = meta.etag;
953
+ if (meta.lastModified) headers["If-Modified-Since"] = meta.lastModified;
954
+ const ac = new AbortController();
955
+ const timer = setTimeout(() => ac.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
956
+ try {
957
+ const res = await doFetch(REGISTRY_URL, { headers, signal: ac.signal });
958
+ if (res.status === 304) return readCachedDoc(root) ?? { agents: {} };
959
+ if (!res.ok) return readCachedDoc(root) ?? { agents: {} };
960
+ const bodyText = await res.text();
961
+ const doc = RegistryDocSchema.parse(JSON.parse(bodyText));
962
+ mkdirSync4(dirname4(registryPath(root)), { recursive: true });
963
+ writeFileSync3(registryPath(root), bodyText);
964
+ const newMeta = {
965
+ etag: res.headers.get("etag") ?? void 0,
966
+ lastModified: res.headers.get("last-modified") ?? void 0,
967
+ fetchedAt: Date.now()
968
+ };
969
+ writeFileSync3(registryCachePath(root), JSON.stringify(newMeta));
970
+ return doc;
971
+ } catch {
972
+ return readCachedDoc(root) ?? { agents: {} };
973
+ } finally {
974
+ clearTimeout(timer);
975
+ }
976
+ }
977
+ async function defaultRuntimes(root, opts = {}) {
978
+ const mode = opts.mode ?? "blocking";
979
+ const cached = mode === "cache-first" ? readCachedDoc(root) : null;
980
+ let doc;
981
+ if (cached) {
982
+ doc = cached;
983
+ void fetchRegistry(root, opts).catch(() => {
984
+ });
985
+ } else {
986
+ doc = await fetchRegistry(root, opts);
987
+ }
988
+ const out = {};
989
+ for (const [id, entry] of Object.entries(doc.agents)) {
990
+ const rt = toRuntimeDef(entry);
991
+ if (rt) out[id] = rt;
992
+ }
993
+ return out;
994
+ }
995
+ async function resolveRuntimes(cfg, root, opts = {}) {
996
+ const userRuntimes = cfg.runtimes ?? {};
997
+ const needed = opts.neededRuntimes;
998
+ if (needed && needed.length > 0 && needed.every((n) => userRuntimes[n])) {
999
+ return { ...userRuntimes };
1000
+ }
1001
+ const defaults = await defaultRuntimes(root, opts);
1002
+ return { ...defaults, ...userRuntimes };
1003
+ }
1004
+
1005
+ // src/log.ts
1006
+ var ORDER = { trace: 10, debug: 20, info: 30, warn: 40, error: 50 };
1007
+ function makeLogger(level = "info") {
1008
+ const threshold = ORDER[level] ?? ORDER.info;
1009
+ const emit = (lvl, msg) => {
1010
+ if (ORDER[lvl] < threshold) return;
1011
+ const line = `[agentconnect] ${lvl.toUpperCase().padEnd(5)} ${msg}`;
1012
+ if (ORDER[lvl] >= ORDER.warn) console.error(line);
1013
+ else console.log(line);
1014
+ };
1015
+ return {
1016
+ trace: (m) => emit("trace", m),
1017
+ debug: (m) => emit("debug", m),
1018
+ info: (m) => emit("info", m),
1019
+ warn: (m) => emit("warn", m),
1020
+ error: (m) => emit("error", m)
1021
+ };
1022
+ }
1023
+
1024
+ // ../protocol/dist/envelope.js
1025
+ import { z as z4 } from "zod";
1026
+ var Envelope = z4.object({
1027
+ v: z4.literal(1),
1028
+ // protocol major; bump = breaking
1029
+ id: z4.string().uuid(),
1030
+ // unique per frame (sender-generated)
1031
+ ts: z4.string().datetime(),
1032
+ // RFC3339, sender clock (advisory only)
1033
+ type: z4.string(),
1034
+ // frame discriminator, e.g. "register"
1035
+ corr: z4.string().uuid().optional(),
1036
+ // correlation: set on a reply to the request's `id`
1037
+ payload: z4.unknown()
1038
+ // validated by the per-type schema
1039
+ });
1040
+ var ControlExt = z4.object({
1041
+ epoch: z4.number().int(),
1042
+ // sessionEpoch this frame was issued under (§3.1 fencing)
1043
+ seq: z4.number().int().optional(),
1044
+ // per-agent monotonic, when agent-scoped
1045
+ agentId: z4.string().uuid().optional(),
1046
+ // present iff seq present
1047
+ launchId: z4.string().uuid().optional()
1048
+ // per-launch fence, §4.4
1049
+ });
1050
+ var NIL_UUID = "00000000-0000-0000-0000-000000000000";
1051
+
1052
+ // ../protocol/dist/frames/auth.js
1053
+ import { z as z5 } from "zod";
1054
+ var AuthReq = z5.object({
1055
+ daemonToken: z5.string(),
1056
+ // short-lived, rotatable daemon credential; its `sub` IS the daemonId
1057
+ // Optional: the token's `sub` is the authoritative daemonId. A daemon MAY echo
1058
+ // it (legacy / explicit), but if present it must equal the token subject.
1059
+ daemonId: z5.string().uuid().optional(),
1060
+ machineId: z5.string().uuid().optional(),
1061
+ // 🅼 machine identity, §3.2
1062
+ attestation: z5.string().optional(),
1063
+ // 🅼 signed proof (JWS), §3.2
1064
+ agentVersion: z5.string(),
1065
+ // daemon build/version
1066
+ resume: z5.object({
1067
+ lastEpoch: z5.number().int(),
1068
+ // sessionEpoch the daemon last held
1069
+ lastRecvSeq: z5.record(z5.string(), z5.number())
1070
+ // per-agent last seq it consumed (§4)
1071
+ }).optional()
1072
+ });
1073
+ var AuthOk = z5.object({
1074
+ daemonId: z5.string().uuid(),
1075
+ sessionEpoch: z5.number().int(),
1076
+ // monotonic; bumped each successful (re)auth — fencing token
1077
+ heartbeatSec: z5.number().int(),
1078
+ // cadence the daemon must emit heartbeat at
1079
+ serverTime: z5.string().datetime(),
1080
+ resume: z5.object({
1081
+ accepted: z5.boolean(),
1082
+ // false ⇒ daemon must do a full register reconcile
1083
+ redeliverFromSeq: z5.record(z5.string(), z5.number()).optional()
1084
+ }).optional()
1085
+ });
1086
+
1087
+ // ../protocol/dist/frames/register.js
1088
+ import { z as z9 } from "zod";
1089
+
1090
+ // ../protocol/dist/frames/route.js
1091
+ import { z as z6 } from "zod";
1092
+ var Platform = z6.enum(["slack", "telegram"]);
1093
+ var SessionKey = z6.object({
1094
+ platform: Platform,
1095
+ channel: z6.string(),
1096
+ thread: z6.string().optional()
1097
+ // absent = channel-root
1098
+ });
1099
+ var BindRule = z6.object({
1100
+ match: z6.discriminatedUnion("kind", [
1101
+ z6.object({ kind: z6.literal("mention") }),
1102
+ z6.object({ kind: z6.literal("dm") }),
1103
+ z6.object({ kind: z6.literal("keyword"), value: z6.string() }),
1104
+ z6.object({ kind: z6.literal("auto") })
1105
+ // alert-channel auto-handle
1106
+ ])
1107
+ });
1108
+ var RouteAssign = z6.object({
1109
+ // also appears in RegisterOk.assignments[]
1110
+ sessionKey: SessionKey,
1111
+ agentId: z6.string().uuid(),
1112
+ workspaceId: z6.string().uuid(),
1113
+ // which D9 workspace to prepare
1114
+ bindRules: z6.array(BindRule).default([])
1115
+ });
1116
+ var RouteAssignAck = z6.object({
1117
+ ok: z6.boolean(),
1118
+ sessionKey: SessionKey,
1119
+ reason: z6.string().optional()
1120
+ });
1121
+ var RouteUpdate = z6.object({
1122
+ routingEpoch: z6.number().int(),
1123
+ rules: z6.array(z6.object({ match: z6.unknown(), agentId: z6.string().uuid() }))
1124
+ });
1125
+ var Drain = z6.object({
1126
+ scope: z6.union([
1127
+ z6.object({ kind: z6.literal("agent"), agentId: z6.string().uuid() }),
1128
+ z6.object({ kind: z6.literal("daemon") }),
1129
+ // whole-daemon drain (shutdown/upgrade)
1130
+ z6.object({ kind: z6.literal("session"), sessionKey: SessionKey })
1131
+ ]),
1132
+ deadline: z6.string().datetime()
1133
+ // hard cutoff; in-flight turns past this are cancelled
1134
+ });
1135
+ var DrainProgress = z6.object({
1136
+ remaining: z6.number().int(),
1137
+ drained: z6.array(SessionKey)
1138
+ });
1139
+ var DrainDone = z6.object({
1140
+ released: z6.array(SessionKey)
1141
+ // CP may now reassign — fenced by new epoch
1142
+ });
1143
+
1144
+ // ../protocol/dist/frames/cron.js
1145
+ import { z as z7 } from "zod";
1146
+ var CronUpsert = z7.object({
1147
+ cronId: z7.string().uuid(),
1148
+ schedule: z7.string(),
1149
+ // croner expression (tz-aware)
1150
+ target: z7.object({ channel: z7.string() }),
1151
+ trigger: z7.string(),
1152
+ // synthetic message text injected into D4 on fire
1153
+ enabled: z7.boolean().default(true)
1154
+ });
1155
+ var CronRemove = z7.object({
1156
+ cronId: z7.string().uuid()
1157
+ });
1158
+
1159
+ // ../protocol/dist/frames/secrets.js
1160
+ import { z as z8 } from "zod";
1161
+ var SecretsRequest = z8.object({
1162
+ // D→C, REQ — daemon asks for a lease at session start
1163
+ scope: z8.object({
1164
+ platform: Platform,
1165
+ workspaceId: z8.string().uuid()
1166
+ })
1167
+ });
1168
+ var SecretsGrant = z8.object({
1169
+ // C→D, REP (also in RegisterOk.leases[])
1170
+ leaseId: z8.string().uuid(),
1171
+ scope: z8.object({
1172
+ platform: z8.string(),
1173
+ workspaceId: z8.string().uuid()
1174
+ }),
1175
+ ref: z8.string(),
1176
+ // Vault/KMS path or handle — NOT the secret
1177
+ ttl: z8.number().int(),
1178
+ // seconds
1179
+ renewBeforeSec: z8.number().int()
1180
+ // daemon should renew this many sec before expiry
1181
+ });
1182
+ var SecretsRenew = z8.object({
1183
+ leaseId: z8.string().uuid()
1184
+ // D→C REQ → new SecretsGrant
1185
+ });
1186
+ var SecretsRevoke = z8.object({
1187
+ leaseId: z8.string().uuid(),
1188
+ reason: z8.string()
1189
+ // C→D EVT (hot revoke)
1190
+ });
1191
+ var ScopeAttestation = z8.object({
1192
+ machineId: z8.string().uuid(),
1193
+ scope: z8.enum(["attachment.put", "attachment.get", "facts.put"]),
1194
+ resourceRef: z8.string(),
1195
+ // opaque object key/prefix
1196
+ jws: z8.string(),
1197
+ // signed capability the store verifies offline
1198
+ exp: z8.string().datetime()
1199
+ });
1200
+
1201
+ // ../protocol/dist/frames/register.js
1202
+ var RegisterReq = z9.object({
1203
+ host: z9.string(),
1204
+ // hostname (display only)
1205
+ capabilities: z9.object({
1206
+ platforms: z9.array(Platform),
1207
+ // D3 adapters present
1208
+ runtimes: z9.array(z9.string()),
1209
+ // e.g. ["claude","codex"]
1210
+ acp: z9.boolean(),
1211
+ // can this daemon host ACP sessions (D6)?
1212
+ features: z9.array(z9.string()).default([])
1213
+ // e.g. ["cli-wrapper-fallback","worktree-iso"]
1214
+ }),
1215
+ maxAgents: z9.number().int(),
1216
+ // concurrency ceiling for placement (C3)
1217
+ localState: z9.object({
1218
+ // what the daemon currently believes it owns (for reconcile)
1219
+ assignments: z9.array(z9.string()),
1220
+ // sessionKeys it is actively serving
1221
+ crons: z9.array(z9.string()),
1222
+ // cronIds it has scheduled
1223
+ leases: z9.array(z9.string())
1224
+ // leaseIds it holds
1225
+ })
1226
+ });
1227
+ var RegisterOk = z9.object({
1228
+ routingEpoch: z9.number().int(),
1229
+ // version of the routing table this snapshot reflects
1230
+ // Authoritative reconcile snapshot — daemon converges its local cache to this:
1231
+ assignments: z9.array(RouteAssign),
1232
+ // the route/assign set the daemon SHOULD own
1233
+ crons: z9.array(CronUpsert),
1234
+ // the cron set it SHOULD run
1235
+ leases: z9.array(SecretsGrant),
1236
+ // secret leases it SHOULD hold
1237
+ drop: z9.object({
1238
+ // things in localState the CP says to release
1239
+ assignments: z9.array(z9.string()),
1240
+ crons: z9.array(z9.string())
1241
+ })
1242
+ });
1243
+
1244
+ // ../protocol/dist/frames/agent.js
1245
+ import { z as z10 } from "zod";
1246
+ var NormalizedMessageRef = z10.object({
1247
+ sessionKey: SessionKey,
1248
+ platformMsgId: z10.string(),
1249
+ seenUpToSeq: z10.number().int()
1250
+ });
1251
+ var AgentLaunch = z10.object({
1252
+ // C→D, carries ControlExt(epoch)
1253
+ agentId: z10.string().uuid(),
1254
+ runtime: z10.string(),
1255
+ // must be in RegisterReq.capabilities.runtimes
1256
+ workspaceId: z10.string().uuid(),
1257
+ capabilities: z10.array(z10.string()),
1258
+ // the active-capability pin (§8.1)
1259
+ mode: z10.enum(["long_lived", "per_turn"]).default("long_lived")
1260
+ // 🅰️ decision #2 knob
1261
+ });
1262
+ var AgentLaunched = z10.object({
1263
+ // D→C, REP/EVT
1264
+ agentId: z10.string().uuid(),
1265
+ launchId: z10.string().uuid(),
1266
+ // new fence value
1267
+ acpSessionId: z10.string().optional(),
1268
+ // 🅰️ present iff long-lived ACP session (default)
1269
+ startedAt: z10.string().datetime(),
1270
+ runtime: z10.string()
1271
+ // e.g. "claude" / "codex"
1272
+ });
1273
+ var AgentStop = z10.object({
1274
+ agentId: z10.string().uuid(),
1275
+ launchId: z10.string().uuid(),
1276
+ reason: z10.string()
1277
+ });
1278
+ var AgentPrompt = z10.object({
1279
+ // C→D, carries ControlExt (epoch/seq/agentId/launchId)
1280
+ sessionKey: SessionKey,
1281
+ agentId: z10.string().uuid(),
1282
+ content: NormalizedMessageRef,
1283
+ // a REFERENCE/digest, NOT the body
1284
+ seenUpToSeq: z10.number().int()
1285
+ // freshness watermark, §4.5
1286
+ });
1287
+ var AgentPromptAck = z10.object({
1288
+ // D→C reply (corr = prompt.id)
1289
+ accepted: z10.boolean(),
1290
+ reason: z10.enum(["queued", "held", "scope_denied", "no_session", "stale"]).optional(),
1291
+ seq: z10.number().int()
1292
+ // echoes the accepted seq
1293
+ });
1294
+ var AgentActivity = z10.object({
1295
+ // D→C, EVT — activity-probe (§7.4)
1296
+ agentId: z10.string().uuid(),
1297
+ launchId: z10.string().uuid(),
1298
+ state: z10.enum(["thinking", "tool_call", "awaiting_permission", "idle"]),
1299
+ ts: z10.string().datetime()
1300
+ });
1301
+ var AgentScopeDenied = z10.object({
1302
+ // D→C, EVT — capability-scope audit (§8.1)
1303
+ agentId: z10.string().uuid(),
1304
+ launchId: z10.string().uuid(),
1305
+ capability: z10.string()
1306
+ });
1307
+
1308
+ // ../protocol/dist/frames/telemetry.js
1309
+ import { z as z11 } from "zod";
1310
+ var Heartbeat = z11.object({
1311
+ load: z11.object({
1312
+ cpu: z11.number(),
1313
+ mem: z11.number(),
1314
+ agents: z11.number().int()
1315
+ }),
1316
+ health: z11.enum(["ok", "degraded"]),
1317
+ activeSessions: z11.number().int(),
1318
+ degradedScopes: z11.array(z11.string()).default([])
1319
+ // e.g. expired-lease bindings (§6)
1320
+ });
1321
+ var EventSession = z11.object({
1322
+ sessionId: z11.string().uuid(),
1323
+ agentId: z11.string().uuid(),
1324
+ launchId: z11.string().uuid(),
1325
+ // 🅰️ ties the event to its launch (§4.4)
1326
+ phase: z11.enum(["start", "plan", "problem", "end"]),
1327
+ link: z11.string().optional(),
1328
+ // deep-link to detail view
1329
+ summary: z11.string().optional(),
1330
+ // short, human-facing milestone text
1331
+ ts: z11.string().datetime()
1332
+ });
1333
+ var FactsRuntimeProfile = z11.object({
1334
+ runtime: z11.string(),
1335
+ // "claude" / "codex" / ...
1336
+ version: z11.string(),
1337
+ models: z11.array(z11.string()),
1338
+ contextWindow: z11.number().int().optional(),
1339
+ acpSupport: z11.enum(["full", "partial", "none"]),
1340
+ // gates the dual-mode decision (#1)
1341
+ toolCalling: z11.boolean()
1342
+ });
1343
+ var ConfigPush = z11.object({
1344
+ keys: z11.record(z11.string(), z11.unknown())
1345
+ });
1346
+ var DaemonRestart = z11.object({
1347
+ reason: z11.string(),
1348
+ drainFirst: z11.boolean().default(true)
1349
+ });
1350
+ var DaemonUpgrade = z11.object({
1351
+ targetVersion: z11.string(),
1352
+ drainFirst: z11.boolean().default(true)
1353
+ });
1354
+ var DaemonControlAck = z11.object({
1355
+ accepted: z11.boolean(),
1356
+ willDrainUntil: z11.string().datetime().optional()
1357
+ });
1358
+ var Ack = z11.object({
1359
+ ok: z11.boolean(),
1360
+ reason: z11.string().optional()
1361
+ });
1362
+
1363
+ // ../protocol/dist/frames/error.js
1364
+ import { z as z12 } from "zod";
1365
+ var ErrorCode = z12.enum([
1366
+ // protocol / framing
1367
+ "UNKNOWN_FRAME",
1368
+ "FRAME_TOO_LARGE",
1369
+ "PROTOCOL_STATE",
1370
+ "BAD_PAYLOAD",
1371
+ // auth / identity
1372
+ "AUTH_FAILED",
1373
+ "ATTESTATION_INVALID",
1374
+ // fencing / ordering
1375
+ "STALE_EPOCH",
1376
+ "STALE_LAUNCH",
1377
+ "SEQ_GAP",
1378
+ // delivery
1379
+ "NO_SESSION",
1380
+ "HELD",
1381
+ "SCOPE_DENIED",
1382
+ // secrets
1383
+ "LEASE_EXPIRED",
1384
+ "LEASE_DENIED",
1385
+ // generic
1386
+ "RATE_LIMITED",
1387
+ "INTERNAL"
1388
+ ]);
1389
+ var ErrorFrame = z12.object({
1390
+ code: ErrorCode,
1391
+ message: z12.string(),
1392
+ // human-readable, redacted of secrets
1393
+ retryable: z12.boolean(),
1394
+ details: z12.record(z12.string(), z12.unknown()).optional()
1395
+ // e.g. {expected: <seq>} for SEQ_GAP
1396
+ });
1397
+
1398
+ // ../protocol/dist/frame.js
1399
+ import { z as z13 } from "zod";
1400
+ var FRAME_SCHEMAS = {
1401
+ // ── auth / identity ──
1402
+ auth: AuthReq,
1403
+ "auth/ok": AuthOk,
1404
+ // ── register / reconcile ──
1405
+ register: RegisterReq,
1406
+ "register/ok": RegisterOk,
1407
+ // ── telemetry ──
1408
+ heartbeat: Heartbeat,
1409
+ // ── agent lifecycle / delivery ──
1410
+ "agent/launch": AgentLaunch,
1411
+ "agent/launched": AgentLaunched,
1412
+ "agent/stop": AgentStop,
1413
+ "agent/prompt": AgentPrompt,
1414
+ "agent/prompt/ack": AgentPromptAck,
1415
+ "agent/activity": AgentActivity,
1416
+ "agent/scope-denied": AgentScopeDenied,
1417
+ // ── routing / orchestration ──
1418
+ "route/assign": RouteAssign,
1419
+ "route/assign/ack": RouteAssignAck,
1420
+ "route/update": RouteUpdate,
1421
+ "daemon/drain": Drain,
1422
+ "drain/progress": DrainProgress,
1423
+ "drain/done": DrainDone,
1424
+ // ── cron ──
1425
+ "cron/upsert": CronUpsert,
1426
+ "cron/remove": CronRemove,
1427
+ // ── secrets ──
1428
+ "secrets/request": SecretsRequest,
1429
+ "secrets/grant": SecretsGrant,
1430
+ "secrets/renew": SecretsRenew,
1431
+ "secrets/revoke": SecretsRevoke,
1432
+ "scope-attestation": ScopeAttestation,
1433
+ // ── dashboard / facts ──
1434
+ "event/session": EventSession,
1435
+ "facts/runtime-profile": FactsRuntimeProfile,
1436
+ // ── fleet / config ──
1437
+ "config/push": ConfigPush,
1438
+ "daemon/restart": DaemonRestart,
1439
+ "daemon/upgrade": DaemonUpgrade,
1440
+ // ── generic replies ──
1441
+ "daemon/control/ack": DaemonControlAck,
1442
+ ack: Ack,
1443
+ // ── error ──
1444
+ error: ErrorFrame
1445
+ };
1446
+ var FRAME_TYPES = Object.keys(FRAME_SCHEMAS);
1447
+ function frame(type, payload) {
1448
+ return z13.object({
1449
+ v: z13.literal(1),
1450
+ id: z13.string().uuid(),
1451
+ ts: z13.string().datetime(),
1452
+ type: z13.literal(type),
1453
+ corr: z13.string().uuid().optional(),
1454
+ payload
1455
+ });
1456
+ }
1457
+ var AnyFrame = z13.discriminatedUnion("type", [
1458
+ frame("auth", FRAME_SCHEMAS["auth"]),
1459
+ frame("auth/ok", FRAME_SCHEMAS["auth/ok"]),
1460
+ frame("register", FRAME_SCHEMAS["register"]),
1461
+ frame("register/ok", FRAME_SCHEMAS["register/ok"]),
1462
+ frame("heartbeat", FRAME_SCHEMAS["heartbeat"]),
1463
+ frame("agent/launch", FRAME_SCHEMAS["agent/launch"]),
1464
+ frame("agent/launched", FRAME_SCHEMAS["agent/launched"]),
1465
+ frame("agent/stop", FRAME_SCHEMAS["agent/stop"]),
1466
+ frame("agent/prompt", FRAME_SCHEMAS["agent/prompt"]),
1467
+ frame("agent/prompt/ack", FRAME_SCHEMAS["agent/prompt/ack"]),
1468
+ frame("agent/activity", FRAME_SCHEMAS["agent/activity"]),
1469
+ frame("agent/scope-denied", FRAME_SCHEMAS["agent/scope-denied"]),
1470
+ frame("route/assign", FRAME_SCHEMAS["route/assign"]),
1471
+ frame("route/assign/ack", FRAME_SCHEMAS["route/assign/ack"]),
1472
+ frame("route/update", FRAME_SCHEMAS["route/update"]),
1473
+ frame("daemon/drain", FRAME_SCHEMAS["daemon/drain"]),
1474
+ frame("drain/progress", FRAME_SCHEMAS["drain/progress"]),
1475
+ frame("drain/done", FRAME_SCHEMAS["drain/done"]),
1476
+ frame("cron/upsert", FRAME_SCHEMAS["cron/upsert"]),
1477
+ frame("cron/remove", FRAME_SCHEMAS["cron/remove"]),
1478
+ frame("secrets/request", FRAME_SCHEMAS["secrets/request"]),
1479
+ frame("secrets/grant", FRAME_SCHEMAS["secrets/grant"]),
1480
+ frame("secrets/renew", FRAME_SCHEMAS["secrets/renew"]),
1481
+ frame("secrets/revoke", FRAME_SCHEMAS["secrets/revoke"]),
1482
+ frame("scope-attestation", FRAME_SCHEMAS["scope-attestation"]),
1483
+ frame("event/session", FRAME_SCHEMAS["event/session"]),
1484
+ frame("facts/runtime-profile", FRAME_SCHEMAS["facts/runtime-profile"]),
1485
+ frame("config/push", FRAME_SCHEMAS["config/push"]),
1486
+ frame("daemon/restart", FRAME_SCHEMAS["daemon/restart"]),
1487
+ frame("daemon/upgrade", FRAME_SCHEMAS["daemon/upgrade"]),
1488
+ frame("daemon/control/ack", FRAME_SCHEMAS["daemon/control/ack"]),
1489
+ frame("ack", FRAME_SCHEMAS["ack"]),
1490
+ frame("error", FRAME_SCHEMAS["error"])
1491
+ ]);
1492
+
1493
+ // ../protocol/dist/codec.js
1494
+ import { randomUUID } from "crypto";
1495
+ var MAX_FRAME_BYTES = 256 * 1024;
1496
+ var textEncoder = new TextEncoder();
1497
+ function byteLength(text) {
1498
+ return textEncoder.encode(text).length;
1499
+ }
1500
+ function extractControlExt(json) {
1501
+ if (typeof json !== "object" || json === null)
1502
+ return void 0;
1503
+ const o = json;
1504
+ const ext = {};
1505
+ if (typeof o.epoch === "number")
1506
+ ext.epoch = o.epoch;
1507
+ if (typeof o.seq === "number")
1508
+ ext.seq = o.seq;
1509
+ if (typeof o.agentId === "string")
1510
+ ext.agentId = o.agentId;
1511
+ if (typeof o.launchId === "string")
1512
+ ext.launchId = o.launchId;
1513
+ return Object.keys(ext).length > 0 ? ext : void 0;
1514
+ }
1515
+ function decodeEnvelope(text) {
1516
+ if (byteLength(text) > MAX_FRAME_BYTES) {
1517
+ return { ok: false, id: NIL_UUID, msg: "FRAME_TOO_LARGE" };
1518
+ }
1519
+ let json;
1520
+ try {
1521
+ json = JSON.parse(text);
1522
+ } catch {
1523
+ return { ok: false, id: NIL_UUID, msg: "invalid json" };
1524
+ }
1525
+ const env = Envelope.safeParse(json);
1526
+ if (!env.success) {
1527
+ const id = typeof json === "object" && json !== null && typeof json.id === "string" ? json.id : NIL_UUID;
1528
+ return { ok: false, id, msg: env.error.message };
1529
+ }
1530
+ const schema = FRAME_SCHEMAS[env.data.type];
1531
+ if (!schema) {
1532
+ return { ok: false, id: env.data.id, msg: "UNKNOWN_FRAME" };
1533
+ }
1534
+ const payload = schema.safeParse(env.data.payload);
1535
+ if (!payload.success) {
1536
+ return { ok: false, id: env.data.id, msg: payload.error.message };
1537
+ }
1538
+ const ext = extractControlExt(json);
1539
+ return {
1540
+ ok: true,
1541
+ frame: { ...env.data, payload: payload.data },
1542
+ ...ext ? { ext } : {}
1543
+ };
1544
+ }
1545
+ function buildEnvelope(type, payload, opts = {}) {
1546
+ const base = {
1547
+ v: 1,
1548
+ id: opts.id ?? randomUUID(),
1549
+ ts: opts.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
1550
+ type,
1551
+ payload,
1552
+ ...opts.corr ? { corr: opts.corr } : {},
1553
+ ...opts.ext ?? {}
1554
+ };
1555
+ return base;
1556
+ }
1557
+ function encode(frame2) {
1558
+ return JSON.stringify(frame2);
1559
+ }
1560
+
1561
+ // src/cp/correlator.ts
1562
+ var WireError = class extends Error {
1563
+ code;
1564
+ retryable;
1565
+ details;
1566
+ constructor(code, message, retryable = false, details) {
1567
+ super(message);
1568
+ this.name = "WireError";
1569
+ this.code = code;
1570
+ this.retryable = retryable;
1571
+ if (details) this.details = details;
1572
+ }
1573
+ };
1574
+ var ReqRep = class {
1575
+ constructor(clock, ackTimeoutMs, maxTries = 5) {
1576
+ this.clock = clock;
1577
+ this.ackTimeoutMs = ackTimeoutMs;
1578
+ this.maxTries = maxTries;
1579
+ }
1580
+ clock;
1581
+ ackTimeoutMs;
1582
+ maxTries;
1583
+ pending = /* @__PURE__ */ new Map();
1584
+ request(frame2, write) {
1585
+ const encoded = JSON.stringify(frame2);
1586
+ return new Promise((resolve3, reject) => {
1587
+ const entry = { id: frame2.id, encoded, resolve: resolve3, reject, tries: 1 };
1588
+ this.pending.set(frame2.id, entry);
1589
+ this.arm(entry, write);
1590
+ write(encoded);
1591
+ });
1592
+ }
1593
+ arm(entry, write) {
1594
+ entry.timer = this.clock.setTimeout(() => {
1595
+ if (!this.pending.has(entry.id)) return;
1596
+ if (entry.tries >= this.maxTries) {
1597
+ this.pending.delete(entry.id);
1598
+ entry.reject(new WireError("INTERNAL", `no ack after ${this.maxTries} tries for ${entry.id}`, true));
1599
+ return;
1600
+ }
1601
+ entry.tries += 1;
1602
+ this.arm(entry, write);
1603
+ write(entry.encoded);
1604
+ }, this.ackTimeoutMs);
1605
+ }
1606
+ settle(frame2) {
1607
+ const corr = frame2.corr;
1608
+ if (!corr) return false;
1609
+ const entry = this.pending.get(corr);
1610
+ if (!entry) return false;
1611
+ this.pending.delete(corr);
1612
+ if (entry.timer !== void 0) this.clock.clearTimeout(entry.timer);
1613
+ if (frame2.type === "error") {
1614
+ const e = frame2.payload;
1615
+ entry.reject(new WireError(e.code, e.message, e.retryable, e.details));
1616
+ } else {
1617
+ entry.resolve(frame2);
1618
+ }
1619
+ return true;
1620
+ }
1621
+ inflight() {
1622
+ return this.pending.size;
1623
+ }
1624
+ rejectAll(err) {
1625
+ for (const entry of this.pending.values()) {
1626
+ if (entry.timer !== void 0) this.clock.clearTimeout(entry.timer);
1627
+ entry.reject(err);
1628
+ }
1629
+ this.pending.clear();
1630
+ }
1631
+ };
1632
+
1633
+ // src/cp/client.ts
1634
+ var ACK_TIMEOUT_MS = 5e3;
1635
+ var BACKOFF_BASE_MS = 1e3;
1636
+ var BACKOFF_CAP_MS = 3e4;
1637
+ var CpClient = class {
1638
+ constructor(deps) {
1639
+ this.deps = deps;
1640
+ this.correlator = new ReqRep(deps.clock, ACK_TIMEOUT_MS);
1641
+ }
1642
+ deps;
1643
+ state = "CLOSED";
1644
+ sessionEpoch = 0;
1645
+ routingEpoch = 0;
1646
+ transport;
1647
+ correlator;
1648
+ stopped = false;
1649
+ fatal = false;
1650
+ // 4401 — never auto-retry
1651
+ attempt = 0;
1652
+ reconnectTimer;
1653
+ lastAuthedEpoch = 0;
1654
+ // for resume on reconnect (per-agent seq tail is out of scope)
1655
+ heartbeatTimer;
1656
+ heartbeatMs = 0;
1657
+ /** Non-blocking: kicks off the connect loop and returns. */
1658
+ start() {
1659
+ this.stopped = false;
1660
+ this.fatal = false;
1661
+ void this.attemptConnect();
1662
+ }
1663
+ async stop() {
1664
+ this.stopped = true;
1665
+ if (this.reconnectTimer !== void 0) {
1666
+ this.deps.clock.clearTimeout(this.reconnectTimer);
1667
+ this.reconnectTimer = void 0;
1668
+ }
1669
+ this.stopHeartbeat();
1670
+ this.correlator.rejectAll(new Error("stopping"));
1671
+ this.transport?.close(1e3, "shutdown");
1672
+ this.state = "CLOSED";
1673
+ }
1674
+ async attemptConnect() {
1675
+ if (this.stopped || this.fatal) return;
1676
+ this.state = "CONNECTING";
1677
+ try {
1678
+ const t = await this.deps.connect();
1679
+ this.transport = t;
1680
+ t.onMessage((txt) => void this.onText(txt));
1681
+ t.onClose((c, r) => this.onClose(c, r));
1682
+ await this.handshake();
1683
+ this.attempt = 0;
1684
+ } catch (err) {
1685
+ this.deps.log.warn(`cp: connect/handshake failed: ${err.message}`);
1686
+ this.transport?.close(1011, "handshake failed");
1687
+ this.transport = void 0;
1688
+ this.scheduleReconnect();
1689
+ }
1690
+ }
1691
+ scheduleReconnect() {
1692
+ if (this.stopped || this.fatal) return;
1693
+ if (this.reconnectTimer !== void 0) return;
1694
+ const jitter = this.deps.jitter ?? Math.random;
1695
+ const base = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** this.attempt);
1696
+ const delay = base + Math.floor(jitter() * base);
1697
+ this.attempt += 1;
1698
+ this.reconnectTimer = this.deps.clock.setTimeout(() => {
1699
+ this.reconnectTimer = void 0;
1700
+ void this.attemptConnect();
1701
+ }, delay);
1702
+ }
1703
+ async handshake() {
1704
+ this.state = "AUTHENTICATING";
1705
+ const authPayload = {
1706
+ daemonToken: this.deps.token,
1707
+ agentVersion: this.deps.agentVersion
1708
+ };
1709
+ if (this.deps.daemonId) {
1710
+ authPayload.daemonId = this.deps.daemonId;
1711
+ }
1712
+ if (this.lastAuthedEpoch > 0) {
1713
+ authPayload.resume = { lastEpoch: this.lastAuthedEpoch, lastRecvSeq: {} };
1714
+ }
1715
+ const authOk = await this.request("auth", authPayload);
1716
+ const ok = authOk.payload;
1717
+ this.sessionEpoch = ok.sessionEpoch;
1718
+ this.lastAuthedEpoch = ok.sessionEpoch;
1719
+ if (ok.daemonId && ok.daemonId !== this.deps.daemonId) {
1720
+ this.deps.daemonId = ok.daemonId;
1721
+ this.deps.onDaemonId?.(ok.daemonId);
1722
+ }
1723
+ this.state = "REGISTERING";
1724
+ const regOk = await this.request("register", {
1725
+ host: this.deps.host,
1726
+ capabilities: this.deps.capabilities(),
1727
+ maxAgents: this.deps.maxAgents,
1728
+ localState: this.deps.localState()
1729
+ });
1730
+ const snap = regOk.payload;
1731
+ this.routingEpoch = snap.routingEpoch;
1732
+ this.deps.configApply.applyReconcileSnapshot(snap);
1733
+ this.state = "READY";
1734
+ this.heartbeatMs = ok.heartbeatSec > 0 ? ok.heartbeatSec * 1e3 : this.deps.heartbeatDefaultMs;
1735
+ this.armHeartbeat();
1736
+ this.deps.log.info(`cp: READY (epoch=${this.sessionEpoch}, routingEpoch=${this.routingEpoch})`);
1737
+ }
1738
+ request(type, payload) {
1739
+ const frame2 = buildEnvelope(type, payload);
1740
+ return this.correlator.request(frame2, (e) => this.transport.send(e));
1741
+ }
1742
+ async onText(text) {
1743
+ const decoded = decodeEnvelope(text);
1744
+ if (!decoded.ok) {
1745
+ this.sendError(decoded.id, this.decodeErrorCode(decoded.msg), decoded.msg, false);
1746
+ return;
1747
+ }
1748
+ const frame2 = decoded.frame;
1749
+ if (frame2.corr && this.correlator.settle(frame2)) return;
1750
+ if (this.state !== "READY" && this.state !== "DRAINING") {
1751
+ this.sendError(frame2.id, "PROTOCOL_STATE", `${frame2.type} illegal in ${this.state}`, false);
1752
+ return;
1753
+ }
1754
+ if (decoded.ext?.epoch !== void 0 && decoded.ext.epoch < this.sessionEpoch) {
1755
+ this.sendError(frame2.id, "STALE_EPOCH", "epoch < current", true);
1756
+ return;
1757
+ }
1758
+ this.dispatchControl(frame2);
1759
+ }
1760
+ decodeErrorCode(msg) {
1761
+ if (msg === "FRAME_TOO_LARGE") return "FRAME_TOO_LARGE";
1762
+ if (msg === "UNKNOWN_FRAME") return "UNKNOWN_FRAME";
1763
+ return "BAD_PAYLOAD";
1764
+ }
1765
+ sendError(corr, code, message, retryable) {
1766
+ if (!this.transport) return;
1767
+ this.transport.send(encode(buildEnvelope("error", { code, message, retryable }, { corr })));
1768
+ }
1769
+ onClose(code, _reason) {
1770
+ this.stopHeartbeat();
1771
+ this.correlator.rejectAll(new WireError("INTERNAL", "connection closed", true));
1772
+ if (code === 4401) {
1773
+ this.fatal = true;
1774
+ this.state = "CLOSED";
1775
+ this.deps.log.error("cp: AUTH_FAILED (4401) \u2014 not reconnecting; re-mint the daemon token");
1776
+ return;
1777
+ }
1778
+ if (this.stopped) {
1779
+ this.state = "CLOSED";
1780
+ return;
1781
+ }
1782
+ this.state = "DEGRADED";
1783
+ this.scheduleReconnect();
1784
+ }
1785
+ armHeartbeat() {
1786
+ this.heartbeatTimer = this.deps.clock.setTimeout(() => {
1787
+ if (this.state !== "READY") return;
1788
+ this.transport?.send(
1789
+ encode(
1790
+ buildEnvelope("heartbeat", {
1791
+ load: this.deps.loadSnapshot(),
1792
+ health: "ok",
1793
+ activeSessions: this.deps.activeSessions(),
1794
+ degradedScopes: this.deps.degradedScopes?.() ?? []
1795
+ })
1796
+ )
1797
+ );
1798
+ this.armHeartbeat();
1799
+ }, this.heartbeatMs);
1800
+ }
1801
+ stopHeartbeat() {
1802
+ if (this.heartbeatTimer !== void 0) {
1803
+ this.deps.clock.clearTimeout(this.heartbeatTimer);
1804
+ this.heartbeatTimer = void 0;
1805
+ }
1806
+ }
1807
+ /** C→D control dispatch. The CP changes config, never live routing. */
1808
+ dispatchControl(frame2) {
1809
+ switch (frame2.type) {
1810
+ case "config/push":
1811
+ this.deps.configApply.applyConfigPush(frame2.payload.keys);
1812
+ return;
1813
+ // EVT — no reply
1814
+ case "cron/upsert":
1815
+ try {
1816
+ this.deps.configApply.upsertCron(frame2.payload);
1817
+ this.reply(frame2, "ack", { ok: true });
1818
+ } catch (err) {
1819
+ this.sendError(frame2.id, "BAD_PAYLOAD", `cron upsert failed: ${err.message}`, false);
1820
+ }
1821
+ return;
1822
+ case "cron/remove":
1823
+ this.deps.configApply.removeCron(frame2.payload.cronId);
1824
+ this.reply(frame2, "ack", { ok: true });
1825
+ return;
1826
+ case "route/assign": {
1827
+ const a = frame2.payload;
1828
+ this.deps.configApply.applyRouteAssign(a);
1829
+ this.reply(frame2, "route/assign/ack", { ok: true, sessionKey: a.sessionKey });
1830
+ return;
1831
+ }
1832
+ case "route/update":
1833
+ this.deps.configApply.applyRouteUpdate(frame2.payload);
1834
+ return;
1835
+ // EVT — no reply
1836
+ // ── unimplemented this slice ──
1837
+ case "agent/launch":
1838
+ case "agent/stop":
1839
+ case "agent/prompt":
1840
+ case "daemon/drain":
1841
+ case "daemon/restart":
1842
+ case "daemon/upgrade":
1843
+ this.sendError(frame2.id, "INTERNAL", `${frame2.type} not implemented`, false);
1844
+ return;
1845
+ default:
1846
+ this.deps.log.debug(`cp: ignoring ${frame2.type}`);
1847
+ return;
1848
+ }
1849
+ }
1850
+ reply(req, type, payload) {
1851
+ this.transport?.send(encode(buildEnvelope(type, payload, { corr: req.id })));
1852
+ }
1853
+ };
1854
+
1855
+ // src/cp/transport.ts
1856
+ import WebSocket from "ws";
1857
+ var SUBPROTOCOL = "agentconnect.v1";
1858
+ var DEFAULT_WS_PATH = "/daemon/ws";
1859
+ var ClientTransport = class _ClientTransport {
1860
+ constructor(ws) {
1861
+ this.ws = ws;
1862
+ }
1863
+ ws;
1864
+ get subprotocol() {
1865
+ return this.ws.protocol;
1866
+ }
1867
+ /**
1868
+ * Dial `wss://<host>/daemon/ws` with the `agentconnect.v1` subprotocol.
1869
+ * Resolves once the socket is open; rejects on any error before open
1870
+ * (refused, bad handshake/subprotocol → HTTP 400).
1871
+ */
1872
+ static dial(url) {
1873
+ const wsUrl = url.endsWith(DEFAULT_WS_PATH) ? url : url.replace(/\/+$/, "") + DEFAULT_WS_PATH;
1874
+ return new Promise((resolve3, reject) => {
1875
+ const ws = new WebSocket(wsUrl, [SUBPROTOCOL], { maxPayload: MAX_FRAME_BYTES });
1876
+ const onPreOpenError = (err) => {
1877
+ ws.removeAllListeners();
1878
+ reject(err);
1879
+ };
1880
+ ws.once("error", onPreOpenError);
1881
+ ws.once("open", () => {
1882
+ ws.removeListener("error", onPreOpenError);
1883
+ resolve3(new _ClientTransport(ws));
1884
+ });
1885
+ });
1886
+ }
1887
+ send(text) {
1888
+ this.ws.send(text);
1889
+ }
1890
+ onMessage(cb) {
1891
+ this.ws.on("message", (data, isBinary) => {
1892
+ if (isBinary) return;
1893
+ cb(typeof data === "string" ? data : String(data));
1894
+ });
1895
+ }
1896
+ onClose(cb) {
1897
+ this.ws.on("close", (code, reason) => cb(code, reason.toString()));
1898
+ }
1899
+ close(code, reason) {
1900
+ this.ws.close(code, reason);
1901
+ }
1902
+ };
1903
+
1904
+ // src/cp/cp-cron.ts
1905
+ import { Cron as Cron2 } from "croner";
1906
+ var CpCronRegistry = class {
1907
+ constructor(deps) {
1908
+ this.deps = deps;
1909
+ }
1910
+ deps;
1911
+ jobs = /* @__PURE__ */ new Map();
1912
+ upsert(cron) {
1913
+ this.remove(cron.cronId);
1914
+ if (cron.enabled === false) return;
1915
+ this.jobs.set(cron.cronId, new Cron2(cron.schedule, () => this.deps.onFire(cron)));
1916
+ }
1917
+ remove(cronId) {
1918
+ this.jobs.get(cronId)?.stop();
1919
+ this.jobs.delete(cronId);
1920
+ }
1921
+ converge(crons) {
1922
+ const keep = new Set(crons.map((c) => c.cronId));
1923
+ for (const id of [...this.jobs.keys()]) if (!keep.has(id)) this.remove(id);
1924
+ for (const c of crons) this.upsert(c);
1925
+ }
1926
+ ids() {
1927
+ return [...this.jobs.keys()];
1928
+ }
1929
+ stop() {
1930
+ for (const id of [...this.jobs.keys()]) this.remove(id);
1931
+ }
1932
+ };
1933
+
1934
+ // src/cp/config-apply.ts
1935
+ var LOG_LEVELS = /* @__PURE__ */ new Set(["trace", "debug", "info", "warn", "error"]);
1936
+ function mergeConfigPush(cfg, keys) {
1937
+ const applied = [];
1938
+ const ignored = [];
1939
+ for (const [key, value] of Object.entries(keys)) {
1940
+ let ok = false;
1941
+ switch (key) {
1942
+ case "logging.level":
1943
+ if (typeof value === "string" && LOG_LEVELS.has(value)) {
1944
+ cfg.logging.level = value;
1945
+ ok = true;
1946
+ }
1947
+ break;
1948
+ case "limits.maxAgents":
1949
+ if (typeof value === "number" && Number.isInteger(value)) {
1950
+ cfg.limits.maxAgents = value;
1951
+ ok = true;
1952
+ }
1953
+ break;
1954
+ case "limits.maxConcurrentSessions":
1955
+ if (typeof value === "number" && Number.isInteger(value)) {
1956
+ cfg.limits.maxConcurrentSessions = value;
1957
+ ok = true;
1958
+ }
1959
+ break;
1960
+ case "limits.agentIdleTimeoutMs":
1961
+ if (typeof value === "number" && Number.isInteger(value)) {
1962
+ cfg.limits.agentIdleTimeoutMs = value;
1963
+ ok = true;
1964
+ }
1965
+ break;
1966
+ default:
1967
+ ok = false;
1968
+ }
1969
+ ;
1970
+ (ok ? applied : ignored).push(key);
1971
+ }
1972
+ return { applied, ignored };
1973
+ }
1974
+
1975
+ // src/cp/clock.ts
1976
+ var SystemClock = class {
1977
+ now() {
1978
+ return Date.now();
1979
+ }
1980
+ setTimeout(fn, ms) {
1981
+ return globalThis.setTimeout(fn, ms);
1982
+ }
1983
+ clearTimeout(h) {
1984
+ globalThis.clearTimeout(h);
1985
+ }
1986
+ };
1987
+ var systemClock = new SystemClock();
1988
+
1989
+ // src/daemon.ts
1990
+ var LOADING_MSGS = ["Working on it\u2026", "Crunching through it\u2026", "Hang tight\u2026"];
1991
+ var Daemon = class {
1992
+ constructor(opts = {}) {
1993
+ this.opts = opts;
1994
+ }
1995
+ opts;
1996
+ store;
1997
+ agents = /* @__PURE__ */ new Map();
1998
+ hosts = /* @__PURE__ */ new Map();
1999
+ connections = [];
2000
+ scheduler;
2001
+ sessions;
2002
+ // integrationId -> the SlackConnection that owns it (for replies)
2003
+ connByIntegration = /* @__PURE__ */ new Map();
2004
+ hostStarts = /* @__PURE__ */ new Map();
2005
+ watcher;
2006
+ debounceTimer;
2007
+ agentsDir = "";
2008
+ cfg;
2009
+ log = makeLogger("info");
2010
+ root = "";
2011
+ runtimes = {};
2012
+ cpClient;
2013
+ cpCrons;
2014
+ botUserIds = {};
2015
+ cpRouting;
2016
+ async start() {
2017
+ const root = resolveRoot(this.opts.root);
2018
+ const cfg = loadConfig({ root, overrides: this.opts.overrides, optional: !!this.opts.agentName, autoCreate: true });
2019
+ this.cfg = cfg;
2020
+ const cpTokenOnboarding = !!(cfg.controlPlane?.enabled && cfg.controlPlane.token);
2021
+ if (!cfg.daemonId && !cpTokenOnboarding) {
2022
+ cfg.daemonId = randomUUID2();
2023
+ persistDaemonId(root, cfg.daemonId);
2024
+ }
2025
+ this.log = makeLogger(cfg.logging.level);
2026
+ this.log.info(`starting daemon (root=${root})`);
2027
+ this.log.info(
2028
+ `control plane: ${cfg.controlPlane?.enabled ? `enabled (${cfg.controlPlane.url ?? "no url"})` : "disabled \u2014 running local"}`
2029
+ );
2030
+ this.agentsDir = cfg.agentsDir;
2031
+ const agents = this.loadAgentList();
2032
+ for (const a of agents) this.agents.set(a.id, a);
2033
+ this.log.info(
2034
+ `loaded ${agents.length} agent(s) from ${this.agentsDir}${agents.length ? `: ${agents.map((a) => a.id).join(", ")}` : ""}`
2035
+ );
2036
+ this.root = root;
2037
+ this.runtimes = await resolveRuntimes(cfg, root, {
2038
+ neededRuntimes: agents.map((a) => a.runtime),
2039
+ mode: "cache-first"
2040
+ });
2041
+ this.log.info(`runtimes ready: ${Object.keys(this.runtimes).join(", ") || "(none)"}`);
2042
+ this.store = new LocalStore(statePath(root));
2043
+ this.cpRouting = new CpRoutingLayer({
2044
+ load: () => {
2045
+ const row = this.store.getCpRouting();
2046
+ return row ? {
2047
+ routingEpoch: row.routingEpoch,
2048
+ assignments: JSON.parse(row.assignments),
2049
+ globalRules: JSON.parse(row.globalRules)
2050
+ } : void 0;
2051
+ },
2052
+ save: (s) => this.store.setCpRouting(s.routingEpoch, JSON.stringify(s.assignments), JSON.stringify(s.globalRules))
2053
+ });
2054
+ this.sessions = new SessionManager({
2055
+ store: this.store,
2056
+ // Must hand back a *started* host: handle() calls host.newSession() immediately,
2057
+ // which needs the ACP connection that start() establishes.
2058
+ hostFor: (agentId) => this.ensureHostAsync(agentId),
2059
+ agentById: (id) => this.agents.get(id)
2060
+ });
2061
+ this.scheduler = new Scheduler({
2062
+ onFire: (agentId, msg) => void this.dispatch(agentId, msg).catch(
2063
+ (err) => this.log.error(`cron dispatch failed for agent "${agentId}": ${err.stack ?? err}`)
2064
+ ),
2065
+ newTraceId: () => randomUUID2()
2066
+ });
2067
+ const groups = consolidate(agents);
2068
+ this.botUserIds = {};
2069
+ if (groups.size === 0) this.log.info("slack: no slack integrations configured");
2070
+ else this.log.info(`slack: opening ${groups.size} socket connection(s)`);
2071
+ for (const group of groups.values()) {
2072
+ const conn = new SlackConnection({
2073
+ group,
2074
+ newTraceId: () => randomUUID2(),
2075
+ onMessage: (msg) => this.onInbound(msg),
2076
+ log: this.log,
2077
+ boltDebug: cfg.logging.level === "debug" || cfg.logging.level === "trace"
2078
+ });
2079
+ this.log.info(
2080
+ `slack: connecting (${group.integrations.length} integration(s): ${group.integrations.map((i) => i.agentId).join(", ")})\u2026`
2081
+ );
2082
+ await conn.start();
2083
+ this.log.info(`slack: socket connected as bot user ${conn.botUserId}`);
2084
+ for (const { integrationId } of group.integrations) {
2085
+ this.botUserIds[integrationId] = conn.botUserId;
2086
+ this.connByIntegration.set(integrationId, conn);
2087
+ }
2088
+ this.connections.push(conn);
2089
+ }
2090
+ let cronCount = 0;
2091
+ for (const a of agents)
2092
+ for (const c of a.crons) {
2093
+ this.scheduler.register(a.id, c);
2094
+ cronCount++;
2095
+ }
2096
+ if (cronCount) this.log.info(`registered ${cronCount} cron(s)`);
2097
+ this.watcher = chokidar.watch(this.agentsDir, {
2098
+ ignoreInitial: true,
2099
+ depth: 4,
2100
+ ignored: (p) => /[\\/](node_modules|\.git)([\\/]|$)/.test(p)
2101
+ });
2102
+ const debounced = () => {
2103
+ clearTimeout(this.debounceTimer);
2104
+ this.debounceTimer = setTimeout(() => {
2105
+ void this.reconcile().catch((err) => console.error("agentconnect: reconcile failed:", err));
2106
+ }, 300);
2107
+ };
2108
+ this.watcher.on("add", debounced).on("change", debounced).on("unlink", debounced);
2109
+ this.log.info(`watching ${this.agentsDir} for agent changes`);
2110
+ this.startCpClient(root);
2111
+ this.log.info("daemon ready");
2112
+ }
2113
+ // Multi-agent: all active agents under agentsDir. Single-agent (--agent): just
2114
+ // the selected agent, regardless of status.
2115
+ loadAgentList() {
2116
+ return this.opts.agentName ? [selectAgent(this.agentsDir, this.opts.agentName)] : loadAgents(this.agentsDir);
2117
+ }
2118
+ async reconcile() {
2119
+ const desired = this.loadAgentList();
2120
+ const { toStart, toStop } = diffAgents(desired, [...this.agents.keys()]);
2121
+ for (const id of toStop) {
2122
+ const host = this.hosts.get(id);
2123
+ if (host) {
2124
+ await host.stop();
2125
+ this.hosts.delete(id);
2126
+ }
2127
+ this.hostStarts.delete(id);
2128
+ this.agents.delete(id);
2129
+ }
2130
+ for (const a of toStart) this.agents.set(a.id, a);
2131
+ }
2132
+ ensureHost(agentId, cfg) {
2133
+ let host = this.hosts.get(agentId);
2134
+ if (host) return host;
2135
+ const agent2 = this.agents.get(agentId);
2136
+ const onUpdate = (sid, u) => this.onAcpUpdate(agentId, sid, u);
2137
+ if (this.opts.hostFactory) {
2138
+ host = this.opts.hostFactory(agent2, onUpdate);
2139
+ } else {
2140
+ const runtime = this.runtimes[agent2.runtime];
2141
+ if (!runtime) throw new Error(`runtime "${agent2.runtime}" not in config.runtimes or ACP registry`);
2142
+ host = new AcpHost(runtime, { onUpdate, env: agentChildEnv(agent2) });
2143
+ }
2144
+ this.hosts.set(agentId, host);
2145
+ return host;
2146
+ }
2147
+ // route an inbound Slack message
2148
+ seenMsgIds = /* @__PURE__ */ new Set();
2149
+ onInbound(msg) {
2150
+ if (this.seenMsgIds.has(msg.msgId)) {
2151
+ this.log.debug(`routing: duplicate ${msg.msgId} ignored`);
2152
+ return;
2153
+ }
2154
+ this.seenMsgIds.add(msg.msgId);
2155
+ if (this.seenMsgIds.size > 2e3) this.seenMsgIds.clear();
2156
+ const result = routeRules(msg, this.mergedRules(), (c, t) => this.sessions.threadOwner(c, t));
2157
+ if (!result) {
2158
+ this.log.debug(
2159
+ `routing: dropped message in ch=${msg.channel} (no agent matched \u2014 not a mention of a known bot, not a subscribed 'all' channel, not a thread/DM hit)`
2160
+ );
2161
+ return;
2162
+ }
2163
+ this.log.info(`routing: ch=${msg.channel} \u2192 agent "${result.agentId}" (integration ${result.integrationId})`);
2164
+ void this.dispatch(result.agentId, msg, result.integrationId).catch(
2165
+ (err) => this.log.error(`dispatch failed for agent "${result.agentId}": ${err.stack ?? err}`)
2166
+ );
2167
+ }
2168
+ /** Local layer (agent.json) ∪ resolved CP layer; unservable CP rules are dropped + warn-logged. */
2169
+ mergedRules() {
2170
+ const local = [...this.agents.values()].flatMap((a) => rulesFromAgent(a, this.botUserIds));
2171
+ const cp = [];
2172
+ for (const cpRule of this.cpRouting?.effectiveRules() ?? []) {
2173
+ const resolved = resolveCpRule(cpRule, (agentId) => this.resolveCpAgent(agentId));
2174
+ if (resolved) cp.push(resolved);
2175
+ else this.log.warn(`cp: routing rule for unknown/Slack-less agent "${cpRule.agentId}" skipped (degraded)`);
2176
+ }
2177
+ return [...local, ...cp];
2178
+ }
2179
+ /** Resolve a CP agentId (== local agent.id) to its Slack integration; null if unservable. */
2180
+ resolveCpAgent(agentId) {
2181
+ return resolveAgentIntegration(this.agents.get(agentId), this.botUserIds);
2182
+ }
2183
+ /** agentIds of CP rules that currently resolve to null (no servable Slack integration). */
2184
+ cpDegradedScopes() {
2185
+ const out = /* @__PURE__ */ new Set();
2186
+ for (const cpRule of this.cpRouting?.effectiveRules() ?? []) {
2187
+ if (!this.resolveCpAgent(cpRule.agentId)) out.add(cpRule.agentId);
2188
+ }
2189
+ return [...out];
2190
+ }
2191
+ // shared dispatch for user + cron messages
2192
+ async dispatch(agentId, msg, integrationId) {
2193
+ const agent2 = this.agents.get(agentId);
2194
+ const conv = new OutputConverger(agent2.output.mode);
2195
+ const replyConn = this.replyConnFor(agentId, integrationId);
2196
+ const wasRunning = this.hostStarts.has(agentId);
2197
+ const statusThread = msg.thread ?? msg.msgId;
2198
+ void replyConn?.setStatus(msg.channel, statusThread, wasRunning ? "is thinking\u2026" : "is starting up\u2026", LOADING_MSGS);
2199
+ const { sessionId, blocks } = await this.sessions.handle(agentId, msg);
2200
+ this.pending.set(sessionId, { conv, channel: msg.channel, thread: msg.thread, conn: replyConn });
2201
+ try {
2202
+ const host = await this.ensureHostAsync(agentId);
2203
+ if (!wasRunning) void replyConn?.setStatus(msg.channel, statusThread, "is thinking\u2026", LOADING_MSGS);
2204
+ await host.prompt(sessionId, blocks);
2205
+ for (const action of conv.onFinal(`local://session/${sessionId}`)) {
2206
+ await this.applyAction(action, replyConn, msg.channel, statusThread);
2207
+ }
2208
+ } finally {
2209
+ this.pending.delete(sessionId);
2210
+ }
2211
+ }
2212
+ /** Route a converger action: set-status → setStatus (loading_messages only when not clearing); else postMessage. */
2213
+ async applyAction(action, conn, channel, thread) {
2214
+ if (action.kind === "set-status") {
2215
+ if (conn && thread) await conn.setStatus(channel, thread, action.text, action.text ? LOADING_MSGS : void 0);
2216
+ return;
2217
+ }
2218
+ await conn?.postMessage(channel, action.text, thread);
2219
+ }
2220
+ pending = /* @__PURE__ */ new Map();
2221
+ onAcpUpdate(_agentId, sessionId, update) {
2222
+ const p = this.pending.get(sessionId);
2223
+ if (!p) return;
2224
+ for (const action of p.conv.onUpdate(update)) {
2225
+ void this.applyAction(action, p.conn, p.channel, p.thread).catch(
2226
+ (err) => console.error("slack post failed:", err)
2227
+ );
2228
+ }
2229
+ }
2230
+ async ensureHostAsync(agentId) {
2231
+ const host = this.ensureHost(agentId, this.cfg);
2232
+ let p = this.hostStarts.get(agentId);
2233
+ if (!p) {
2234
+ p = host.start();
2235
+ this.hostStarts.set(agentId, p);
2236
+ }
2237
+ await p;
2238
+ return host;
2239
+ }
2240
+ replyConnFor(agentId, integrationId) {
2241
+ const intId = integrationId ?? this.agents.get(agentId)?.integrations[0]?.id;
2242
+ return intId ? this.connByIntegration.get(intId) : void 0;
2243
+ }
2244
+ // ── ConfigApply seam (CP changes config, never live routing) ──
2245
+ cpConfigApply() {
2246
+ return {
2247
+ applyConfigPush: (keys) => {
2248
+ const { applied, ignored } = mergeConfigPush(this.cfg, keys);
2249
+ if (applied.includes("logging.level")) this.log = makeLogger(this.cfg.logging.level);
2250
+ if (applied.length) this.log.info(`cp: applied config keys: ${applied.join(", ")}`);
2251
+ if (ignored.length) this.log.warn(`cp: ignored config keys: ${ignored.join(", ")}`);
2252
+ },
2253
+ applyReconcileSnapshot: (snap) => {
2254
+ this.cpCrons?.converge(snap.crons);
2255
+ this.cpRouting?.converge({
2256
+ routingEpoch: snap.routingEpoch,
2257
+ assignments: snap.assignments,
2258
+ drop: { assignments: snap.drop.assignments }
2259
+ });
2260
+ if (snap.leases.length) this.log.debug(`cp: ${snap.leases.length} lease(s) noted (secrets handled later)`);
2261
+ if (snap.assignments.length) this.log.debug(`cp: converged ${snap.assignments.length} assignment(s)`);
2262
+ },
2263
+ upsertCron: (cron) => this.cpCrons.upsert(cron),
2264
+ removeCron: (cronId) => this.cpCrons.remove(cronId),
2265
+ applyRouteAssign: (a) => this.cpRouting?.upsertAssign(a),
2266
+ applyRouteUpdate: (u) => this.cpRouting?.applyUpdate(u)
2267
+ };
2268
+ }
2269
+ /** A CP cron fired: build a synthetic message and run it through the normal routing path. */
2270
+ onCpCronFire(cron) {
2271
+ const traceId = randomUUID2();
2272
+ const msg = {
2273
+ msgId: `cpcron:${cron.cronId}:${traceId}`,
2274
+ traceId,
2275
+ source: "cron",
2276
+ platform: "slack",
2277
+ channel: cron.target.channel,
2278
+ thread: `cpcron:${cron.cronId}:${traceId}`,
2279
+ sender: { id: `cpcron:${cron.cronId}`, isBot: false },
2280
+ text: cron.trigger,
2281
+ mentionedBots: [],
2282
+ isDm: false,
2283
+ trigger: "cron"
2284
+ };
2285
+ this.onInbound(msg);
2286
+ }
2287
+ startCpClient(root) {
2288
+ const cp = this.cfg.controlPlane;
2289
+ if (!cp?.enabled || !cp.url || !cp.token) {
2290
+ this.log.info("cp: not connecting (disabled or missing url/token) \u2014 running local");
2291
+ return;
2292
+ }
2293
+ this.cpCrons = new CpCronRegistry({ onFire: (cron) => this.onCpCronFire(cron) });
2294
+ const url = cp.url;
2295
+ this.cpClient = new CpClient({
2296
+ url,
2297
+ token: cp.token,
2298
+ ...this.cfg.daemonId ? { daemonId: this.cfg.daemonId } : {},
2299
+ onDaemonId: (id) => {
2300
+ this.cfg.daemonId = id;
2301
+ persistDaemonId(root, id);
2302
+ this.log.info(`cp: adopted daemonId ${id} from auth/ok`);
2303
+ },
2304
+ agentVersion: "0.0.0",
2305
+ host: hostname(),
2306
+ heartbeatDefaultMs: cp.heartbeatMs,
2307
+ maxAgents: this.cfg.limits.maxAgents,
2308
+ capabilities: () => ({
2309
+ platforms: ["slack"],
2310
+ runtimes: Object.keys(this.runtimes),
2311
+ acp: true,
2312
+ features: []
2313
+ }),
2314
+ localState: () => ({ assignments: [], crons: this.cpCrons?.ids() ?? [], leases: [] }),
2315
+ loadSnapshot: () => ({
2316
+ cpu: loadavg()[0] ?? 0,
2317
+ mem: totalmem() > 0 ? 1 - freemem() / totalmem() : 0,
2318
+ agents: this.hosts.size
2319
+ }),
2320
+ activeSessions: () => this.pending.size,
2321
+ degradedScopes: () => this.cpDegradedScopes(),
2322
+ configApply: this.cpConfigApply(),
2323
+ clock: systemClock,
2324
+ connect: () => ClientTransport.dial(url),
2325
+ log: this.log
2326
+ });
2327
+ this.cpClient.start();
2328
+ this.log.info(`cp: connecting to ${url}\u2026`);
2329
+ }
2330
+ async stop() {
2331
+ clearTimeout(this.debounceTimer);
2332
+ await this.watcher?.close();
2333
+ const errors = [];
2334
+ this.scheduler?.stop();
2335
+ await Promise.resolve(this.cpClient?.stop()).catch((e) => errors.push(e));
2336
+ this.cpCrons?.stop();
2337
+ for (const c of this.connections) await Promise.resolve(c.stop()).catch((e) => errors.push(e));
2338
+ for (const h of this.hosts.values()) await Promise.resolve(h.stop()).catch((e) => errors.push(e));
2339
+ this.store?.close();
2340
+ if (errors.length) throw new AggregateError(errors, "stop: partial failure");
2341
+ }
2342
+ };
2343
+
2344
+ // src/cli/chat.ts
2345
+ import { createInterface } from "readline";
2346
+ function renderUpdate(out) {
2347
+ return (_sessionId, update) => {
2348
+ if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") {
2349
+ out.write(update.content.text);
2350
+ } else if (update.sessionUpdate === "tool_call") {
2351
+ const title = "title" in update && update.title || update.toolCallId || "tool";
2352
+ out.write(`
2353
+ [tool] ${title}
2354
+ `);
2355
+ }
2356
+ };
2357
+ }
2358
+ async function runChat(opts) {
2359
+ const out = opts.out ?? process.stdout;
2360
+ const root = resolveRoot(opts.root);
2361
+ const cfg = loadConfig({
2362
+ root: opts.root,
2363
+ configPath: opts.configPath,
2364
+ optional: true,
2365
+ overrides: { agentsDir: opts.agentsDir }
2366
+ });
2367
+ const agent2 = selectAgent(cfg.agentsDir, opts.agentName);
2368
+ const runtimes = await resolveRuntimes(cfg, root, { neededRuntimes: [agent2.runtime], mode: "cache-first" });
2369
+ const runtime = runtimes[agent2.runtime];
2370
+ if (!runtime) {
2371
+ const available = Object.keys(runtimes).sort().join(", ") || "(none)";
2372
+ throw new Error(`runtime "${agent2.runtime}" not found. Available: ${available}`);
2373
+ }
2374
+ const host = new AcpHost(runtime, { onUpdate: renderUpdate(out), env: agentChildEnv(agent2) });
2375
+ await host.start();
2376
+ try {
2377
+ const cwd = await prepareWorkspace(agent2);
2378
+ const sessionId = await host.newSession(cwd);
2379
+ const send = async (text) => {
2380
+ const blocks = [{ type: "text", text }];
2381
+ await host.prompt(sessionId, blocks);
2382
+ out.write("\n");
2383
+ };
2384
+ if (typeof opts.message === "string" && opts.message.length > 0) {
2385
+ await send(opts.message);
2386
+ return;
2387
+ }
2388
+ const rl = createInterface({ input: opts.input ?? process.stdin });
2389
+ out.write(`Chatting with ${agent2.name} (${agent2.id}). Type .exit to quit.
2390
+ > `);
2391
+ try {
2392
+ for await (const line of rl) {
2393
+ const text = line.trim();
2394
+ if (text === ".exit" || text === ".quit") break;
2395
+ if (text.length > 0) await send(text);
2396
+ out.write("> ");
2397
+ }
2398
+ } finally {
2399
+ rl.close();
2400
+ }
2401
+ } finally {
2402
+ await host.stop();
2403
+ }
2404
+ }
2405
+
2406
+ // src/index.ts
2407
+ var version = true ? "0.0.0" : "0.0.0-dev";
2408
+ var program = new Command();
2409
+ program.name("agentconnect").description("AgentConnect daemon \u2014 edge message + agent execution unit").version(version);
2410
+ program.option("--config <path>", "path to config.json (default ~/.agentconnect/config.json)").option("--root <dir>", "override ~/.agentconnect root directory").option("--cp-url <url>", "override controlPlane.url").option("--cp-token <token>", "override controlPlane.token").option("--no-cp", "run fully local, do not connect to the Control Plane").option("--daemon-id <id>", "override daemon identity").option("--log-level <level>", "trace|debug|info|warn|error").option("--agents-dir <dir>", "override agents directory").option("--max-agents <n>", "max agents this daemon advertises / enforces").option("--dry-run", "load + validate config and print the reconcile plan, then exit").option("--agent <name>", "select a single agent by id (run/chat)");
2411
+ var todo = (name) => () => {
2412
+ console.log(`agentconnect: ${name} (not yet implemented)`);
2413
+ };
2414
+ program.command("run").description("Run the daemon in the foreground").action(async () => {
2415
+ const opts = program.opts();
2416
+ const daemon = new Daemon({
2417
+ root: opts.root,
2418
+ agentName: opts.agent,
2419
+ overrides: {
2420
+ cpUrl: opts.cpUrl,
2421
+ cpToken: opts.cpToken,
2422
+ noCp: opts.cp === false,
2423
+ daemonId: opts.daemonId,
2424
+ logLevel: opts.logLevel,
2425
+ agentsDir: opts.agentsDir,
2426
+ maxAgents: opts.maxAgents ? Number(opts.maxAgents) : void 0
2427
+ }
2428
+ });
2429
+ try {
2430
+ await daemon.start();
2431
+ } catch (err) {
2432
+ console.error(`agentconnect run: ${err.message}`);
2433
+ process.exit(1);
2434
+ }
2435
+ const shutdown = async () => {
2436
+ await daemon.stop();
2437
+ process.exit(0);
2438
+ };
2439
+ process.on("SIGINT", shutdown);
2440
+ process.on("SIGTERM", shutdown);
2441
+ console.log("agentconnect: daemon running (Ctrl-C to stop)");
2442
+ });
2443
+ program.command("start").description("Start the daemon in the background (detached)").action(todo("start"));
2444
+ program.command("stop").description("Gracefully stop the daemon").action(todo("stop"));
2445
+ program.command("restart").description("Restart the daemon").action(todo("restart"));
2446
+ program.command("status").description("Print runtime status").action(todo("status"));
2447
+ program.command("install-service").description("Install launchd / systemd service").action(todo("install-service"));
2448
+ program.command("uninstall-service").description("Uninstall the system service").action(todo("uninstall-service"));
2449
+ program.command("login").description("Write daemon-token / CP url into config").action(todo("login"));
2450
+ var config = program.command("config").description("Read / write / validate config.json");
2451
+ config.command("get").action(todo("config get"));
2452
+ config.command("set").action(todo("config set"));
2453
+ config.command("validate").action(todo("config validate"));
2454
+ config.command("path").action(todo("config path"));
2455
+ var agent = program.command("agent").description("Manage local agent directories");
2456
+ agent.command("list").action(todo("agent list"));
2457
+ agent.command("add [id]").action(todo("agent add"));
2458
+ agent.command("remove [id]").action(todo("agent remove"));
2459
+ agent.command("enable [id]").action(todo("agent enable"));
2460
+ agent.command("disable [id]").action(todo("agent disable"));
2461
+ program.command("chat [message]").description("Discover an agent under --agents-dir (or --agent <name>) and chat over ACP").action(async (message) => {
2462
+ const opts = program.opts();
2463
+ try {
2464
+ await runChat({
2465
+ agentsDir: opts.agentsDir,
2466
+ agentName: opts.agent,
2467
+ message,
2468
+ configPath: opts.config,
2469
+ root: opts.root
2470
+ });
2471
+ process.exit(0);
2472
+ } catch (err) {
2473
+ console.error(`agentconnect chat: ${err.message}`);
2474
+ process.exit(1);
2475
+ }
2476
+ });
2477
+ program.parse();
2478
+ //# sourceMappingURL=index.js.map