@agentconnect.md/daemon 1.0.0-rc.29 → 1.0.0-rc.30

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
@@ -12768,6 +12768,7 @@ var simpleGit = gitInstanceFactory;
12768
12768
  //#endregion
12769
12769
  //#region src/workspace/workspace-manager.ts
12770
12770
  const PULL_TIMEOUT_MS = 4500;
12771
+ const cloneInFlight = /* @__PURE__ */ new Map();
12771
12772
  async function prepareWorkspace(agent) {
12772
12773
  const cwd = agent.workspace.path;
12773
12774
  mkdirSync(cwd, { recursive: true });
@@ -12776,11 +12777,52 @@ async function prepareWorkspace(agent) {
12776
12777
  if (!existsSync(mem)) writeFileSync(mem, `# ${agent.name} memory\n`);
12777
12778
  return cwd;
12778
12779
  }
12779
- if (agent.workspace.pullOnNewSession && existsSync(join(cwd, ".git"))) try {
12780
+ if (!existsSync(join(cwd, ".git"))) {
12781
+ await cloneRepo(agent);
12782
+ return cwd;
12783
+ }
12784
+ if (agent.workspace.pullOnNewSession) try {
12780
12785
  await Promise.race([simpleGit(cwd).pull(["--ff-only"]), new Promise((_, rej) => setTimeout(() => rej(/* @__PURE__ */ new Error("pull timeout")), PULL_TIMEOUT_MS))]);
12781
12786
  } catch {}
12782
12787
  return cwd;
12783
12788
  }
12789
+ /**
12790
+ * Eagerly clone a git-repo workspace that has no checkout yet — for reconcile-time
12791
+ * prefetch so the repo is warm before the first message arrives. No-op for
12792
+ * from-scratch or an already-cloned checkout (the session-time `prepareWorkspace`
12793
+ * stays authoritative and still owns pull + hard-fail). Reuses the single-flight
12794
+ * lock so a prefetch and a concurrent session-start clone never race into the same
12795
+ * dir. The caller invokes this fire-and-forget and logs any rejection — clone
12796
+ * failure here is non-fatal because `prepareWorkspace` re-clones (and hard-fails)
12797
+ * on the first session as the backstop.
12798
+ */
12799
+ async function prefetchWorkspace(agent) {
12800
+ if (agent.workspace.mode !== "git-repo") return;
12801
+ const cwd = agent.workspace.path;
12802
+ if (existsSync(join(cwd, ".git"))) return;
12803
+ mkdirSync(cwd, { recursive: true });
12804
+ await cloneRepo(agent);
12805
+ }
12806
+ /** Clone agent.workspace.gitRepo @ gitBranch into cwd, single-flight per cwd. */
12807
+ async function cloneRepo(agent) {
12808
+ const cwd = agent.workspace.path;
12809
+ const inflight = cloneInFlight.get(cwd);
12810
+ if (inflight) return inflight;
12811
+ const gitRepo = agent.workspace.gitRepo;
12812
+ if (!gitRepo) throw new Error(`workspace clone: agent "${agent.id}" has git-repo mode but no gitRepo configured`);
12813
+ const branch = agent.workspace.gitBranch;
12814
+ const p = (async () => {
12815
+ await simpleGit().clone(gitRepo, cwd, [
12816
+ "--branch",
12817
+ branch,
12818
+ "--single-branch"
12819
+ ]);
12820
+ })().finally(() => {
12821
+ cloneInFlight.delete(cwd);
12822
+ });
12823
+ cloneInFlight.set(cwd, p);
12824
+ return p;
12825
+ }
12784
12826
  //#endregion
12785
12827
  //#region ../../node_modules/.pnpm/@agentclientprotocol+sdk@1.0.0_zod@4.4.3/node_modules/@agentclientprotocol/sdk/dist/schema/index.js
12786
12828
  const AGENT_METHODS = {
@@ -23121,27 +23163,60 @@ function signature(a) {
23121
23163
  const { dir, env, ...rest } = a;
23122
23164
  return JSON.stringify(rest);
23123
23165
  }
23166
+ /** Sub-signature over the dimensions that determine the ACP host subprocess —
23167
+ * the spawn binary (`runtime`) and the child env / system-prompt seed knobs that
23168
+ * are baked into the host at spawn (agentChildEnv + cpRuntimeEnv). A change here
23169
+ * means the cached host must be evicted so the next session respawns it fresh. */
23170
+ function hostSpawnSig(a) {
23171
+ return JSON.stringify({
23172
+ runtime: a.runtime,
23173
+ model: a.runtimeOverrides?.model,
23174
+ description: a.description,
23175
+ reasoningEffort: a.reasoningEffort,
23176
+ executionMode: a.executionMode,
23177
+ env: a.runtimeOverrides?.env
23178
+ });
23179
+ }
23180
+ /** Sub-signature over the workspace dimension — cwd is materialized per session by
23181
+ * prepareWorkspace(agent), so a workspace change must also evict the host so the
23182
+ * next session re-materializes the (possibly re-pointed/re-cloned) checkout. */
23183
+ function workspaceSig(a) {
23184
+ return JSON.stringify(a.workspace);
23185
+ }
23186
+ /** Sub-signature over the integration dimension — Slack app/bot tokens + bindRules.
23187
+ * A change here rebuilds routing (and, where safe, re-opens Slack connections) but
23188
+ * does NOT by itself touch the host. */
23189
+ function integrationsSig(a) {
23190
+ return JSON.stringify(a.integrations);
23191
+ }
23124
23192
  /**
23125
23193
  * Diff desired (freshly loaded active agents) against the running set.
23126
- * - `toStart` — desired ids not currently running.
23127
- * - `toStop` — running ids no longer desired.
23128
- * - `toRestart` — same id, changed config (runtime/workspace/integrations/…): the
23129
- * host must be evicted so the next session picks up fresh config (design §5.2).
23130
- * Without this, an in-place edit to an existing agent.json is silently a no-op.
23194
+ * - `toStart` — desired ids not currently running.
23195
+ * - `toStop` — running ids no longer desired.
23196
+ * - `toChange` — same id, changed effective config. Emitted whenever the overall
23197
+ * signature differs; the three booleans classify which dimensions moved so the
23198
+ * caller can react minimally (evict host vs rebuild routing vs nothing). All
23199
+ * three false ⇒ a soft-only change. Without `toChange` an in-place agent.json
23200
+ * edit would silently be a no-op.
23131
23201
  */
23132
23202
  function diffAgents(desired, actual) {
23133
23203
  const desiredIds = new Set(desired.map((a) => a.id));
23134
23204
  const toStart = [];
23135
- const toRestart = [];
23205
+ const toChange = [];
23136
23206
  for (const a of desired) {
23137
23207
  const cur = actual.get(a.id);
23138
23208
  if (!cur) toStart.push(a);
23139
- else if (signature(cur) !== signature(a)) toRestart.push(a);
23209
+ else if (signature(cur) !== signature(a)) toChange.push({
23210
+ agent: a,
23211
+ hostRespawn: hostSpawnSig(cur) !== hostSpawnSig(a),
23212
+ workspace: workspaceSig(cur) !== workspaceSig(a),
23213
+ integrations: integrationsSig(cur) !== integrationsSig(a)
23214
+ });
23140
23215
  }
23141
23216
  return {
23142
23217
  toStart,
23143
23218
  toStop: [...actual.keys()].filter((id) => !desiredIds.has(id)),
23144
- toRestart
23219
+ toChange
23145
23220
  };
23146
23221
  }
23147
23222
  //#endregion
@@ -79663,6 +79738,10 @@ var SlackConnection = class {
79663
79738
  deps;
79664
79739
  app;
79665
79740
  botUserId = "";
79741
+ /** The appToken this socket is keyed by (one socket per unique appToken). */
79742
+ appToken;
79743
+ /** The botToken this socket authenticated with (used to detect a same-appToken swap). */
79744
+ botToken;
79666
79745
  constructor(deps, factory = (o) => new App({
79667
79746
  token: o.token,
79668
79747
  appToken: o.appToken,
@@ -79670,6 +79749,8 @@ var SlackConnection = class {
79670
79749
  ...deps.boltDebug ? { logLevel: LogLevel.DEBUG } : {}
79671
79750
  })) {
79672
79751
  this.deps = deps;
79752
+ this.appToken = deps.group.appToken;
79753
+ this.botToken = deps.group.botToken;
79673
79754
  this.app = factory({
79674
79755
  token: deps.group.botToken,
79675
79756
  appToken: deps.group.appToken
@@ -81564,8 +81645,15 @@ var Daemon = class {
81564
81645
  const files = this.loadAgentList();
81565
81646
  this.fileAgents = new Map(files.map((a) => [a.id, a]));
81566
81647
  const desired = this.effectiveAgents();
81567
- const { toStart, toStop, toRestart } = diffAgents(desired, this.agents);
81568
- 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(", ")}]`);
81648
+ const { toStart, toStop, toChange } = diffAgents(desired, this.agents);
81649
+ if (toStart.length || toStop.length || toChange.length) this.log.info(`reconcile: ${desired.length} desired agent(s) from ${this.agentsDir}; start=[${toStart.map((a) => a.id).join(", ")}] stop=[${toStop.join(", ")}] change=[${toChange.map((c) => {
81650
+ const dims = [
81651
+ c.hostRespawn && "host",
81652
+ c.workspace && "workspace",
81653
+ c.integrations && "integrations"
81654
+ ].filter(Boolean).join("+");
81655
+ return `${c.agent.id}(${dims || "soft"})`;
81656
+ }).join(", ")}]`);
81569
81657
  else this.log.debug(`reconcile: no changes (${desired.length} desired agent(s))`);
81570
81658
  for (const id of toStop) {
81571
81659
  const host = this.hosts.get(id);
@@ -81576,16 +81664,88 @@ var Daemon = class {
81576
81664
  this.hostStarts.delete(id);
81577
81665
  this.agents.delete(id);
81578
81666
  }
81579
- for (const a of toRestart) {
81580
- const host = this.hosts.get(a.id);
81581
- if (host) {
81582
- await host.stop();
81583
- this.hosts.delete(a.id);
81667
+ for (const change of toChange) {
81668
+ const a = change.agent;
81669
+ this.agents.set(a.id, a);
81670
+ if (change.hostRespawn || change.workspace) {
81671
+ const host = this.hosts.get(a.id);
81672
+ if (host) {
81673
+ await host.stop();
81674
+ this.hosts.delete(a.id);
81675
+ }
81676
+ this.hostStarts.delete(a.id);
81584
81677
  }
81585
- this.hostStarts.delete(a.id);
81678
+ if (change.workspace) this.prefetchClone(a);
81679
+ if (change.integrations) await this.reconcileSlackConnections();
81680
+ }
81681
+ for (const a of toStart) {
81586
81682
  this.agents.set(a.id, a);
81683
+ this.prefetchClone(a);
81684
+ }
81685
+ if (toStart.some((a) => a.integrations.some((i) => i.platform === "slack")) || toStop.length) await this.reconcileSlackConnections();
81686
+ }
81687
+ /**
81688
+ * Fire-and-forget eager clone of a git-repo workspace so the checkout is warm
81689
+ * before the first message. Deliberately NOT awaited: reconcile must not block on
81690
+ * the network (design §4.3), and `prepareWorkspace` is the authoritative clone
81691
+ * (awaited + hard-fail) at session start — so a prefetch failure here is only
81692
+ * logged and harmlessly retried then. No-op for from-scratch / already-cloned.
81693
+ */
81694
+ prefetchClone(agent) {
81695
+ prefetchWorkspace(agent).catch((err) => this.log.warn(`workspace: prefetch clone for "${agent.id}" failed (will retry at first session): ${formatErr(err)}`));
81696
+ }
81697
+ /**
81698
+ * Reconcile the connection-derived Slack state (`botUserIds`, `connByIntegration`,
81699
+ * open sockets) against the live `this.agents` set. Routing itself is rebuilt
81700
+ * implicitly each message (mergedRules reads this.agents) — this only maintains the
81701
+ * socket layer that is otherwise written only at startup.
81702
+ *
81703
+ * Safe-by-construction (per the recon report):
81704
+ * - NEW appToken → construct + start an isolated socket, then fan botUserId/conn
81705
+ * out to every integrationId on that appToken. A failed start() is logged and
81706
+ * leaves all existing sockets untouched (never throws out of reconcile).
81707
+ * - NEW integration reusing an ALREADY-OPEN appToken → no socket churn; just
81708
+ * backfill botUserIds/connByIntegration from the existing conn (mention routing
81709
+ * for the new bot would otherwise silently never match).
81710
+ * - REMOVED appToken / close-or-replace of a still-shared socket → NOT performed
81711
+ * here. Tearing down a socket that other agents or in-flight turns depend on is
81712
+ * the one genuinely dangerous op; it is left as a logged deferred-close TODO so
81713
+ * a stale socket simply lingers (harmless) rather than risking the daemon.
81714
+ */
81715
+ async reconcileSlackConnections() {
81716
+ const groups = consolidate([...this.agents.values()]);
81717
+ new Set([...this.connByIntegration.values()].map((c) => c.appToken));
81718
+ for (const group of groups.values()) {
81719
+ const existing = [...this.connByIntegration.values()].find((c) => c.appToken === group.appToken);
81720
+ if (existing) {
81721
+ if (existing.botToken !== group.botToken) this.log.warn(`slack: botToken changed for appToken (socket bot user ${existing.botUserId}) — deferred: the live socket keeps the old botToken until it is closed+reopened`);
81722
+ for (const { integrationId } of group.integrations) if (this.connByIntegration.get(integrationId) !== existing) {
81723
+ this.botUserIds[integrationId] = existing.botUserId;
81724
+ this.connByIntegration.set(integrationId, existing);
81725
+ this.log.info(`slack: bound integration ${integrationId} onto existing socket (appToken reuse)`);
81726
+ }
81727
+ continue;
81728
+ }
81729
+ try {
81730
+ const conn = new SlackConnection({
81731
+ group,
81732
+ newTraceId: () => randomUUID(),
81733
+ onMessage: (msg) => this.onInbound(msg),
81734
+ log: this.log,
81735
+ boltDebug: this.cfg.logging.level === "debug" || this.cfg.logging.level === "trace"
81736
+ });
81737
+ this.log.info(`slack: opening new socket at runtime (${group.integrations.length} integration(s): ${group.integrations.map((i) => i.agentId).join(", ")})…`);
81738
+ await conn.start();
81739
+ this.log.info(`slack: runtime socket connected as bot user ${conn.botUserId}`);
81740
+ for (const { integrationId } of group.integrations) {
81741
+ this.botUserIds[integrationId] = conn.botUserId;
81742
+ this.connByIntegration.set(integrationId, conn);
81743
+ }
81744
+ this.connections.push(conn);
81745
+ } catch (err) {
81746
+ this.log.error(`slack: failed to open runtime socket for appToken — leaving existing sockets intact: ${formatErr(err)}`);
81747
+ }
81587
81748
  }
81588
- for (const a of toStart) this.agents.set(a.id, a);
81589
81749
  }
81590
81750
  ensureHost(agentId, cfg) {
81591
81751
  let host = this.hosts.get(agentId);