@agentconnect.md/daemon 1.0.0-rc.22 → 1.0.0-rc.24

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 CHANGED
@@ -12,7 +12,6 @@ import { createInterface } from "node:readline";
12
12
  import { freemem, homedir, hostname, loadavg, totalmem, type } from "node:os";
13
13
  import { spawn as spawn$1 } from "child_process";
14
14
  import { Readable, Writable } from "node:stream";
15
- import { MAX_FRAME_BYTES, buildEnvelope, decodeEnvelope, encode, isFrame } from "@agentconnect.md/protocol";
16
15
  import { randomUUID } from "node:crypto";
17
16
  import { lstat, open, readdir, realpath, stat as stat$1 } from "node:fs/promises";
18
17
  import { DatabaseSync } from "node:sqlite";
@@ -16742,6 +16741,9 @@ var AcpHost = class {
16742
16741
  opts;
16743
16742
  child;
16744
16743
  conn;
16744
+ live = /* @__PURE__ */ new Set();
16745
+ loadingSessions = /* @__PURE__ */ new Set();
16746
+ canLoad = false;
16745
16747
  constructor(runtime, opts) {
16746
16748
  this.runtime = runtime;
16747
16749
  this.opts = opts;
@@ -16765,6 +16767,7 @@ var AcpHost = class {
16765
16767
  const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
16766
16768
  const self = this;
16767
16769
  this.conn = client({ name: "agentconnect" }).onNotification(methods.client.session.update, (ctx) => {
16770
+ if (self.loadingSessions.has(ctx.params.sessionId)) return;
16768
16771
  self.opts.onUpdate(ctx.params.sessionId, ctx.params.update);
16769
16772
  }).onRequest(methods.client.session.requestPermission, (ctx) => {
16770
16773
  const optionId = (ctx.params.options.find((o) => o.kind === "allow_once" || o.kind === "allow_always") ?? ctx.params.options[0])?.optionId;
@@ -16773,19 +16776,52 @@ var AcpHost = class {
16773
16776
  optionId
16774
16777
  } : { outcome: "cancelled" } };
16775
16778
  }).connect(stream);
16776
- await this.conn.agent.request(methods.agent.initialize, {
16779
+ const init = await this.conn.agent.request(methods.agent.initialize, {
16777
16780
  protocolVersion: PROTOCOL_VERSION,
16778
16781
  clientCapabilities: { fs: {
16779
16782
  readTextFile: false,
16780
16783
  writeTextFile: false
16781
16784
  } }
16782
16785
  });
16786
+ this.canLoad = init.agentCapabilities?.loadSession ?? false;
16787
+ this.opts.log?.debug(`acp: agent initialized (loadSession capability=${this.canLoad})`);
16783
16788
  }
16784
16789
  async newSession(cwd) {
16785
- return (await this.conn.agent.request(methods.agent.session.new, {
16790
+ const res = await this.conn.agent.request(methods.agent.session.new, {
16786
16791
  cwd,
16787
16792
  mcpServers: []
16788
- })).sessionId;
16793
+ });
16794
+ this.live.add(res.sessionId);
16795
+ return res.sessionId;
16796
+ }
16797
+ /** True iff THIS agent process created or loaded `sessionId` in its current lifetime. */
16798
+ hasSession(sessionId) {
16799
+ return this.live.has(sessionId);
16800
+ }
16801
+ /** Whether the agent advertised the `loadSession` capability (session/load is usable). */
16802
+ loadSupported() {
16803
+ return this.canLoad;
16804
+ }
16805
+ /** Resume a previously-created session by id (ACP `session/load`). The agent
16806
+ * restores its own history server-side; its replayed session/update stream is
16807
+ * suppressed so it isn't re-posted. Throws if the agent can't load the id
16808
+ * (caller falls back to newSession). */
16809
+ async loadSession(sessionId, cwd) {
16810
+ this.loadingSessions.add(sessionId);
16811
+ try {
16812
+ await this.conn.agent.request(methods.agent.session.load, {
16813
+ sessionId,
16814
+ cwd,
16815
+ mcpServers: []
16816
+ });
16817
+ this.live.add(sessionId);
16818
+ this.opts.log?.info(`acp: resumed session ${sessionId} via session/load`);
16819
+ } catch (err) {
16820
+ this.opts.log?.debug(`acp: session/load failed for ${sessionId} (${err.message}) — will recreate`);
16821
+ throw err;
16822
+ } finally {
16823
+ this.loadingSessions.delete(sessionId);
16824
+ }
16789
16825
  }
16790
16826
  async prompt(sessionId, blocks) {
16791
16827
  return (await this.conn.agent.request(methods.agent.session.prompt, {
@@ -16872,6 +16908,553 @@ async function runChat(opts) {
16872
16908
  //#region src/version.ts
16873
16909
  const DAEMON_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
16874
16910
  //#endregion
16911
+ //#region ../protocol/dist/envelope.js
16912
+ /**
16913
+ * The protocol envelope — every frame, both directions, is wrapped in this.
16914
+ * Mirrors daemon-cp-ws-protocol.md §1.1.
16915
+ *
16916
+ * `payload` is left as `unknown` here and validated by the per-type schema in
16917
+ * `frames/*` via `FRAME_SCHEMAS[type]` (see `frame.ts`). This two-step parse is
16918
+ * what lets the codec answer an unknown `type` with `UNKNOWN_FRAME` (a REP)
16919
+ * instead of a hard close — forward-compat, protocol §1.
16920
+ */
16921
+ const Envelope = object({
16922
+ v: literal(1),
16923
+ id: string().uuid(),
16924
+ ts: string().datetime(),
16925
+ type: string(),
16926
+ corr: string().uuid().optional(),
16927
+ payload: unknown()
16928
+ });
16929
+ object({
16930
+ epoch: number().int(),
16931
+ seq: number().int().optional(),
16932
+ agentId: string().uuid().optional(),
16933
+ launchId: string().uuid().optional()
16934
+ });
16935
+ /** The all-zero UUID used when a frame is malformed past the point of reading `id`. */
16936
+ const NIL_UUID = "00000000-0000-0000-0000-000000000000";
16937
+ //#endregion
16938
+ //#region ../protocol/dist/frames/auth.js
16939
+ /**
16940
+ * Auth & identity — protocol §3.1 / §3.2.
16941
+ *
16942
+ * `auth` is the first frame after the socket opens. `auth/ok` carries the
16943
+ * minted `sessionEpoch` (the global fencing token), heartbeat cadence, and the
16944
+ * resume verdict.
16945
+ */
16946
+ const AuthReq = object({
16947
+ apiKey: string(),
16948
+ daemonId: string().uuid().optional(),
16949
+ machineId: string().uuid().optional(),
16950
+ attestation: string().optional(),
16951
+ agentVersion: string(),
16952
+ resume: object({
16953
+ lastEpoch: number().int(),
16954
+ lastRecvSeq: record(string(), number())
16955
+ }).optional()
16956
+ });
16957
+ const AuthOk = object({
16958
+ daemonId: string().uuid(),
16959
+ sessionEpoch: number().int(),
16960
+ heartbeatSec: number().int(),
16961
+ serverTime: string().datetime(),
16962
+ resume: object({
16963
+ accepted: boolean(),
16964
+ redeliverFromSeq: record(string(), number()).optional()
16965
+ }).optional()
16966
+ });
16967
+ //#endregion
16968
+ //#region ../protocol/dist/frames/route.js
16969
+ /**
16970
+ * Routing & orchestration (C→D control) — protocol §5.
16971
+ *
16972
+ * `SessionKey` is the canonical session primitive shared across route/*,
16973
+ * agent/*, and event/session. Its canonical string form is
16974
+ * `${platform}:${channel}:${thread ?? "-"}`.
16975
+ */
16976
+ const Platform = _enum(["slack", "telegram"]);
16977
+ const SessionKey = object({
16978
+ platform: Platform,
16979
+ channel: string(),
16980
+ thread: string().optional()
16981
+ });
16982
+ /** Trigger-matching rule for a binding (protocol §5.1). */
16983
+ const BindRule = object({ match: discriminatedUnion("kind", [
16984
+ object({ kind: literal("mention") }),
16985
+ object({ kind: literal("dm") }),
16986
+ object({
16987
+ kind: literal("keyword"),
16988
+ value: string()
16989
+ }),
16990
+ object({ kind: literal("auto") })
16991
+ ]) });
16992
+ const RouteAssign = object({
16993
+ sessionKey: SessionKey,
16994
+ agentId: string().uuid(),
16995
+ workspaceId: string().uuid(),
16996
+ bindRules: array(BindRule).default([])
16997
+ });
16998
+ const RouteAssignAck = object({
16999
+ ok: boolean(),
17000
+ sessionKey: SessionKey,
17001
+ reason: string().optional()
17002
+ });
17003
+ const RouteUpdate = object({
17004
+ routingEpoch: number().int(),
17005
+ rules: array(object({
17006
+ match: unknown(),
17007
+ agentId: string().uuid()
17008
+ }))
17009
+ });
17010
+ /** Graceful scale-down / rebalance — protocol §5.3. */
17011
+ const Drain = object({
17012
+ scope: union([
17013
+ object({
17014
+ kind: literal("agent"),
17015
+ agentId: string().uuid()
17016
+ }),
17017
+ object({ kind: literal("daemon") }),
17018
+ object({
17019
+ kind: literal("session"),
17020
+ sessionKey: SessionKey
17021
+ })
17022
+ ]),
17023
+ deadline: string().datetime()
17024
+ });
17025
+ const DrainProgress = object({
17026
+ remaining: number().int(),
17027
+ drained: array(SessionKey)
17028
+ });
17029
+ const DrainDone = object({ released: array(SessionKey) });
17030
+ //#endregion
17031
+ //#region ../protocol/dist/frames/cron.js
17032
+ /**
17033
+ * Cron sinks to the daemon (D5) — protocol §5.4.
17034
+ *
17035
+ * The CP owns the definition; the daemon owns firing + last-run persistence so
17036
+ * crons fire even when the CP is down.
17037
+ */
17038
+ const CronUpsert = object({
17039
+ cronId: string().uuid(),
17040
+ schedule: string(),
17041
+ target: object({ channel: string() }),
17042
+ trigger: string(),
17043
+ enabled: boolean().default(true)
17044
+ });
17045
+ const CronRemove = object({ cronId: string().uuid() });
17046
+ //#endregion
17047
+ //#region ../protocol/dist/frames/secrets.js
17048
+ /**
17049
+ * Secrets (C5 ↔ D10) — protocol §6.
17050
+ *
17051
+ * Lease-based, no plaintext on the wire or in PG. Every frame carries a
17052
+ * REFERENCE to a Vault/KMS path, never the secret material itself.
17053
+ */
17054
+ const SecretsRequest = object({ scope: object({
17055
+ platform: Platform,
17056
+ workspaceId: string().uuid()
17057
+ }) });
17058
+ const SecretsGrant = object({
17059
+ leaseId: string().uuid(),
17060
+ scope: object({
17061
+ platform: string(),
17062
+ workspaceId: string().uuid()
17063
+ }),
17064
+ ref: string(),
17065
+ ttl: number().int(),
17066
+ renewBeforeSec: number().int()
17067
+ });
17068
+ const SecretsRenew = object({ leaseId: string().uuid() });
17069
+ const SecretsRevoke = object({
17070
+ leaseId: string().uuid(),
17071
+ reason: string()
17072
+ });
17073
+ /** 🅼 Direct-to-store upload/download grant — protocol §3.2 / frame #25. */
17074
+ const ScopeAttestation = object({
17075
+ machineId: string().uuid(),
17076
+ scope: _enum([
17077
+ "attachment.put",
17078
+ "attachment.get",
17079
+ "facts.put"
17080
+ ]),
17081
+ resourceRef: string(),
17082
+ jws: string(),
17083
+ exp: string().datetime()
17084
+ });
17085
+ //#endregion
17086
+ //#region ../protocol/dist/frames/agent.js
17087
+ /**
17088
+ * Agent lifecycle + delivery (protocol §4.3, §4.4, §7.4, §8).
17089
+ *
17090
+ * Body-locality invariant (protocol §12): no frame here carries
17091
+ * `NormalizedMessage.text`. `agent/prompt` ships a `NormalizedMessageRef` — a
17092
+ * reference the daemon resolves against its local body store.
17093
+ */
17094
+ /**
17095
+ * A REFERENCE/digest of a normalized message, NOT the body (protocol §4.3).
17096
+ * Enough for D4/D6 to fetch the local body and prompt.
17097
+ */
17098
+ const NormalizedMessageRef = object({
17099
+ sessionKey: SessionKey,
17100
+ platformMsgId: string(),
17101
+ seenUpToSeq: number().int()
17102
+ });
17103
+ /**
17104
+ * The editable agent definition the CP owns and the daemon needs to run it:
17105
+ * prompt + runtime selection. Raft ships this in the launch config and the
17106
+ * daemon synthesizes the system prompt locally; `description` IS the prompt.
17107
+ */
17108
+ const AgentSpec = object({
17109
+ name: string(),
17110
+ description: string().optional(),
17111
+ model: string().optional(),
17112
+ reasoningEffort: string().optional(),
17113
+ executionMode: string().optional(),
17114
+ env: record(string(), string()).optional()
17115
+ });
17116
+ const AgentLaunch = object({
17117
+ agentId: string().uuid(),
17118
+ runtime: string(),
17119
+ workspaceId: string().uuid(),
17120
+ capabilities: array(string()),
17121
+ spec: AgentSpec,
17122
+ mode: _enum(["long_lived", "per_turn"]).default("long_lived")
17123
+ });
17124
+ /**
17125
+ * Live agent CRUD (C→D): the console edited an agent's spec; push it so a
17126
+ * running daemon reloads without waiting for the next launch. `agent/remove`
17127
+ * tears the agent down. Deleting an agent never relaunches it.
17128
+ */
17129
+ const AgentUpsert = object({
17130
+ agentId: string().uuid(),
17131
+ spec: AgentSpec
17132
+ });
17133
+ const AgentRemove = object({ agentId: string().uuid() });
17134
+ const AgentLaunched = object({
17135
+ agentId: string().uuid(),
17136
+ launchId: string().uuid(),
17137
+ acpSessionId: string().optional(),
17138
+ startedAt: string().datetime(),
17139
+ runtime: string()
17140
+ });
17141
+ const AgentStop = object({
17142
+ agentId: string().uuid(),
17143
+ launchId: string().uuid(),
17144
+ reason: string()
17145
+ });
17146
+ const AgentPrompt = object({
17147
+ sessionKey: SessionKey,
17148
+ agentId: string().uuid(),
17149
+ content: NormalizedMessageRef,
17150
+ seenUpToSeq: number().int()
17151
+ });
17152
+ const AgentPromptAck = object({
17153
+ accepted: boolean(),
17154
+ reason: _enum([
17155
+ "queued",
17156
+ "held",
17157
+ "scope_denied",
17158
+ "no_session",
17159
+ "stale"
17160
+ ]).optional(),
17161
+ seq: number().int()
17162
+ });
17163
+ const AgentActivity = object({
17164
+ agentId: string().uuid(),
17165
+ launchId: string().uuid(),
17166
+ state: _enum([
17167
+ "thinking",
17168
+ "tool_call",
17169
+ "awaiting_permission",
17170
+ "idle"
17171
+ ]),
17172
+ ts: string().datetime()
17173
+ });
17174
+ const AgentScopeDenied = object({
17175
+ agentId: string().uuid(),
17176
+ launchId: string().uuid(),
17177
+ capability: string()
17178
+ });
17179
+ //#endregion
17180
+ //#region ../protocol/dist/frame.js
17181
+ /**
17182
+ * The single source of truth for the wire: `type` string → payload zod schema.
17183
+ *
17184
+ * Mirrors the frame index in daemon-cp-ws-protocol.md §10 (the 29 numbered
17185
+ * frames) plus the correlated REP types named in the "Reply" column
17186
+ * (`route/assign/ack`, `drain/done`, and the generic `ack` replies) that also
17187
+ * travel on the wire and must be decodable.
17188
+ *
17189
+ * `ws/codec.ts` validates every inbound `payload` against `FRAME_SCHEMAS[type]`;
17190
+ * an unknown `type` → `ErrorFrame{code:"UNKNOWN_FRAME"}` (a REP, not a close).
17191
+ */
17192
+ const FRAME_SCHEMAS = {
17193
+ auth: AuthReq,
17194
+ "auth/ok": AuthOk,
17195
+ register: object({
17196
+ host: string(),
17197
+ capabilities: object({
17198
+ platforms: array(Platform),
17199
+ runtimes: array(string()),
17200
+ acp: boolean(),
17201
+ features: array(string()).default([])
17202
+ }),
17203
+ maxAgents: number().int(),
17204
+ localState: object({
17205
+ assignments: array(string()),
17206
+ crons: array(string()),
17207
+ leases: array(string())
17208
+ })
17209
+ }),
17210
+ "register/ok": object({
17211
+ routingEpoch: number().int(),
17212
+ assignments: array(RouteAssign),
17213
+ agents: array(AgentSpec.extend({ agentId: string().uuid() })).default([]),
17214
+ crons: array(CronUpsert),
17215
+ leases: array(SecretsGrant),
17216
+ drop: object({
17217
+ assignments: array(string()),
17218
+ crons: array(string())
17219
+ })
17220
+ }),
17221
+ heartbeat: object({
17222
+ load: object({
17223
+ cpu: number(),
17224
+ mem: number(),
17225
+ agents: number().int()
17226
+ }),
17227
+ health: _enum(["ok", "degraded"]),
17228
+ activeSessions: number().int(),
17229
+ degradedScopes: array(string()).default([])
17230
+ }),
17231
+ "agent/launch": AgentLaunch,
17232
+ "agent/launched": AgentLaunched,
17233
+ "agent/stop": AgentStop,
17234
+ "agent/upsert": AgentUpsert,
17235
+ "agent/remove": AgentRemove,
17236
+ "agent/prompt": AgentPrompt,
17237
+ "agent/prompt/ack": AgentPromptAck,
17238
+ "agent/activity": AgentActivity,
17239
+ "agent/scope-denied": AgentScopeDenied,
17240
+ "route/assign": RouteAssign,
17241
+ "route/assign/ack": RouteAssignAck,
17242
+ "route/update": RouteUpdate,
17243
+ "daemon/drain": Drain,
17244
+ "drain/progress": DrainProgress,
17245
+ "drain/done": DrainDone,
17246
+ "cron/upsert": CronUpsert,
17247
+ "cron/remove": CronRemove,
17248
+ "secrets/request": SecretsRequest,
17249
+ "secrets/grant": SecretsGrant,
17250
+ "secrets/renew": SecretsRenew,
17251
+ "secrets/revoke": SecretsRevoke,
17252
+ "scope-attestation": ScopeAttestation,
17253
+ "event/session": object({
17254
+ sessionId: string().uuid(),
17255
+ agentId: string().uuid(),
17256
+ launchId: string().uuid(),
17257
+ phase: _enum([
17258
+ "start",
17259
+ "plan",
17260
+ "problem",
17261
+ "end"
17262
+ ]),
17263
+ link: string().optional(),
17264
+ summary: string().optional(),
17265
+ ts: string().datetime()
17266
+ }),
17267
+ "facts/runtime-profile": object({
17268
+ runtime: string(),
17269
+ version: string(),
17270
+ models: array(string()),
17271
+ contextWindow: number().int().optional(),
17272
+ acpSupport: _enum([
17273
+ "full",
17274
+ "partial",
17275
+ "none"
17276
+ ]),
17277
+ toolCalling: boolean()
17278
+ }),
17279
+ "config/push": object({ keys: record(string(), unknown()) }),
17280
+ "daemon/restart": object({
17281
+ reason: string(),
17282
+ drainFirst: boolean().default(true)
17283
+ }),
17284
+ "daemon/upgrade": object({
17285
+ targetVersion: string(),
17286
+ drainFirst: boolean().default(true)
17287
+ }),
17288
+ "daemon/control/ack": object({
17289
+ accepted: boolean(),
17290
+ willDrainUntil: string().datetime().optional()
17291
+ }),
17292
+ ack: object({
17293
+ ok: boolean(),
17294
+ reason: string().optional()
17295
+ }),
17296
+ error: object({
17297
+ code: _enum([
17298
+ "UNKNOWN_FRAME",
17299
+ "FRAME_TOO_LARGE",
17300
+ "PROTOCOL_STATE",
17301
+ "BAD_PAYLOAD",
17302
+ "AUTH_FAILED",
17303
+ "ATTESTATION_INVALID",
17304
+ "STALE_EPOCH",
17305
+ "STALE_LAUNCH",
17306
+ "SEQ_GAP",
17307
+ "NO_SESSION",
17308
+ "HELD",
17309
+ "SCOPE_DENIED",
17310
+ "LEASE_EXPIRED",
17311
+ "LEASE_DENIED",
17312
+ "RATE_LIMITED",
17313
+ "INTERNAL"
17314
+ ]),
17315
+ message: string(),
17316
+ retryable: boolean(),
17317
+ details: record(string(), unknown()).optional()
17318
+ })
17319
+ };
17320
+ Object.keys(FRAME_SCHEMAS);
17321
+ /**
17322
+ * Builds the envelope schema for one frame `type` with a `type` literal and the
17323
+ * typed payload, so the discriminated union infers `payload` precisely.
17324
+ */
17325
+ function frame(type, payload) {
17326
+ return object({
17327
+ v: literal(1),
17328
+ id: string().uuid(),
17329
+ ts: string().datetime(),
17330
+ type: literal(type),
17331
+ corr: string().uuid().optional(),
17332
+ payload
17333
+ });
17334
+ }
17335
+ discriminatedUnion("type", [
17336
+ frame("auth", FRAME_SCHEMAS["auth"]),
17337
+ frame("auth/ok", FRAME_SCHEMAS["auth/ok"]),
17338
+ frame("register", FRAME_SCHEMAS["register"]),
17339
+ frame("register/ok", FRAME_SCHEMAS["register/ok"]),
17340
+ frame("heartbeat", FRAME_SCHEMAS["heartbeat"]),
17341
+ frame("agent/launch", FRAME_SCHEMAS["agent/launch"]),
17342
+ frame("agent/launched", FRAME_SCHEMAS["agent/launched"]),
17343
+ frame("agent/stop", FRAME_SCHEMAS["agent/stop"]),
17344
+ frame("agent/upsert", FRAME_SCHEMAS["agent/upsert"]),
17345
+ frame("agent/remove", FRAME_SCHEMAS["agent/remove"]),
17346
+ frame("agent/prompt", FRAME_SCHEMAS["agent/prompt"]),
17347
+ frame("agent/prompt/ack", FRAME_SCHEMAS["agent/prompt/ack"]),
17348
+ frame("agent/activity", FRAME_SCHEMAS["agent/activity"]),
17349
+ frame("agent/scope-denied", FRAME_SCHEMAS["agent/scope-denied"]),
17350
+ frame("route/assign", FRAME_SCHEMAS["route/assign"]),
17351
+ frame("route/assign/ack", FRAME_SCHEMAS["route/assign/ack"]),
17352
+ frame("route/update", FRAME_SCHEMAS["route/update"]),
17353
+ frame("daemon/drain", FRAME_SCHEMAS["daemon/drain"]),
17354
+ frame("drain/progress", FRAME_SCHEMAS["drain/progress"]),
17355
+ frame("drain/done", FRAME_SCHEMAS["drain/done"]),
17356
+ frame("cron/upsert", FRAME_SCHEMAS["cron/upsert"]),
17357
+ frame("cron/remove", FRAME_SCHEMAS["cron/remove"]),
17358
+ frame("secrets/request", FRAME_SCHEMAS["secrets/request"]),
17359
+ frame("secrets/grant", FRAME_SCHEMAS["secrets/grant"]),
17360
+ frame("secrets/renew", FRAME_SCHEMAS["secrets/renew"]),
17361
+ frame("secrets/revoke", FRAME_SCHEMAS["secrets/revoke"]),
17362
+ frame("scope-attestation", FRAME_SCHEMAS["scope-attestation"]),
17363
+ frame("event/session", FRAME_SCHEMAS["event/session"]),
17364
+ frame("facts/runtime-profile", FRAME_SCHEMAS["facts/runtime-profile"]),
17365
+ frame("config/push", FRAME_SCHEMAS["config/push"]),
17366
+ frame("daemon/restart", FRAME_SCHEMAS["daemon/restart"]),
17367
+ frame("daemon/upgrade", FRAME_SCHEMAS["daemon/upgrade"]),
17368
+ frame("daemon/control/ack", FRAME_SCHEMAS["daemon/control/ack"]),
17369
+ frame("ack", FRAME_SCHEMAS["ack"]),
17370
+ frame("error", FRAME_SCHEMAS["error"])
17371
+ ]);
17372
+ //#endregion
17373
+ //#region ../protocol/dist/codec.js
17374
+ /** Soft cap per frame — 256 KiB (protocol §1). Over this → FRAME_TOO_LARGE. */
17375
+ const MAX_FRAME_BYTES = 256 * 1024;
17376
+ const textEncoder = new TextEncoder();
17377
+ function byteLength(text) {
17378
+ return textEncoder.encode(text).length;
17379
+ }
17380
+ function extractControlExt(json) {
17381
+ if (typeof json !== "object" || json === null) return void 0;
17382
+ const o = json;
17383
+ const ext = {};
17384
+ if (typeof o.epoch === "number") ext.epoch = o.epoch;
17385
+ if (typeof o.seq === "number") ext.seq = o.seq;
17386
+ if (typeof o.agentId === "string") ext.agentId = o.agentId;
17387
+ if (typeof o.launchId === "string") ext.launchId = o.launchId;
17388
+ return Object.keys(ext).length > 0 ? ext : void 0;
17389
+ }
17390
+ function decodeEnvelope(text) {
17391
+ if (byteLength(text) > 262144) return {
17392
+ ok: false,
17393
+ id: NIL_UUID,
17394
+ msg: "FRAME_TOO_LARGE"
17395
+ };
17396
+ let json;
17397
+ try {
17398
+ json = JSON.parse(text);
17399
+ } catch {
17400
+ return {
17401
+ ok: false,
17402
+ id: NIL_UUID,
17403
+ msg: "invalid json"
17404
+ };
17405
+ }
17406
+ const env = Envelope.safeParse(json);
17407
+ if (!env.success) return {
17408
+ ok: false,
17409
+ id: typeof json === "object" && json !== null && typeof json.id === "string" ? json.id : NIL_UUID,
17410
+ msg: env.error.message
17411
+ };
17412
+ const schema = FRAME_SCHEMAS[env.data.type];
17413
+ if (!schema) return {
17414
+ ok: false,
17415
+ id: env.data.id,
17416
+ msg: "UNKNOWN_FRAME"
17417
+ };
17418
+ const payload = schema.safeParse(env.data.payload);
17419
+ if (!payload.success) return {
17420
+ ok: false,
17421
+ id: env.data.id,
17422
+ msg: payload.error.message
17423
+ };
17424
+ const ext = extractControlExt(json);
17425
+ return {
17426
+ ok: true,
17427
+ frame: {
17428
+ ...env.data,
17429
+ payload: payload.data
17430
+ },
17431
+ ...ext ? { ext } : {}
17432
+ };
17433
+ }
17434
+ function buildEnvelope(type, payload, opts = {}) {
17435
+ return {
17436
+ v: 1,
17437
+ id: opts.id ?? randomUUID(),
17438
+ ts: opts.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
17439
+ type,
17440
+ payload,
17441
+ ...opts.corr ? { corr: opts.corr } : {},
17442
+ ...opts.ext ?? {}
17443
+ };
17444
+ }
17445
+ function encode(frame) {
17446
+ return JSON.stringify(frame);
17447
+ }
17448
+ //#endregion
17449
+ //#region ../protocol/dist/index.js
17450
+ /**
17451
+ * Narrowing guard factory: `isFrame("auth")(frame)` narrows a decoded
17452
+ * `AnyFrame` to the member whose `type` matches.
17453
+ */
17454
+ function isFrame(type) {
17455
+ return (frame) => frame.type === type;
17456
+ }
17457
+ //#endregion
16875
17458
  //#region ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/constants.js
16876
17459
  var require_constants$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
16877
17460
  const BINARY_TYPES = [
@@ -20594,7 +21177,7 @@ const defaultExec = (cmd, args) => new Promise((resolve) => {
20594
21177
  /** macOS launchd controller. Writes a LaunchAgent plist that runs
20595
21178
  * `<node> <cli-entry> run`, and drives it with `launchctl bootstrap/bootout`
20596
21179
  * (falling back to legacy `load/unload` on older macOS). */
20597
- const LABEL = "co.sentio.agentconnect";
21180
+ const LABEL = "md.agentconnect.daemon";
20598
21181
  function buildPlist(a) {
20599
21182
  const env = a.includeRootEnv ? ` <key>EnvironmentVariables</key>\n <dict>\n <key>AGENTCONNECT_ROOT</key>\n <string>${a.root}</string>\n </dict>\n` : "";
20600
21183
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -22403,11 +22986,36 @@ function watch$1(paths, options = {}) {
22403
22986
  }
22404
22987
  //#endregion
22405
22988
  //#region src/reconciler/reconciler.ts
22406
- function diffAgents(desired, actualIds) {
22989
+ /** Stable identity-free signature of an agent's effective config. Excludes the
22990
+ * loader-only `dir`/`env` fields (present on `LoadedAgent`) so the comparison is
22991
+ * over the agent's behaviour, not where it was found. Both sides are the LOADED
22992
+ * form (interpolated, path-absolutized), so this is apples-to-apples and stable
22993
+ * across re-serialization (zod key order is fixed by the schema). */
22994
+ function signature(a) {
22995
+ const { dir, env, ...rest } = a;
22996
+ return JSON.stringify(rest);
22997
+ }
22998
+ /**
22999
+ * Diff desired (freshly loaded active agents) against the running set.
23000
+ * - `toStart` — desired ids not currently running.
23001
+ * - `toStop` — running ids no longer desired.
23002
+ * - `toRestart` — same id, changed config (runtime/workspace/integrations/…): the
23003
+ * host must be evicted so the next session picks up fresh config (design §5.2).
23004
+ * Without this, an in-place edit to an existing agent.json is silently a no-op.
23005
+ */
23006
+ function diffAgents(desired, actual) {
22407
23007
  const desiredIds = new Set(desired.map((a) => a.id));
23008
+ const toStart = [];
23009
+ const toRestart = [];
23010
+ for (const a of desired) {
23011
+ const cur = actual.get(a.id);
23012
+ if (!cur) toStart.push(a);
23013
+ else if (signature(cur) !== signature(a)) toRestart.push(a);
23014
+ }
22408
23015
  return {
22409
- toStart: desired.filter((a) => !actualIds.includes(a.id)),
22410
- toStop: actualIds.filter((id) => !desiredIds.has(id))
23016
+ toStart,
23017
+ toStop: [...actual.keys()].filter((id) => !desiredIds.has(id)),
23018
+ toRestart
22411
23019
  };
22412
23020
  }
22413
23021
  //#endregion
@@ -22511,6 +23119,24 @@ var SessionManager = class {
22511
23119
  updatedAt: Date.now()
22512
23120
  };
22513
23121
  this.deps.store.upsertSession(rec);
23122
+ } else if (host.hasSession?.(rec.acpSessionId) === false) {
23123
+ const cwd = await prepareWorkspace(agent);
23124
+ let resumed = false;
23125
+ if (host.loadSupported?.()) try {
23126
+ await host.loadSession(rec.acpSessionId, cwd);
23127
+ resumed = true;
23128
+ } catch {}
23129
+ if (!resumed) {
23130
+ const acpSessionId = await host.newSession(cwd);
23131
+ rec = {
23132
+ ...rec,
23133
+ acpSessionId,
23134
+ state: "idle",
23135
+ lastDeliveredTs: null,
23136
+ updatedAt: Date.now()
23137
+ };
23138
+ this.deps.store.upsertSession(rec);
23139
+ }
22514
23140
  }
22515
23141
  const gap = this.deps.store.transcriptSince(msg.channel, thread, rec.lastDeliveredTs);
22516
23142
  const blocks = [];
@@ -22582,11 +23208,8 @@ function routeRules(msg, rules, threadOwner) {
22582
23208
  if (msg.thread) {
22583
23209
  const owner = threadOwner(msg.channel, msg.thread);
22584
23210
  if (owner) {
22585
- const reachable = new Set(scopeCandidates.map((r) => r.agentId));
22586
- if (reachable.has(owner)) {
22587
- if (reachable.size === 1) return pickRule(scopeCandidates.find((x) => x.agentId === owner));
22588
- return null;
22589
- }
23211
+ const ownerRule = scopeCandidates.find((x) => x.agentId === owner);
23212
+ if (ownerRule) return pickRule(ownerRule);
22590
23213
  }
22591
23214
  }
22592
23215
  const layer = kindCandidates.some((r) => r.source === "cp" && r.scope.channel === msg.channel && (r.scope.thread === void 0 || r.scope.thread === msg.thread)) ? kindCandidates.filter((r) => r.source === "cp") : kindCandidates;
@@ -80031,11 +80654,17 @@ var SystemClock = class {
80031
80654
  const systemClock = new SystemClock();
80032
80655
  //#endregion
80033
80656
  //#region src/daemon.ts
80034
- const LOADING_MSGS = [
80035
- "Working on it…",
80036
- "Crunching through it…",
80037
- "Hang tight…"
80038
- ];
80657
+ /** Format an error for logs, surfacing a JSON-RPC/ACP RequestError's `code` and
80658
+ * `data` for an agent-side `Internal error` the actionable detail (the adapter's
80659
+ * underlying exception) lives in `data`, which a bare `.stack` discards. */
80660
+ function formatErr(err) {
80661
+ const e = err;
80662
+ if (e && typeof e.code === "number") {
80663
+ const data = e.data === void 0 ? "" : ` data=${typeof e.data === "string" ? e.data : JSON.stringify(e.data)}`;
80664
+ return `${e.name ?? "Error"}: ${e.message ?? ""} (code=${e.code})${data}`;
80665
+ }
80666
+ return e?.stack ?? String(err);
80667
+ }
80039
80668
  const MAX_QUEUED_PER_SESSION = 10;
80040
80669
  var Daemon = class {
80041
80670
  opts;
@@ -80111,7 +80740,7 @@ var Daemon = class {
80111
80740
  agentById: (id) => this.agents.get(id)
80112
80741
  });
80113
80742
  this.scheduler = new Scheduler({
80114
- onFire: (agentId, msg) => void this.dispatch(agentId, msg).catch((err) => this.log.error(`cron dispatch failed for agent "${agentId}": ${err.stack ?? err}`)),
80743
+ onFire: (agentId, msg) => void this.dispatch(agentId, msg).catch((err) => this.log.error(`cron dispatch failed for agent "${agentId}": ${formatErr(err)}`)),
80115
80744
  newTraceId: () => randomUUID()
80116
80745
  });
80117
80746
  const groups = consolidate(agents);
@@ -80161,7 +80790,10 @@ var Daemon = class {
80161
80790
  return this.opts.agentName ? [selectAgent(this.agentsDir, this.opts.agentName)] : loadAgents(this.agentsDir);
80162
80791
  }
80163
80792
  async reconcile() {
80164
- const { toStart, toStop } = diffAgents(this.loadAgentList(), [...this.agents.keys()]);
80793
+ const desired = this.loadAgentList();
80794
+ const { toStart, toStop, toRestart } = diffAgents(desired, this.agents);
80795
+ if (toStart.length || toStop.length || toRestart.length) this.log.info(`reconcile: ${desired.length} desired agent(s) from ${this.agentsDir}; start=[${toStart.map((a) => a.id).join(", ")}] stop=[${toStop.join(", ")}] restart=[${toRestart.map((a) => a.id).join(", ")}]`);
80796
+ else this.log.debug(`reconcile: no changes (${desired.length} desired agent(s))`);
80165
80797
  for (const id of toStop) {
80166
80798
  const host = this.hosts.get(id);
80167
80799
  if (host) {
@@ -80171,6 +80803,15 @@ var Daemon = class {
80171
80803
  this.hostStarts.delete(id);
80172
80804
  this.agents.delete(id);
80173
80805
  }
80806
+ for (const a of toRestart) {
80807
+ const host = this.hosts.get(a.id);
80808
+ if (host) {
80809
+ await host.stop();
80810
+ this.hosts.delete(a.id);
80811
+ }
80812
+ this.hostStarts.delete(a.id);
80813
+ this.agents.set(a.id, a);
80814
+ }
80174
80815
  for (const a of toStart) this.agents.set(a.id, a);
80175
80816
  }
80176
80817
  ensureHost(agentId, cfg) {
@@ -80184,7 +80825,8 @@ var Daemon = class {
80184
80825
  if (!runtime) throw new Error(`runtime "${agent.runtime}" not available: not installed on this host, or absent from config.runtimes / the ACP registry`);
80185
80826
  host = new AcpHost(runtime, {
80186
80827
  onUpdate,
80187
- env: agentChildEnv(agent)
80828
+ env: agentChildEnv(agent),
80829
+ log: this.log
80188
80830
  });
80189
80831
  }
80190
80832
  this.hosts.set(agentId, host);
@@ -80209,7 +80851,7 @@ var Daemon = class {
80209
80851
  return;
80210
80852
  }
80211
80853
  this.log.info(`routing: ch=${msg.channel} → agent "${result.agentId}" (integration ${result.integrationId})`);
80212
- this.dispatch(result.agentId, msg, result.integrationId).catch((err) => this.log.error(`dispatch failed for agent "${result.agentId}": ${err.stack ?? err}`));
80854
+ this.dispatch(result.agentId, msg, result.integrationId).catch((err) => this.log.error(`dispatch failed for agent "${result.agentId}": ${formatErr(err)}`));
80213
80855
  }
80214
80856
  queued = /* @__PURE__ */ new Map();
80215
80857
  /**
@@ -80298,7 +80940,7 @@ var Daemon = class {
80298
80940
  const replyConn = this.replyConnFor(agentId, integrationId);
80299
80941
  const wasRunning = this.hostStarts.has(agentId);
80300
80942
  const statusThread = msg.thread ?? msg.msgId;
80301
- replyConn?.setStatus(msg.channel, statusThread, wasRunning ? "is thinking…" : "is starting up…", LOADING_MSGS);
80943
+ replyConn?.setStatus(msg.channel, statusThread, wasRunning ? "is thinking…" : "is starting up…");
80302
80944
  const { sessionId, blocks } = await this.sessions.handle(agentId, msg);
80303
80945
  this.pending.set(sessionId, {
80304
80946
  conv,
@@ -80308,7 +80950,7 @@ var Daemon = class {
80308
80950
  });
80309
80951
  try {
80310
80952
  const host = await this.ensureHostAsync(agentId);
80311
- if (!wasRunning) replyConn?.setStatus(msg.channel, statusThread, "is thinking…", LOADING_MSGS);
80953
+ if (!wasRunning) replyConn?.setStatus(msg.channel, statusThread, "is thinking…");
80312
80954
  await host.prompt(sessionId, blocks);
80313
80955
  for (const action of conv.onFinal(`local://session/${sessionId}`)) await this.applyAction(action, replyConn, msg.channel, statusThread);
80314
80956
  } finally {
@@ -80316,10 +80958,10 @@ var Daemon = class {
80316
80958
  }
80317
80959
  this.flushQueued(agentId, sessionId, integrationId);
80318
80960
  }
80319
- /** Route a converger action: set-status → setStatus (loading_messages only when not clearing); else postMessage. */
80961
+ /** Route a converger action: set-status → setStatus (status text only; '' clears); else postMessage. */
80320
80962
  async applyAction(action, conn, channel, thread) {
80321
80963
  if (action.kind === "set-status") {
80322
- if (conn && thread) await conn.setStatus(channel, thread, action.text, action.text ? LOADING_MSGS : void 0);
80964
+ if (conn && thread) await conn.setStatus(channel, thread, action.text);
80323
80965
  return;
80324
80966
  }
80325
80967
  await conn?.postMessage(channel, action.text, thread);