@agentconnect.md/daemon 1.0.0-rc.23 → 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
@@ -16741,6 +16741,9 @@ var AcpHost = class {
16741
16741
  opts;
16742
16742
  child;
16743
16743
  conn;
16744
+ live = /* @__PURE__ */ new Set();
16745
+ loadingSessions = /* @__PURE__ */ new Set();
16746
+ canLoad = false;
16744
16747
  constructor(runtime, opts) {
16745
16748
  this.runtime = runtime;
16746
16749
  this.opts = opts;
@@ -16764,6 +16767,7 @@ var AcpHost = class {
16764
16767
  const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
16765
16768
  const self = this;
16766
16769
  this.conn = client({ name: "agentconnect" }).onNotification(methods.client.session.update, (ctx) => {
16770
+ if (self.loadingSessions.has(ctx.params.sessionId)) return;
16767
16771
  self.opts.onUpdate(ctx.params.sessionId, ctx.params.update);
16768
16772
  }).onRequest(methods.client.session.requestPermission, (ctx) => {
16769
16773
  const optionId = (ctx.params.options.find((o) => o.kind === "allow_once" || o.kind === "allow_always") ?? ctx.params.options[0])?.optionId;
@@ -16772,19 +16776,52 @@ var AcpHost = class {
16772
16776
  optionId
16773
16777
  } : { outcome: "cancelled" } };
16774
16778
  }).connect(stream);
16775
- await this.conn.agent.request(methods.agent.initialize, {
16779
+ const init = await this.conn.agent.request(methods.agent.initialize, {
16776
16780
  protocolVersion: PROTOCOL_VERSION,
16777
16781
  clientCapabilities: { fs: {
16778
16782
  readTextFile: false,
16779
16783
  writeTextFile: false
16780
16784
  } }
16781
16785
  });
16786
+ this.canLoad = init.agentCapabilities?.loadSession ?? false;
16787
+ this.opts.log?.debug(`acp: agent initialized (loadSession capability=${this.canLoad})`);
16782
16788
  }
16783
16789
  async newSession(cwd) {
16784
- return (await this.conn.agent.request(methods.agent.session.new, {
16790
+ const res = await this.conn.agent.request(methods.agent.session.new, {
16785
16791
  cwd,
16786
16792
  mcpServers: []
16787
- })).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
+ }
16788
16825
  }
16789
16826
  async prompt(sessionId, blocks) {
16790
16827
  return (await this.conn.agent.request(methods.agent.session.prompt, {
@@ -21140,7 +21177,7 @@ const defaultExec = (cmd, args) => new Promise((resolve) => {
21140
21177
  /** macOS launchd controller. Writes a LaunchAgent plist that runs
21141
21178
  * `<node> <cli-entry> run`, and drives it with `launchctl bootstrap/bootout`
21142
21179
  * (falling back to legacy `load/unload` on older macOS). */
21143
- const LABEL = "co.sentio.agentconnect";
21180
+ const LABEL = "md.agentconnect.daemon";
21144
21181
  function buildPlist(a) {
21145
21182
  const env = a.includeRootEnv ? ` <key>EnvironmentVariables</key>\n <dict>\n <key>AGENTCONNECT_ROOT</key>\n <string>${a.root}</string>\n </dict>\n` : "";
21146
21183
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -22949,11 +22986,36 @@ function watch$1(paths, options = {}) {
22949
22986
  }
22950
22987
  //#endregion
22951
22988
  //#region src/reconciler/reconciler.ts
22952
- 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) {
22953
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
+ }
22954
23015
  return {
22955
- toStart: desired.filter((a) => !actualIds.includes(a.id)),
22956
- toStop: actualIds.filter((id) => !desiredIds.has(id))
23016
+ toStart,
23017
+ toStop: [...actual.keys()].filter((id) => !desiredIds.has(id)),
23018
+ toRestart
22957
23019
  };
22958
23020
  }
22959
23021
  //#endregion
@@ -23057,6 +23119,24 @@ var SessionManager = class {
23057
23119
  updatedAt: Date.now()
23058
23120
  };
23059
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
+ }
23060
23140
  }
23061
23141
  const gap = this.deps.store.transcriptSince(msg.channel, thread, rec.lastDeliveredTs);
23062
23142
  const blocks = [];
@@ -23128,11 +23208,8 @@ function routeRules(msg, rules, threadOwner) {
23128
23208
  if (msg.thread) {
23129
23209
  const owner = threadOwner(msg.channel, msg.thread);
23130
23210
  if (owner) {
23131
- const reachable = new Set(scopeCandidates.map((r) => r.agentId));
23132
- if (reachable.has(owner)) {
23133
- if (reachable.size === 1) return pickRule(scopeCandidates.find((x) => x.agentId === owner));
23134
- return null;
23135
- }
23211
+ const ownerRule = scopeCandidates.find((x) => x.agentId === owner);
23212
+ if (ownerRule) return pickRule(ownerRule);
23136
23213
  }
23137
23214
  }
23138
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;
@@ -80577,11 +80654,17 @@ var SystemClock = class {
80577
80654
  const systemClock = new SystemClock();
80578
80655
  //#endregion
80579
80656
  //#region src/daemon.ts
80580
- const LOADING_MSGS = [
80581
- "Working on it…",
80582
- "Crunching through it…",
80583
- "Hang tight…"
80584
- ];
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
+ }
80585
80668
  const MAX_QUEUED_PER_SESSION = 10;
80586
80669
  var Daemon = class {
80587
80670
  opts;
@@ -80657,7 +80740,7 @@ var Daemon = class {
80657
80740
  agentById: (id) => this.agents.get(id)
80658
80741
  });
80659
80742
  this.scheduler = new Scheduler({
80660
- 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)}`)),
80661
80744
  newTraceId: () => randomUUID()
80662
80745
  });
80663
80746
  const groups = consolidate(agents);
@@ -80707,7 +80790,10 @@ var Daemon = class {
80707
80790
  return this.opts.agentName ? [selectAgent(this.agentsDir, this.opts.agentName)] : loadAgents(this.agentsDir);
80708
80791
  }
80709
80792
  async reconcile() {
80710
- 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))`);
80711
80797
  for (const id of toStop) {
80712
80798
  const host = this.hosts.get(id);
80713
80799
  if (host) {
@@ -80717,6 +80803,15 @@ var Daemon = class {
80717
80803
  this.hostStarts.delete(id);
80718
80804
  this.agents.delete(id);
80719
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
+ }
80720
80815
  for (const a of toStart) this.agents.set(a.id, a);
80721
80816
  }
80722
80817
  ensureHost(agentId, cfg) {
@@ -80730,7 +80825,8 @@ var Daemon = class {
80730
80825
  if (!runtime) throw new Error(`runtime "${agent.runtime}" not available: not installed on this host, or absent from config.runtimes / the ACP registry`);
80731
80826
  host = new AcpHost(runtime, {
80732
80827
  onUpdate,
80733
- env: agentChildEnv(agent)
80828
+ env: agentChildEnv(agent),
80829
+ log: this.log
80734
80830
  });
80735
80831
  }
80736
80832
  this.hosts.set(agentId, host);
@@ -80755,7 +80851,7 @@ var Daemon = class {
80755
80851
  return;
80756
80852
  }
80757
80853
  this.log.info(`routing: ch=${msg.channel} → agent "${result.agentId}" (integration ${result.integrationId})`);
80758
- 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)}`));
80759
80855
  }
80760
80856
  queued = /* @__PURE__ */ new Map();
80761
80857
  /**
@@ -80844,7 +80940,7 @@ var Daemon = class {
80844
80940
  const replyConn = this.replyConnFor(agentId, integrationId);
80845
80941
  const wasRunning = this.hostStarts.has(agentId);
80846
80942
  const statusThread = msg.thread ?? msg.msgId;
80847
- 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…");
80848
80944
  const { sessionId, blocks } = await this.sessions.handle(agentId, msg);
80849
80945
  this.pending.set(sessionId, {
80850
80946
  conv,
@@ -80854,7 +80950,7 @@ var Daemon = class {
80854
80950
  });
80855
80951
  try {
80856
80952
  const host = await this.ensureHostAsync(agentId);
80857
- if (!wasRunning) replyConn?.setStatus(msg.channel, statusThread, "is thinking…", LOADING_MSGS);
80953
+ if (!wasRunning) replyConn?.setStatus(msg.channel, statusThread, "is thinking…");
80858
80954
  await host.prompt(sessionId, blocks);
80859
80955
  for (const action of conv.onFinal(`local://session/${sessionId}`)) await this.applyAction(action, replyConn, msg.channel, statusThread);
80860
80956
  } finally {
@@ -80862,10 +80958,10 @@ var Daemon = class {
80862
80958
  }
80863
80959
  this.flushQueued(agentId, sessionId, integrationId);
80864
80960
  }
80865
- /** 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. */
80866
80962
  async applyAction(action, conn, channel, thread) {
80867
80963
  if (action.kind === "set-status") {
80868
- 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);
80869
80965
  return;
80870
80966
  }
80871
80967
  await conn?.postMessage(channel, action.text, thread);