@agentconnect.md/daemon 1.0.0-rc.23 → 1.0.0-rc.25

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
@@ -7567,6 +7567,9 @@ const AgentSchema = object({
7567
7567
  "paused"
7568
7568
  ]).default("active"),
7569
7569
  runtime: string(),
7570
+ description: string().optional(),
7571
+ reasoningEffort: string().optional(),
7572
+ executionMode: string().optional(),
7570
7573
  runtimeOverrides: object({
7571
7574
  model: string().optional(),
7572
7575
  env: array(object({
@@ -16741,6 +16744,9 @@ var AcpHost = class {
16741
16744
  opts;
16742
16745
  child;
16743
16746
  conn;
16747
+ live = /* @__PURE__ */ new Set();
16748
+ loadingSessions = /* @__PURE__ */ new Set();
16749
+ canLoad = false;
16744
16750
  constructor(runtime, opts) {
16745
16751
  this.runtime = runtime;
16746
16752
  this.opts = opts;
@@ -16764,6 +16770,7 @@ var AcpHost = class {
16764
16770
  const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
16765
16771
  const self = this;
16766
16772
  this.conn = client({ name: "agentconnect" }).onNotification(methods.client.session.update, (ctx) => {
16773
+ if (self.loadingSessions.has(ctx.params.sessionId)) return;
16767
16774
  self.opts.onUpdate(ctx.params.sessionId, ctx.params.update);
16768
16775
  }).onRequest(methods.client.session.requestPermission, (ctx) => {
16769
16776
  const optionId = (ctx.params.options.find((o) => o.kind === "allow_once" || o.kind === "allow_always") ?? ctx.params.options[0])?.optionId;
@@ -16772,19 +16779,52 @@ var AcpHost = class {
16772
16779
  optionId
16773
16780
  } : { outcome: "cancelled" } };
16774
16781
  }).connect(stream);
16775
- await this.conn.agent.request(methods.agent.initialize, {
16782
+ const init = await this.conn.agent.request(methods.agent.initialize, {
16776
16783
  protocolVersion: PROTOCOL_VERSION,
16777
16784
  clientCapabilities: { fs: {
16778
16785
  readTextFile: false,
16779
16786
  writeTextFile: false
16780
16787
  } }
16781
16788
  });
16789
+ this.canLoad = init.agentCapabilities?.loadSession ?? false;
16790
+ this.opts.log?.debug(`acp: agent initialized (loadSession capability=${this.canLoad})`);
16782
16791
  }
16783
16792
  async newSession(cwd) {
16784
- return (await this.conn.agent.request(methods.agent.session.new, {
16793
+ const res = await this.conn.agent.request(methods.agent.session.new, {
16785
16794
  cwd,
16786
16795
  mcpServers: []
16787
- })).sessionId;
16796
+ });
16797
+ this.live.add(res.sessionId);
16798
+ return res.sessionId;
16799
+ }
16800
+ /** True iff THIS agent process created or loaded `sessionId` in its current lifetime. */
16801
+ hasSession(sessionId) {
16802
+ return this.live.has(sessionId);
16803
+ }
16804
+ /** Whether the agent advertised the `loadSession` capability (session/load is usable). */
16805
+ loadSupported() {
16806
+ return this.canLoad;
16807
+ }
16808
+ /** Resume a previously-created session by id (ACP `session/load`). The agent
16809
+ * restores its own history server-side; its replayed session/update stream is
16810
+ * suppressed so it isn't re-posted. Throws if the agent can't load the id
16811
+ * (caller falls back to newSession). */
16812
+ async loadSession(sessionId, cwd) {
16813
+ this.loadingSessions.add(sessionId);
16814
+ try {
16815
+ await this.conn.agent.request(methods.agent.session.load, {
16816
+ sessionId,
16817
+ cwd,
16818
+ mcpServers: []
16819
+ });
16820
+ this.live.add(sessionId);
16821
+ this.opts.log?.info(`acp: resumed session ${sessionId} via session/load`);
16822
+ } catch (err) {
16823
+ this.opts.log?.debug(`acp: session/load failed for ${sessionId} (${err.message}) — will recreate`);
16824
+ throw err;
16825
+ } finally {
16826
+ this.loadingSessions.delete(sessionId);
16827
+ }
16788
16828
  }
16789
16829
  async prompt(sessionId, blocks) {
16790
16830
  return (await this.conn.agent.request(methods.agent.session.prompt, {
@@ -21140,7 +21180,7 @@ const defaultExec = (cmd, args) => new Promise((resolve) => {
21140
21180
  /** macOS launchd controller. Writes a LaunchAgent plist that runs
21141
21181
  * `<node> <cli-entry> run`, and drives it with `launchctl bootstrap/bootout`
21142
21182
  * (falling back to legacy `load/unload` on older macOS). */
21143
- const LABEL = "co.sentio.agentconnect";
21183
+ const LABEL = "md.agentconnect.daemon";
21144
21184
  function buildPlist(a) {
21145
21185
  const env = a.includeRootEnv ? ` <key>EnvironmentVariables</key>\n <dict>\n <key>AGENTCONNECT_ROOT</key>\n <string>${a.root}</string>\n </dict>\n` : "";
21146
21186
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -22948,12 +22988,75 @@ function watch$1(paths, options = {}) {
22948
22988
  return watcher;
22949
22989
  }
22950
22990
  //#endregion
22991
+ //#region src/agents/cp-overlay.ts
22992
+ /** Apply a CP `AgentSpec` onto a local agent, returning a new overlaid agent. */
22993
+ function overlayCpSpec(base, spec) {
22994
+ const envMap = new Map((base.runtimeOverrides?.env ?? []).map((e) => [e.name, e.value]));
22995
+ for (const [name, value] of Object.entries(spec.env ?? {})) envMap.set(name, value);
22996
+ const env = [...envMap].map(([name, value]) => ({
22997
+ name,
22998
+ value
22999
+ }));
23000
+ return {
23001
+ ...base,
23002
+ name: spec.name,
23003
+ ...spec.description !== void 0 ? { description: spec.description } : {},
23004
+ ...spec.reasoningEffort !== void 0 ? { reasoningEffort: spec.reasoningEffort } : {},
23005
+ ...spec.executionMode !== void 0 ? { executionMode: spec.executionMode } : {},
23006
+ runtimeOverrides: {
23007
+ ...base.runtimeOverrides,
23008
+ ...spec.model !== void 0 ? { model: spec.model } : {},
23009
+ env
23010
+ }
23011
+ };
23012
+ }
23013
+ /**
23014
+ * Runtime config the daemon can only surface to an ACP child as environment
23015
+ * variables: ACP `session/new`/`initialize` carry no model or system-prompt
23016
+ * field (SDK v1), so model / reasoning / prompt are exposed under `AGENTCONNECT_*`
23017
+ * for runtimes that read them. (`runtimeOverrides.env` is applied separately by
23018
+ * `agentChildEnv`.) Only emits a key when the value is set.
23019
+ */
23020
+ function cpRuntimeEnv(agent) {
23021
+ const out = {};
23022
+ if (agent.runtimeOverrides?.model) out.AGENTCONNECT_MODEL = agent.runtimeOverrides.model;
23023
+ if (agent.reasoningEffort) out.AGENTCONNECT_REASONING_EFFORT = agent.reasoningEffort;
23024
+ if (agent.executionMode) out.AGENTCONNECT_EXECUTION_MODE = agent.executionMode;
23025
+ if (agent.description) out.AGENTCONNECT_SYSTEM_PROMPT = agent.description;
23026
+ return out;
23027
+ }
23028
+ //#endregion
22951
23029
  //#region src/reconciler/reconciler.ts
22952
- function diffAgents(desired, actualIds) {
23030
+ /** Stable identity-free signature of an agent's effective config. Excludes the
23031
+ * loader-only `dir`/`env` fields (present on `LoadedAgent`) so the comparison is
23032
+ * over the agent's behaviour, not where it was found. Both sides are the LOADED
23033
+ * form (interpolated, path-absolutized), so this is apples-to-apples and stable
23034
+ * across re-serialization (zod key order is fixed by the schema). */
23035
+ function signature(a) {
23036
+ const { dir, env, ...rest } = a;
23037
+ return JSON.stringify(rest);
23038
+ }
23039
+ /**
23040
+ * Diff desired (freshly loaded active agents) against the running set.
23041
+ * - `toStart` — desired ids not currently running.
23042
+ * - `toStop` — running ids no longer desired.
23043
+ * - `toRestart` — same id, changed config (runtime/workspace/integrations/…): the
23044
+ * host must be evicted so the next session picks up fresh config (design §5.2).
23045
+ * Without this, an in-place edit to an existing agent.json is silently a no-op.
23046
+ */
23047
+ function diffAgents(desired, actual) {
22953
23048
  const desiredIds = new Set(desired.map((a) => a.id));
23049
+ const toStart = [];
23050
+ const toRestart = [];
23051
+ for (const a of desired) {
23052
+ const cur = actual.get(a.id);
23053
+ if (!cur) toStart.push(a);
23054
+ else if (signature(cur) !== signature(a)) toRestart.push(a);
23055
+ }
22954
23056
  return {
22955
- toStart: desired.filter((a) => !actualIds.includes(a.id)),
22956
- toStop: actualIds.filter((id) => !desiredIds.has(id))
23057
+ toStart,
23058
+ toStop: [...actual.keys()].filter((id) => !desiredIds.has(id)),
23059
+ toRestart
22957
23060
  };
22958
23061
  }
22959
23062
  //#endregion
@@ -22980,6 +23083,10 @@ var LocalStore = class {
22980
23083
  id INTEGER PRIMARY KEY CHECK (id = 1),
22981
23084
  routingEpoch INTEGER, assignments TEXT, globalRules TEXT
22982
23085
  );
23086
+ CREATE TABLE IF NOT EXISTS cp_agents (
23087
+ id INTEGER PRIMARY KEY CHECK (id = 1),
23088
+ specs TEXT
23089
+ );
22983
23090
  `);
22984
23091
  }
22985
23092
  getSession(key) {
@@ -23013,6 +23120,13 @@ var LocalStore = class {
23013
23120
  globalRules
23014
23121
  });
23015
23122
  }
23123
+ getCpAgents() {
23124
+ return this.db.prepare("SELECT specs FROM cp_agents WHERE id = 1").get();
23125
+ }
23126
+ setCpAgents(specs) {
23127
+ this.db.prepare(`INSERT INTO cp_agents (id, specs) VALUES (1, @specs)
23128
+ ON CONFLICT(id) DO UPDATE SET specs=excluded.specs`).run({ specs });
23129
+ }
23016
23130
  close() {
23017
23131
  this.db.close();
23018
23132
  }
@@ -23057,6 +23171,24 @@ var SessionManager = class {
23057
23171
  updatedAt: Date.now()
23058
23172
  };
23059
23173
  this.deps.store.upsertSession(rec);
23174
+ } else if (host.hasSession?.(rec.acpSessionId) === false) {
23175
+ const cwd = await prepareWorkspace(agent);
23176
+ let resumed = false;
23177
+ if (host.loadSupported?.()) try {
23178
+ await host.loadSession(rec.acpSessionId, cwd);
23179
+ resumed = true;
23180
+ } catch {}
23181
+ if (!resumed) {
23182
+ const acpSessionId = await host.newSession(cwd);
23183
+ rec = {
23184
+ ...rec,
23185
+ acpSessionId,
23186
+ state: "idle",
23187
+ lastDeliveredTs: null,
23188
+ updatedAt: Date.now()
23189
+ };
23190
+ this.deps.store.upsertSession(rec);
23191
+ }
23060
23192
  }
23061
23193
  const gap = this.deps.store.transcriptSince(msg.channel, thread, rec.lastDeliveredTs);
23062
23194
  const blocks = [];
@@ -23128,11 +23260,8 @@ function routeRules(msg, rules, threadOwner) {
23128
23260
  if (msg.thread) {
23129
23261
  const owner = threadOwner(msg.channel, msg.thread);
23130
23262
  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
- }
23263
+ const ownerRule = scopeCandidates.find((x) => x.agentId === owner);
23264
+ if (ownerRule) return pickRule(ownerRule);
23136
23265
  }
23137
23266
  }
23138
23267
  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;
@@ -80456,6 +80585,14 @@ var CpClient = class {
80456
80585
  case "route/update":
80457
80586
  this.deps.configApply.applyRouteUpdate(frame.payload);
80458
80587
  return;
80588
+ case "agent/upsert": {
80589
+ const u = frame.payload;
80590
+ this.deps.configApply.applyAgentUpsert(u.agentId, u.spec);
80591
+ return;
80592
+ }
80593
+ case "agent/remove":
80594
+ this.deps.configApply.applyAgentRemove(frame.payload.agentId);
80595
+ return;
80459
80596
  case "agent/launch":
80460
80597
  case "agent/stop":
80461
80598
  case "agent/prompt":
@@ -80509,6 +80646,46 @@ var CpCronRegistry = class {
80509
80646
  }
80510
80647
  };
80511
80648
  //#endregion
80649
+ //#region src/cp/cp-agent-registry.ts
80650
+ var CpAgentRegistry = class {
80651
+ io;
80652
+ onChange;
80653
+ specs = /* @__PURE__ */ new Map();
80654
+ constructor(io, onChange) {
80655
+ this.io = io;
80656
+ this.onChange = onChange;
80657
+ const s = io.load();
80658
+ if (s) this.specs = new Map(Object.entries(s));
80659
+ }
80660
+ /** Add or replace one agent's spec (agent/upsert EVT). */
80661
+ upsert(agentId, spec) {
80662
+ this.specs.set(agentId, spec);
80663
+ this.changed();
80664
+ }
80665
+ /** Drop one agent's spec (agent/remove EVT). No-op if absent. */
80666
+ remove(agentId) {
80667
+ if (this.specs.delete(agentId)) this.changed();
80668
+ }
80669
+ /** Make the live set exactly `roster` (register/ok reconcile snapshot). */
80670
+ converge(roster) {
80671
+ const next = /* @__PURE__ */ new Map();
80672
+ for (const { agentId, ...spec } of roster) next.set(agentId, spec);
80673
+ this.specs = next;
80674
+ this.changed();
80675
+ }
80676
+ get(agentId) {
80677
+ return this.specs.get(agentId);
80678
+ }
80679
+ /** agentIds the CP currently wants present. */
80680
+ ids() {
80681
+ return [...this.specs.keys()];
80682
+ }
80683
+ changed() {
80684
+ this.io.save(Object.fromEntries(this.specs));
80685
+ this.onChange();
80686
+ }
80687
+ };
80688
+ //#endregion
80512
80689
  //#region src/cp/config-apply.ts
80513
80690
  const LOG_LEVELS = /* @__PURE__ */ new Set([
80514
80691
  "trace",
@@ -80577,16 +80754,23 @@ var SystemClock = class {
80577
80754
  const systemClock = new SystemClock();
80578
80755
  //#endregion
80579
80756
  //#region src/daemon.ts
80580
- const LOADING_MSGS = [
80581
- "Working on it…",
80582
- "Crunching through it…",
80583
- "Hang tight…"
80584
- ];
80757
+ /** Format an error for logs, surfacing a JSON-RPC/ACP RequestError's `code` and
80758
+ * `data` for an agent-side `Internal error` the actionable detail (the adapter's
80759
+ * underlying exception) lives in `data`, which a bare `.stack` discards. */
80760
+ function formatErr(err) {
80761
+ const e = err;
80762
+ if (e && typeof e.code === "number") {
80763
+ const data = e.data === void 0 ? "" : ` data=${typeof e.data === "string" ? e.data : JSON.stringify(e.data)}`;
80764
+ return `${e.name ?? "Error"}: ${e.message ?? ""} (code=${e.code})${data}`;
80765
+ }
80766
+ return e?.stack ?? String(err);
80767
+ }
80585
80768
  const MAX_QUEUED_PER_SESSION = 10;
80586
80769
  var Daemon = class {
80587
80770
  opts;
80588
80771
  store;
80589
80772
  agents = /* @__PURE__ */ new Map();
80773
+ fileAgents = /* @__PURE__ */ new Map();
80590
80774
  hosts = /* @__PURE__ */ new Map();
80591
80775
  connections = [];
80592
80776
  scheduler;
@@ -80603,6 +80787,7 @@ var Daemon = class {
80603
80787
  runtimeNames = {};
80604
80788
  cpClient;
80605
80789
  cpCrons;
80790
+ cpAgents;
80606
80791
  botUserIds = {};
80607
80792
  cpRouting;
80608
80793
  constructor(opts = {}) {
@@ -80627,7 +80812,7 @@ var Daemon = class {
80627
80812
  this.log.info(`control plane: ${cfg.controlPlane?.enabled ? `enabled (${cfg.controlPlane.url ?? "no url"})` : "disabled — running local"}`);
80628
80813
  this.agentsDir = cfg.agentsDir;
80629
80814
  const agents = this.loadAgentList();
80630
- for (const a of agents) this.agents.set(a.id, a);
80815
+ this.fileAgents = new Map(agents.map((a) => [a.id, a]));
80631
80816
  this.log.info(`loaded ${agents.length} agent(s) from ${this.agentsDir}${agents.length ? `: ${agents.map((a) => a.id).join(", ")}` : ""}`);
80632
80817
  this.root = root;
80633
80818
  const resolvedRuntimes = await resolveRuntimes(cfg, root, {
@@ -80651,13 +80836,21 @@ var Daemon = class {
80651
80836
  },
80652
80837
  save: (s) => this.store.setCpRouting(s.routingEpoch, JSON.stringify(s.assignments), JSON.stringify(s.globalRules))
80653
80838
  });
80839
+ this.cpAgents = new CpAgentRegistry({
80840
+ load: () => {
80841
+ const row = this.store.getCpAgents();
80842
+ return row ? JSON.parse(row.specs) : void 0;
80843
+ },
80844
+ save: (s) => this.store.setCpAgents(JSON.stringify(s))
80845
+ }, () => void this.reconcile().catch((err) => this.log.error(`cp: agent reconcile failed: ${err.stack ?? err}`)));
80846
+ for (const a of this.effectiveAgents()) this.agents.set(a.id, a);
80654
80847
  this.sessions = new SessionManager({
80655
80848
  store: this.store,
80656
80849
  hostFor: (agentId) => this.ensureHostAsync(agentId),
80657
80850
  agentById: (id) => this.agents.get(id)
80658
80851
  });
80659
80852
  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}`)),
80853
+ onFire: (agentId, msg) => void this.dispatch(agentId, msg).catch((err) => this.log.error(`cron dispatch failed for agent "${agentId}": ${formatErr(err)}`)),
80661
80854
  newTraceId: () => randomUUID()
80662
80855
  });
80663
80856
  const groups = consolidate(agents);
@@ -80706,8 +80899,54 @@ var Daemon = class {
80706
80899
  loadAgentList() {
80707
80900
  return this.opts.agentName ? [selectAgent(this.agentsDir, this.opts.agentName)] : loadAgents(this.agentsDir);
80708
80901
  }
80902
+ /**
80903
+ * Effective agent set = on-disk `agent.json` base with the CP spec overlaid
80904
+ * (joined by id; CP owns name/description/model/reasoning/execution/env, the
80905
+ * file owns runtime/workspace). CP-only ids with no local base are skipped —
80906
+ * not runnable — and surfaced via cpDegradedScopes().
80907
+ */
80908
+ effectiveAgents() {
80909
+ return [...this.fileAgents.values()].map((base) => {
80910
+ const spec = this.cpAgents?.get(base.id);
80911
+ return spec ? overlayCpSpec(base, spec) : base;
80912
+ });
80913
+ }
80914
+ /**
80915
+ * Converge the running set to the desired effective agents. Driven by both the
80916
+ * `agents/**` file-watch AND CP convergence (agent/upsert, agent/remove, the
80917
+ * register/ok roster, via the registry's onChange). `diffAgents` detects a
80918
+ * changed effective config (runtime/workspace/model/prompt/env/…) and restarts
80919
+ * the host lazily so the next session picks up fresh config.
80920
+ *
80921
+ * Single-flight: overlapping triggers (e.g. a burst of CP frames, or a file
80922
+ * event landing mid-reconcile) coalesce into one trailing re-run so we never
80923
+ * run two passes concurrently and double-evict a host.
80924
+ */
80925
+ reconcileRun;
80926
+ reconcilePending = false;
80709
80927
  async reconcile() {
80710
- const { toStart, toStop } = diffAgents(this.loadAgentList(), [...this.agents.keys()]);
80928
+ if (this.reconcileRun) {
80929
+ this.reconcilePending = true;
80930
+ return this.reconcileRun;
80931
+ }
80932
+ this.reconcileRun = this.runReconcile();
80933
+ try {
80934
+ await this.reconcileRun;
80935
+ } finally {
80936
+ this.reconcileRun = void 0;
80937
+ }
80938
+ if (this.reconcilePending) {
80939
+ this.reconcilePending = false;
80940
+ await this.reconcile();
80941
+ }
80942
+ }
80943
+ async runReconcile() {
80944
+ const files = this.loadAgentList();
80945
+ this.fileAgents = new Map(files.map((a) => [a.id, a]));
80946
+ const desired = this.effectiveAgents();
80947
+ const { toStart, toStop, toRestart } = diffAgents(desired, this.agents);
80948
+ 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(", ")}]`);
80949
+ else this.log.debug(`reconcile: no changes (${desired.length} desired agent(s))`);
80711
80950
  for (const id of toStop) {
80712
80951
  const host = this.hosts.get(id);
80713
80952
  if (host) {
@@ -80717,6 +80956,15 @@ var Daemon = class {
80717
80956
  this.hostStarts.delete(id);
80718
80957
  this.agents.delete(id);
80719
80958
  }
80959
+ for (const a of toRestart) {
80960
+ const host = this.hosts.get(a.id);
80961
+ if (host) {
80962
+ await host.stop();
80963
+ this.hosts.delete(a.id);
80964
+ }
80965
+ this.hostStarts.delete(a.id);
80966
+ this.agents.set(a.id, a);
80967
+ }
80720
80968
  for (const a of toStart) this.agents.set(a.id, a);
80721
80969
  }
80722
80970
  ensureHost(agentId, cfg) {
@@ -80730,7 +80978,11 @@ var Daemon = class {
80730
80978
  if (!runtime) throw new Error(`runtime "${agent.runtime}" not available: not installed on this host, or absent from config.runtimes / the ACP registry`);
80731
80979
  host = new AcpHost(runtime, {
80732
80980
  onUpdate,
80733
- env: agentChildEnv(agent)
80981
+ env: {
80982
+ ...agentChildEnv(agent),
80983
+ ...cpRuntimeEnv(agent)
80984
+ },
80985
+ log: this.log
80734
80986
  });
80735
80987
  }
80736
80988
  this.hosts.set(agentId, host);
@@ -80755,7 +81007,7 @@ var Daemon = class {
80755
81007
  return;
80756
81008
  }
80757
81009
  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}`));
81010
+ this.dispatch(result.agentId, msg, result.integrationId).catch((err) => this.log.error(`dispatch failed for agent "${result.agentId}": ${formatErr(err)}`));
80759
81011
  }
80760
81012
  queued = /* @__PURE__ */ new Map();
80761
81013
  /**
@@ -80833,10 +81085,15 @@ var Daemon = class {
80833
81085
  resolveCpAgent(agentId) {
80834
81086
  return resolveAgentIntegration(this.agents.get(agentId), this.botUserIds);
80835
81087
  }
80836
- /** agentIds of CP rules that currently resolve to null (no servable Slack integration). */
81088
+ /**
81089
+ * agentIds the daemon can't fully serve: CP routing rules with no servable
81090
+ * Slack integration, plus CP agent specs with no on-disk base (no runtime /
81091
+ * workspace to materialize — the spec alone can't be run).
81092
+ */
80837
81093
  cpDegradedScopes() {
80838
81094
  const out = /* @__PURE__ */ new Set();
80839
81095
  for (const cpRule of this.cpRouting?.effectiveRules() ?? []) if (!this.resolveCpAgent(cpRule.agentId)) out.add(cpRule.agentId);
81096
+ for (const id of this.cpAgents?.ids() ?? []) if (!this.fileAgents.has(id)) out.add(id);
80840
81097
  return [...out];
80841
81098
  }
80842
81099
  async dispatch(agentId, msg, integrationId) {
@@ -80844,7 +81101,7 @@ var Daemon = class {
80844
81101
  const replyConn = this.replyConnFor(agentId, integrationId);
80845
81102
  const wasRunning = this.hostStarts.has(agentId);
80846
81103
  const statusThread = msg.thread ?? msg.msgId;
80847
- replyConn?.setStatus(msg.channel, statusThread, wasRunning ? "is thinking…" : "is starting up…", LOADING_MSGS);
81104
+ replyConn?.setStatus(msg.channel, statusThread, wasRunning ? "is thinking…" : "is starting up…");
80848
81105
  const { sessionId, blocks } = await this.sessions.handle(agentId, msg);
80849
81106
  this.pending.set(sessionId, {
80850
81107
  conv,
@@ -80854,7 +81111,7 @@ var Daemon = class {
80854
81111
  });
80855
81112
  try {
80856
81113
  const host = await this.ensureHostAsync(agentId);
80857
- if (!wasRunning) replyConn?.setStatus(msg.channel, statusThread, "is thinking…", LOADING_MSGS);
81114
+ if (!wasRunning) replyConn?.setStatus(msg.channel, statusThread, "is thinking…");
80858
81115
  await host.prompt(sessionId, blocks);
80859
81116
  for (const action of conv.onFinal(`local://session/${sessionId}`)) await this.applyAction(action, replyConn, msg.channel, statusThread);
80860
81117
  } finally {
@@ -80862,10 +81119,10 @@ var Daemon = class {
80862
81119
  }
80863
81120
  this.flushQueued(agentId, sessionId, integrationId);
80864
81121
  }
80865
- /** Route a converger action: set-status → setStatus (loading_messages only when not clearing); else postMessage. */
81122
+ /** Route a converger action: set-status → setStatus (status text only; '' clears); else postMessage. */
80866
81123
  async applyAction(action, conn, channel, thread) {
80867
81124
  if (action.kind === "set-status") {
80868
- if (conn && thread) await conn.setStatus(channel, thread, action.text, action.text ? LOADING_MSGS : void 0);
81125
+ if (conn && thread) await conn.setStatus(channel, thread, action.text);
80869
81126
  return;
80870
81127
  }
80871
81128
  await conn?.postMessage(channel, action.text, thread);
@@ -80900,6 +81157,7 @@ var Daemon = class {
80900
81157
  },
80901
81158
  applyReconcileSnapshot: (snap) => {
80902
81159
  this.cpCrons?.converge(snap.crons);
81160
+ this.cpAgents?.converge(snap.agents);
80903
81161
  this.cpRouting?.converge({
80904
81162
  routingEpoch: snap.routingEpoch,
80905
81163
  assignments: snap.assignments,
@@ -80907,7 +81165,10 @@ var Daemon = class {
80907
81165
  });
80908
81166
  if (snap.leases.length) this.log.debug(`cp: ${snap.leases.length} lease(s) noted (secrets handled later)`);
80909
81167
  if (snap.assignments.length) this.log.debug(`cp: converged ${snap.assignments.length} assignment(s)`);
81168
+ if (snap.agents.length) this.log.debug(`cp: converged ${snap.agents.length} agent spec(s)`);
80910
81169
  },
81170
+ applyAgentUpsert: (agentId, spec) => this.cpAgents?.upsert(agentId, spec),
81171
+ applyAgentRemove: (agentId) => this.cpAgents?.remove(agentId),
80911
81172
  upsertCron: (cron) => this.cpCrons.upsert(cron),
80912
81173
  removeCron: (cronId) => this.cpCrons.remove(cronId),
80913
81174
  applyRouteAssign: (a) => this.cpRouting?.upsertAssign(a),