@agentconnect.md/daemon 1.0.0-rc.289 → 1.0.0-rc.290

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.
@@ -3,19 +3,19 @@ import { n as require_ms, t as require_src$3 } from "./src-DDtlVutj.js";
3
3
  import { n as init_esm_shims, t as __dirname } from "./esm-shims-DxGcm4Hw.js";
4
4
  import { t as DAEMON_VERSION } from "./version-DGaRXjBK.js";
5
5
  import { C as _coercedNumber, T as NEVER, a as array, d as literal, f as looseObject, i as any, m as object, o as boolean, p as number$1, t as ZodNumber, v as string, x as url$4 } from "./schemas-CQkO9hC5.js";
6
- import { C as ZodIssueCode, S as RESERVED_MCP_SERVER_NAME, _ as buildEnvelope, a as agentChildEnv, b as MAX_FRAME_BYTES, c as persistRelays, d as normalizeGitUrl, f as RELAY_DAEMON_SUBPROTOCOL, g as SHARED_CONFIG_ACTION_ID, h as decodeRelayDaemonFrame, i as selectAgent, m as buildRelayDaemonFrame, n as findAgentFiles, o as loadConfig, p as RELAY_DAEMON_WS_PATH, r as loadAgents, s as persistDaemonId, v as decodeEnvelope, y as encode } from "./load-agents-BLNSM987.js";
6
+ import { C as ZodIssueCode, S as RESERVED_MCP_SERVER_NAME, _ as buildEnvelope, a as agentChildEnv, b as MAX_FRAME_BYTES, c as persistRelays, d as normalizeGitUrl, f as RELAY_DAEMON_SUBPROTOCOL, g as SHARED_CONFIG_ACTION_ID, h as decodeRelayDaemonFrame, i as selectAgent, m as buildRelayDaemonFrame, n as findAgentFiles, o as loadConfig, p as RELAY_DAEMON_WS_PATH, r as loadAgents, s as persistDaemonId, v as decodeEnvelope, y as encode } from "./load-agents-CZJD_fRG.js";
7
7
  import { l as resolveRoot, o as mcpSocketPath, u as statePath } from "./paths-DEcUO8lC.js";
8
- import { S as resolveRuntimes, _ as writeMemoryFile, a as initGitInjection, b as cachedRuntimeNames, c as simpleGit, d as memoryProviderFor, f as MemoryConflictError, g as resolveInMemoryDir, h as listMemory, i as prepareWorkspace, l as createMemoryProvider, m as MemoryTooLargeError, n as installedRuntimes, o as probeGitVersion, p as MemoryPathError, r as prefetchWorkspace, s as sessionGitEnv, t as AcpHost, u as memoryKindOf, v as MEMORY_TOOL_NAMES, x as cachedRuntimeVersions, y as toolsForIntegrations } from "./acp-host-BlkeNrQB.js";
8
+ import { S as resolveRuntimes, _ as writeMemoryFile, a as initGitInjection, b as cachedRuntimeNames, c as simpleGit, d as memoryProviderFor, f as MemoryConflictError, g as resolveInMemoryDir, h as listMemory, i as prepareWorkspace, l as createMemoryProvider, m as MemoryTooLargeError, n as installedRuntimes, o as probeGitVersion, p as MemoryPathError, r as prefetchWorkspace, s as sessionGitEnv, t as AcpHost, u as memoryKindOf, v as MEMORY_TOOL_NAMES, x as cachedRuntimeVersions, y as toolsForIntegrations } from "./acp-host-B91pZ6PG.js";
9
9
  import { i as GitCredentialCache, n as gitcredSocketPath, r as writeGitcredShim, t as GitCredServer } from "./gitcred-server-Dhp-tyEI.js";
10
10
  import { B as ToolListChangedNotificationSchema, D as ListPromptsResultSchema, E as ListChangedOptionsBaseSchema, F as PromptListChangedNotificationSchema, G as isZ4Schema, H as isJSONRPCRequest, I as ReadResourceResultSchema, J as encodeFrame, K as safeParse, L as ResourceListChangedNotificationSchema, M as ListToolsResultSchema, O as ListResourceTemplatesResultSchema, P as McpError, R as SUPPORTED_PROTOCOL_VERSIONS, S as InitializeResultSchema, T as LATEST_PROTOCOL_VERSION, U as isJSONRPCResultResponse, V as isInitializedNotification, W as getObjectShape, _ as ElicitResultSchema, a as AjvJsonSchemaValidator, b as GetPromptResultSchema, c as mergeCapabilities, d as CompleteResultSchema, f as CreateMessageRequestSchema, g as ElicitRequestSchema, h as CreateTaskResultSchema, i as assertToolsCallTaskCapability, k as ListResourcesResultSchema, m as CreateMessageResultWithToolsSchema, n as serializeMessage, o as require_fast_deep_equal, p as CreateMessageResultSchema, q as decodeFrames, r as assertClientRequestTaskCapability, s as Protocol, t as ReadBuffer, u as CallToolResultSchema, v as EmptyResultSchema, w as JSONRPCMessageSchema, y as ErrorCode } from "./stdio-DnS8WcXi.js";
11
11
  import { URL as URL$1 } from "url";
12
12
  import * as tls from "tls";
13
13
  import * as net$5 from "net";
14
14
  import * as path$16 from "node:path";
15
- import { dirname, join, relative, resolve, sep } from "node:path";
15
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
16
16
  import * as http$8 from "http";
17
17
  import { Agent } from "https";
18
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, promises, readFileSync, rmSync, stat, unwatchFile, watch, watchFile, writeFileSync } from "node:fs";
18
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, promises, readFileSync, readdirSync, renameSync, rmSync, stat, unwatchFile, watch, watchFile, writeFileSync } from "node:fs";
19
19
  import { EventEmitter } from "node:events";
20
20
  import process$1 from "node:process";
21
21
  import { randomBytes, randomUUID } from "node:crypto";
@@ -4527,6 +4527,12 @@ var CpClient = class {
4527
4527
  case "agent/remove":
4528
4528
  this.deps.configApply.applyAgentRemove(frame.payload.agentId);
4529
4529
  return;
4530
+ case "agent/detach":
4531
+ this.deps.configApply.applyAgentDetach(frame.payload).then((ack) => this.reply(frame, "ack", ack)).catch((err) => this.sendError(frame.id, "INTERNAL", `agent/detach failed: ${err.message}`, false));
4532
+ return;
4533
+ case "agent/activate":
4534
+ this.deps.configApply.applyAgentActivate(frame.payload).then((ack) => this.reply(frame, "ack", ack)).catch((err) => this.sendError(frame.id, "INTERNAL", `agent/activate failed: ${err.message}`, false));
4535
+ return;
4530
4536
  case "integration/upsert":
4531
4537
  this.deps.configApply.applyIntegrationUpsert(frame.payload);
4532
4538
  return;
@@ -198188,6 +198194,227 @@ async function fetchHeadMtime(root) {
198188
198194
  * without the key leaves the local value alone — hand-authored agent.json keeps
198189
198195
  * working).
198190
198196
  */
198197
+ const DETACHED_DIR = ".detached";
198198
+ const STAGED_DIR = ".staged";
198199
+ const DETACHED_DATA_DIR = "agent";
198200
+ const DETACHED_META_FILE = "metadata.json";
198201
+ const MOVE_META_FILE = "metadata.json";
198202
+ /** Stable cold-move archive root. It is hidden, so agent discovery ignores it. */
198203
+ function detachedAgentDir(agentsDir, agentId) {
198204
+ return join(agentsDir, DETACHED_DIR, agentId);
198205
+ }
198206
+ function stagedAgentDir(agentsDir, agentId) {
198207
+ return join(agentsDir, STAGED_DIR, agentId);
198208
+ }
198209
+ function writeMoveStage(agentsDir, agentId, metadata) {
198210
+ const root = stagedAgentDir(agentsDir, agentId);
198211
+ const file = join(root, MOVE_META_FILE);
198212
+ const temp = join(root, `${MOVE_META_FILE}.tmp`);
198213
+ mkdirSync(root, { recursive: true });
198214
+ try {
198215
+ writeFileSync(temp, JSON.stringify(metadata, null, 2) + "\n");
198216
+ renameSync(temp, file);
198217
+ } catch (err) {
198218
+ rmSync(temp, { force: true });
198219
+ throw err;
198220
+ }
198221
+ }
198222
+ /** Durable fail-closed tombstone: a new move atomically supersedes the old id/state. */
198223
+ function stageAgentMove(agentsDir, agentId, moveId) {
198224
+ writeMoveStage(agentsDir, agentId, {
198225
+ moveId,
198226
+ state: "staging"
198227
+ });
198228
+ }
198229
+ function readAgentMoveStage(agentsDir, agentId) {
198230
+ const root = stagedAgentDir(agentsDir, agentId);
198231
+ if (!existsSync(root)) return void 0;
198232
+ try {
198233
+ const raw = JSON.parse(readFileSync(join(root, MOVE_META_FILE), "utf8"));
198234
+ if (typeof raw.moveId === "string" && (raw.state === "staging" || raw.state === "committed")) return {
198235
+ moveId: raw.moveId,
198236
+ state: raw.state
198237
+ };
198238
+ } catch {}
198239
+ return {
198240
+ moveId: "",
198241
+ state: "staging"
198242
+ };
198243
+ }
198244
+ function commitAgentMove(agentsDir, agentId, moveId) {
198245
+ const current = readAgentMoveStage(agentsDir, agentId);
198246
+ if (!current || current.moveId !== moveId) throw new Error(`cannot commit move "${moveId}" for agent "${agentId}": staging fence changed`);
198247
+ if (current.state === "committed") return;
198248
+ writeMoveStage(agentsDir, agentId, {
198249
+ moveId,
198250
+ state: "committed"
198251
+ });
198252
+ }
198253
+ function clearAgentMoveStage(agentsDir, agentId) {
198254
+ rmSync(stagedAgentDir(agentsDir, agentId), {
198255
+ recursive: true,
198256
+ force: true
198257
+ });
198258
+ }
198259
+ function agentMoveStages(agentsDir) {
198260
+ const root = join(agentsDir, STAGED_DIR);
198261
+ const out = /* @__PURE__ */ new Map();
198262
+ if (!existsSync(root)) return out;
198263
+ try {
198264
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
198265
+ if (!entry.isDirectory()) continue;
198266
+ const metadata = readAgentMoveStage(agentsDir, entry.name);
198267
+ if (metadata) out.set(entry.name, metadata);
198268
+ }
198269
+ } catch {}
198270
+ return out;
198271
+ }
198272
+ function detachedDataDir(agentsDir, agentId) {
198273
+ return join(detachedAgentDir(agentsDir, agentId), DETACHED_DATA_DIR);
198274
+ }
198275
+ function scrubAgentIntegrations(file) {
198276
+ const original = readFileSync(file, "utf8");
198277
+ const raw = JSON.parse(original);
198278
+ if (Array.isArray(raw.integrations) && raw.integrations.length === 0) return original;
198279
+ raw.integrations = [];
198280
+ writeFileSync(file, JSON.stringify(raw, null, 2) + "\n");
198281
+ return original;
198282
+ }
198283
+ /**
198284
+ * Move an active agent root out of the discovery tree without deleting any of
198285
+ * its daemon-local state. The metadata preserves custom/nested directory names
198286
+ * so a later activate or upsert can restore the exact original location.
198287
+ */
198288
+ function archiveAgent(agentsDir, agentId) {
198289
+ const file = findAgentFileById(agentsDir, agentId);
198290
+ const archived = detachedDataDir(agentsDir, agentId);
198291
+ if (!file) {
198292
+ if (!existsSync(archived)) return "missing";
198293
+ const archivedFile = findAgentFileById(archived, agentId);
198294
+ if (!archivedFile) throw new Error(`cannot detach agent "${agentId}": detached archive is incomplete`);
198295
+ scrubAgentIntegrations(archivedFile);
198296
+ return "already-detached";
198297
+ }
198298
+ const base = resolve(agentsDir);
198299
+ const activeDir = resolve(dirname(file));
198300
+ const relativePath = relative(base, activeDir);
198301
+ if (!relativePath || isAbsolute(relativePath) || relativePath === ".." || relativePath.startsWith(`..${sep}`)) throw new Error(`cannot detach agent "${agentId}": its root is not a child of agentsDir`);
198302
+ const archiveRoot = detachedAgentDir(agentsDir, agentId);
198303
+ if (existsSync(archiveRoot)) if (!existsSync(archived)) rmSync(archiveRoot, {
198304
+ recursive: true,
198305
+ force: true
198306
+ });
198307
+ else throw new Error(`cannot detach agent "${agentId}": an archive already exists while the agent is active`);
198308
+ const metadata = {
198309
+ agentId,
198310
+ relativePath
198311
+ };
198312
+ const originalAgentJson = readFileSync(file, "utf8");
198313
+ try {
198314
+ mkdirSync(archiveRoot, { recursive: true });
198315
+ scrubAgentIntegrations(file);
198316
+ writeFileSync(join(archiveRoot, DETACHED_META_FILE), JSON.stringify(metadata, null, 2) + "\n");
198317
+ renameSync(activeDir, archived);
198318
+ } catch (err) {
198319
+ if (existsSync(file)) writeFileSync(file, originalAgentJson);
198320
+ rmSync(archiveRoot, {
198321
+ recursive: true,
198322
+ force: true
198323
+ });
198324
+ throw err;
198325
+ }
198326
+ return "archived";
198327
+ }
198328
+ /** Restore a cold-move archive to its exact former relative location. */
198329
+ function restoreArchivedAgent(agentsDir, agentId) {
198330
+ const archiveRoot = detachedAgentDir(agentsDir, agentId);
198331
+ const archived = detachedDataDir(agentsDir, agentId);
198332
+ if (!existsSync(archiveRoot)) return false;
198333
+ if (!existsSync(archived)) throw new Error(`cannot activate agent "${agentId}": detached archive is incomplete`);
198334
+ let metadata;
198335
+ try {
198336
+ metadata = JSON.parse(readFileSync(join(archiveRoot, DETACHED_META_FILE), "utf8"));
198337
+ } catch {
198338
+ throw new Error(`cannot activate agent "${agentId}": detached archive metadata is invalid`);
198339
+ }
198340
+ if (metadata.agentId !== agentId || typeof metadata.relativePath !== "string" || !metadata.relativePath) throw new Error(`cannot activate agent "${agentId}": detached archive metadata does not match`);
198341
+ const base = resolve(agentsDir);
198342
+ const target = resolve(base, metadata.relativePath);
198343
+ if (target === base || !target.startsWith(base + sep)) throw new Error(`cannot activate agent "${agentId}": detached archive path escapes agentsDir`);
198344
+ if (existsSync(target)) throw new Error(`cannot activate agent "${agentId}": original path is already occupied`);
198345
+ if (!findAgentFileById(archived, agentId)) throw new Error(`cannot activate agent "${agentId}": detached archive does not contain the agent`);
198346
+ mkdirSync(dirname(target), { recursive: true });
198347
+ renameSync(archived, target);
198348
+ rmSync(archiveRoot, {
198349
+ recursive: true,
198350
+ force: true
198351
+ });
198352
+ return true;
198353
+ }
198354
+ /**
198355
+ * Exact-set the CP-owned dependents restored from a move-back archive BEFORE
198356
+ * the agent becomes visible to reconcile. CP crons are explicitly tagged and
198357
+ * can coexist with hand-authored entries. Integrations historically have no
198358
+ * ownership marker, so move activation deliberately treats the CP list as
198359
+ * authoritative/fail-closed and removes every non-listed integration; retaining
198360
+ * an unknown stale bot credential would be the less safe ambiguity.
198361
+ */
198362
+ function pruneMovedAgentDependents(agentsDir, agentId, desired) {
198363
+ const file = findAgentFileById(agentsDir, agentId);
198364
+ if (!file) return false;
198365
+ const raw = JSON.parse(readFileSync(file, "utf8"));
198366
+ const integrationIds = new Set(desired.integrationIds);
198367
+ const cronIds = new Set(desired.cronIds);
198368
+ let changed = false;
198369
+ if (Array.isArray(raw.integrations)) {
198370
+ const seen = /* @__PURE__ */ new Set();
198371
+ const next = raw.integrations.filter((integration) => {
198372
+ if (typeof integration !== "object" || integration === null) return false;
198373
+ const id = integration.id;
198374
+ if (typeof id !== "string" || !integrationIds.has(id) || seen.has(id)) return false;
198375
+ seen.add(id);
198376
+ return true;
198377
+ });
198378
+ if (JSON.stringify(next) !== JSON.stringify(raw.integrations)) {
198379
+ raw.integrations = next;
198380
+ changed = true;
198381
+ }
198382
+ }
198383
+ const localCronCollisions = /* @__PURE__ */ new Set();
198384
+ if (Array.isArray(raw.crons)) {
198385
+ const seenCp = /* @__PURE__ */ new Set();
198386
+ const next = raw.crons.filter((cron) => {
198387
+ if (typeof cron !== "object" || cron === null) return true;
198388
+ const item = cron;
198389
+ if (item.origin !== "cp") {
198390
+ if (typeof item.id === "string" && cronIds.has(item.id)) localCronCollisions.add(item.id);
198391
+ return true;
198392
+ }
198393
+ if (typeof item.id !== "string" || !cronIds.has(item.id) || seenCp.has(item.id)) return false;
198394
+ seenCp.add(item.id);
198395
+ return true;
198396
+ });
198397
+ if (JSON.stringify(next) !== JSON.stringify(raw.crons)) {
198398
+ raw.crons = next;
198399
+ changed = true;
198400
+ }
198401
+ }
198402
+ if (changed) writeFileSync(file, JSON.stringify(raw, null, 2) + "\n");
198403
+ const finalIntegrations = new Set((Array.isArray(raw.integrations) ? raw.integrations : []).flatMap((integration) => {
198404
+ if (typeof integration !== "object" || integration === null) return [];
198405
+ const id = integration.id;
198406
+ return typeof id === "string" ? [id] : [];
198407
+ }));
198408
+ const finalCpCrons = new Set((Array.isArray(raw.crons) ? raw.crons : []).flatMap((cron) => {
198409
+ if (typeof cron !== "object" || cron === null) return [];
198410
+ const item = cron;
198411
+ return item.origin === "cp" && typeof item.id === "string" ? [item.id] : [];
198412
+ }));
198413
+ const missingIntegrations = desired.integrationIds.filter((id) => !finalIntegrations.has(id));
198414
+ const missingCrons = desired.cronIds.filter((id) => !finalCpCrons.has(id));
198415
+ if (missingIntegrations.length || missingCrons.length || localCronCollisions.size) throw new Error(`cannot activate agent "${agentId}": authoritative dependent bundle did not persist (missing integrations: ${missingIntegrations.join(", ") || "none"}; missing crons: ${missingCrons.join(", ") || "none"}; local cron collisions: ${[...localCronCollisions].join(", ") || "none"})`);
198416
+ return changed;
198417
+ }
198191
198418
  /** Map a CP workspace mode onto the daemon's AgentSchema workspace mode. */
198192
198419
  function mapWorkspaceMode(mode) {
198193
198420
  return mode === "scratch" ? "from-scratch" : "git-repo";
@@ -198267,7 +198494,8 @@ function applySpecFields(raw, spec, opts) {
198267
198494
  * from-scratch, path daemon-generated.
198268
198495
  */
198269
198496
  function writeAgentSpec(agentsDir, agentId, spec, deps) {
198270
- const existingFile = findAgentFileById(agentsDir, agentId);
198497
+ let existingFile = findAgentFileById(agentsDir, agentId);
198498
+ if (!existingFile && restoreArchivedAgent(agentsDir, agentId)) existingFile = findAgentFileById(agentsDir, agentId);
198271
198499
  if (existingFile) {
198272
198500
  const raw = JSON.parse(readFileSync(existingFile, "utf8"));
198273
198501
  applySpecFields(raw, spec, {
@@ -198317,6 +198545,11 @@ function removeAgent(agentsDir, agentId) {
198317
198545
  recursive: true,
198318
198546
  force: true
198319
198547
  });
198548
+ rmSync(detachedAgentDir(agentsDir, agentId), {
198549
+ recursive: true,
198550
+ force: true
198551
+ });
198552
+ clearAgentMoveStage(agentsDir, agentId);
198320
198553
  }
198321
198554
  //#endregion
198322
198555
  //#region src/agents/write-cron.ts
@@ -198461,6 +198694,20 @@ var CpAgentRegistry = class {
198461
198694
  removeAgent(this.agentsDir, agentId);
198462
198695
  this.onChange();
198463
198696
  }
198697
+ /** Non-destructively move an agent root out of the active discovery tree. */
198698
+ detach(agentId) {
198699
+ const result = archiveAgent(this.agentsDir, agentId);
198700
+ if (result !== "missing") this.onChange();
198701
+ return result;
198702
+ }
198703
+ /** Restore a detached root in place. Idempotent while already active. */
198704
+ activate(agentId, desired) {
198705
+ const alreadyActive = !!findAgentFileById(this.agentsDir, agentId);
198706
+ if (!alreadyActive && !restoreArchivedAgent(this.agentsDir, agentId)) return "missing";
198707
+ const pruned = pruneMovedAgentDependents(this.agentsDir, agentId, desired);
198708
+ if (!alreadyActive || pruned) this.onChange();
198709
+ return alreadyActive ? "already-active" : "restored";
198710
+ }
198464
198711
  /**
198465
198712
  * Apply the register/ok reconcile roster: create-or-merge EACH entry and
198466
198713
  * NOTHING else. Never prunes agents absent from the roster (deletion is only
@@ -214428,10 +214675,18 @@ var Daemon = class Daemon {
214428
214675
  requestExit;
214429
214676
  draining = false;
214430
214677
  drainingAgents = /* @__PURE__ */ new Set();
214678
+ moveStagedAgents = /* @__PURE__ */ new Set();
214679
+ moveStageMetadata = /* @__PURE__ */ new Map();
214680
+ activatingAgents = /* @__PURE__ */ new Set();
214681
+ agentMoveTails = /* @__PURE__ */ new Map();
214682
+ agentMoveInFlight = /* @__PURE__ */ new Map();
214683
+ activeDispatchesByAgent = /* @__PURE__ */ new Map();
214684
+ activeReplyConnectionUses = /* @__PURE__ */ new Map();
214431
214685
  hostStopping = /* @__PURE__ */ new Map();
214432
214686
  idleSweepTimer;
214433
214687
  cancelTimers = /* @__PURE__ */ new Map();
214434
214688
  slackRetryTimers = /* @__PURE__ */ new Map();
214689
+ slackRetryRuns = /* @__PURE__ */ new Map();
214435
214690
  constructor(opts = {}) {
214436
214691
  this.opts = opts;
214437
214692
  this.clock = opts.clock ?? systemClock;
@@ -214476,12 +214731,16 @@ var Daemon = class Daemon {
214476
214731
  this.log.info(`starting daemon (root=${root})`);
214477
214732
  this.log.info(`control plane: ${cfg.controlPlane?.enabled ? `enabled (${cfg.controlPlane.url ?? "no url"})` : "disabled — running local"}`);
214478
214733
  this.agentsDir = cfg.agentsDir;
214479
- const agents = this.loadAgentList();
214480
- this.fileAgents = new Map(agents.map((a) => [a.id, a]));
214734
+ this.moveStageMetadata = agentMoveStages(this.agentsDir);
214735
+ this.moveStagedAgents = new Set([...this.moveStageMetadata].filter(([, metadata]) => metadata.state === "staging").map(([agentId]) => agentId));
214736
+ for (const agentId of this.moveStagedAgents) this.drainingAgents.add(agentId);
214737
+ const discoveredAgents = this.loadAgentList();
214738
+ const agents = discoveredAgents.filter((agent) => !this.moveStagedAgents.has(agent.id));
214739
+ this.fileAgents = new Map(discoveredAgents.map((a) => [a.id, a]));
214481
214740
  this.log.info(`loaded ${agents.length} agent(s) from ${this.agentsDir}${agents.length ? `: ${agents.map((a) => a.id).join(", ")}` : ""}`);
214482
214741
  this.root = root;
214483
214742
  const resolvedRuntimes = await resolveRuntimes(cfg, root, {
214484
- neededRuntimes: agents.map((a) => a.runtime),
214743
+ neededRuntimes: discoveredAgents.map((a) => a.runtime),
214485
214744
  mode: "cache-first"
214486
214745
  });
214487
214746
  this.runtimes = installedRuntimes(resolvedRuntimes);
@@ -214628,7 +214887,7 @@ var Daemon = class Daemon {
214628
214887
  this.log.warn(`slack: connection failed for appToken — retrying in 60s: ${formatErr(err)}`);
214629
214888
  const timer = this.clock.setTimeout(() => {
214630
214889
  if (this.draining) return;
214631
- this.retrySlackConnection(group).catch((e) => this.log.error(`slack: retry loop error: ${formatErr(e)}`));
214890
+ this.startSlackRetry(group.appToken);
214632
214891
  }, 6e4);
214633
214892
  this.slackRetryTimers.set(group.appToken, timer);
214634
214893
  }
@@ -214642,7 +214901,7 @@ var Daemon = class Daemon {
214642
214901
  this.watcher = watch$1(this.agentsDir, {
214643
214902
  ignoreInitial: true,
214644
214903
  depth: 4,
214645
- ignored: (p) => /[\\/](node_modules|\.git)([\\/]|$)/.test(p)
214904
+ ignored: (p) => /[\\/](node_modules|\.git|\.detached|\.staged)([\\/]|$)/.test(p)
214646
214905
  });
214647
214906
  const debounced = () => {
214648
214907
  clearTimeout(this.debounceTimer);
@@ -214671,7 +214930,7 @@ var Daemon = class Daemon {
214671
214930
  * routing (all of which read `agent.integrations`) see one merged view.
214672
214931
  */
214673
214932
  effectiveAgents() {
214674
- return [...this.fileAgents.values()];
214933
+ return [...this.fileAgents.values()].filter((agent) => !this.moveStagedAgents.has(agent.id));
214675
214934
  }
214676
214935
  /**
214677
214936
  * Converge the running set to the desired effective agents. Driven by both the
@@ -214726,6 +214985,7 @@ var Daemon = class Daemon {
214726
214985
  this.agents.delete(id);
214727
214986
  this.scheduler.unregister(id);
214728
214987
  }
214988
+ let connectionsDirty = toStart.length > 0 || toStop.length > 0;
214729
214989
  for (const change of toChange) {
214730
214990
  const a = change.agent;
214731
214991
  this.agents.set(a.id, a);
@@ -214739,20 +214999,19 @@ var Daemon = class Daemon {
214739
214999
  this.hostStarts.delete(a.id);
214740
215000
  }
214741
215001
  if (change.workspace) this.prefetchClone(a);
214742
- if (change.integrations) {
214743
- await this.reconcileSlackConnections();
214744
- await this.reconcileTelegramConnections();
214745
- await this.reconcileDiscordConnections();
214746
- }
215002
+ if (change.integrations) connectionsDirty = true;
214747
215003
  }
214748
215004
  for (const a of toStart) {
214749
215005
  this.agents.set(a.id, a);
214750
215006
  this.prefetchClone(a);
214751
215007
  this.scheduler.sync(a.id, a.crons);
214752
215008
  }
214753
- if (toStart.some((a) => a.integrations.some((i) => i.platform === "slack")) || toStop.length) await this.reconcileSlackConnections();
214754
- if (toStart.some((a) => a.integrations.some((i) => i.platform === "telegram")) || toStop.length) await this.reconcileTelegramConnections();
214755
- if (toStart.some((a) => a.integrations.some((i) => i.platform === "discord")) || toStop.length) await this.reconcileDiscordConnections();
215009
+ if (connectionsDirty) {
215010
+ await this.closeUnusedPlatformConnections();
215011
+ await this.reconcileSlackConnections();
215012
+ await this.reconcileTelegramConnections();
215013
+ await this.reconcileDiscordConnections();
215014
+ }
214756
215015
  }
214757
215016
  /**
214758
215017
  * Fire-and-forget eager clone of a git-repo workspace so the checkout is warm
@@ -214765,6 +215024,93 @@ var Daemon = class Daemon {
214765
215024
  prefetchWorkspace(agent).catch((err) => this.log.warn(`workspace: prefetch clone for "${agent.id}" failed (will retry at first session): ${formatErr(err)}`));
214766
215025
  }
214767
215026
  /**
215027
+ * Close platform clients whose credential key has no reference in the FINAL
215028
+ * active-agent roster, and evict every derived index that points at a removed
215029
+ * or re-keyed integration. Consolidation maps are the reference counts: direct
215030
+ * Slack is keyed by appToken; shared Slack, Telegram and Discord by botToken.
215031
+ *
215032
+ * A captured connection on a live turn is a temporary reference too. Detach
215033
+ * drains its own dispatch leases before reaching this method; the guard also
215034
+ * keeps ordinary concurrent reconcile safe for unrelated in-flight turns.
215035
+ */
215036
+ async closeUnusedPlatformConnections() {
215037
+ const agents = [...this.agents.values()];
215038
+ const direct = consolidate(agents);
215039
+ const shared = consolidateShared(agents);
215040
+ const telegram = consolidateTelegram(agents);
215041
+ const discord = consolidateDiscord(agents);
215042
+ const directByIntegration = /* @__PURE__ */ new Map();
215043
+ for (const group of direct.values()) for (const { integrationId } of group.integrations) directByIntegration.set(integrationId, {
215044
+ appToken: group.appToken,
215045
+ botToken: group.botToken
215046
+ });
215047
+ const sharedByIntegration = /* @__PURE__ */ new Map();
215048
+ for (const group of shared.values()) for (const { integrationId } of group.integrations) sharedByIntegration.set(integrationId, group.botToken);
215049
+ const telegramByIntegration = /* @__PURE__ */ new Map();
215050
+ for (const group of telegram.values()) for (const { integrationId } of group.integrations) telegramByIntegration.set(integrationId, group.botToken);
215051
+ const discordByIntegration = /* @__PURE__ */ new Map();
215052
+ for (const group of discord.values()) for (const { integrationId } of group.integrations) discordByIntegration.set(integrationId, group.botToken);
215053
+ const allDesiredIds = /* @__PURE__ */ new Set([
215054
+ ...directByIntegration.keys(),
215055
+ ...sharedByIntegration.keys(),
215056
+ ...telegramByIntegration.keys(),
215057
+ ...discordByIntegration.keys()
215058
+ ]);
215059
+ const dropIdentity = (integrationId) => {
215060
+ delete this.botUserIds[integrationId];
215061
+ this.channelSnapshots.delete(integrationId);
215062
+ };
215063
+ for (const integrationId of Object.keys(this.botUserIds)) if (!allDesiredIds.has(integrationId)) dropIdentity(integrationId);
215064
+ for (const integrationId of [...this.channelSnapshots.keys()]) if (!allDesiredIds.has(integrationId)) this.channelSnapshots.delete(integrationId);
215065
+ for (const [integrationId, conn] of this.connByIntegration) {
215066
+ const expectedDirect = directByIntegration.get(integrationId);
215067
+ const expectedShared = sharedByIntegration.get(integrationId);
215068
+ if (!(expectedDirect ? conn.appToken === expectedDirect.appToken && conn.botToken === expectedDirect.botToken : expectedShared !== void 0 ? conn.appToken === "" && conn.botToken === expectedShared : false)) {
215069
+ this.connByIntegration.delete(integrationId);
215070
+ dropIdentity(integrationId);
215071
+ }
215072
+ }
215073
+ for (const [integrationId, conn] of this.tgConnByIntegration) if (conn.botToken !== telegramByIntegration.get(integrationId)) {
215074
+ this.tgConnByIntegration.delete(integrationId);
215075
+ dropIdentity(integrationId);
215076
+ }
215077
+ for (const [integrationId, conn] of this.dcConnByIntegration) if (conn.botToken !== discordByIntegration.get(integrationId)) {
215078
+ this.dcConnByIntegration.delete(integrationId);
215079
+ dropIdentity(integrationId);
215080
+ }
215081
+ for (const [appToken, timer] of this.slackRetryTimers) {
215082
+ if (direct.has(appToken)) continue;
215083
+ this.clock.clearTimeout(timer);
215084
+ this.slackRetryTimers.delete(appToken);
215085
+ }
215086
+ const retryRuns = [...this.slackRetryRuns].filter(([appToken, run]) => direct.get(appToken)?.botToken !== run.botToken).map(([, run]) => run.promise);
215087
+ if (retryRuns.length) await Promise.all(retryRuns);
215088
+ for (const conn of [...this.connections]) {
215089
+ if (direct.get(conn.appToken)?.botToken === conn.botToken) continue;
215090
+ await this.waitForConnectionUses(conn);
215091
+ await conn.stop();
215092
+ this.connections = this.connections.filter((candidate) => candidate !== conn);
215093
+ }
215094
+ for (const [botToken, conn] of [...this.sharedSlackConns]) {
215095
+ if (shared.has(botToken)) continue;
215096
+ await this.waitForConnectionUses(conn);
215097
+ await conn.stop();
215098
+ this.sharedSlackConns.delete(botToken);
215099
+ }
215100
+ for (const conn of [...this.telegramConns]) {
215101
+ if (telegram.has(conn.botToken)) continue;
215102
+ await this.waitForConnectionUses(conn);
215103
+ await conn.stop();
215104
+ this.telegramConns = this.telegramConns.filter((candidate) => candidate !== conn);
215105
+ }
215106
+ for (const conn of [...this.discordConns]) {
215107
+ if (discord.has(conn.botToken)) continue;
215108
+ await this.waitForConnectionUses(conn);
215109
+ await conn.stop();
215110
+ this.discordConns = this.discordConns.filter((candidate) => candidate !== conn);
215111
+ }
215112
+ }
215113
+ /**
214768
215114
  * Reconcile the connection-derived Slack state (`botUserIds`, `connByIntegration`,
214769
215115
  * open sockets) against the live `this.agents` set. Routing itself is rebuilt
214770
215116
  * implicitly each message (mergedRules reads this.agents) — this only maintains the
@@ -214777,18 +215123,14 @@ var Daemon = class Daemon {
214777
215123
  * - NEW integration reusing an ALREADY-OPEN appToken → no socket churn; just
214778
215124
  * backfill botUserIds/connByIntegration from the existing conn (mention routing
214779
215125
  * for the new bot would otherwise silently never match).
214780
- * - REMOVED appToken / close-or-replace of a still-shared socket → NOT performed
214781
- * here. Tearing down a socket that other agents or in-flight turns depend on is
214782
- * the one genuinely dangerous op; it is left as a logged deferred-close TODO so
214783
- * a stale socket simply lingers (harmless) rather than risking the daemon.
215126
+ * - REMOVED/re-keyed appToken → closed by closeUnusedPlatformConnections first,
215127
+ * after checking the final roster and captured-turn connection leases.
214784
215128
  */
214785
215129
  async reconcileSlackConnections() {
214786
215130
  const groups = consolidate([...this.agents.values()]);
214787
- new Set([...this.connByIntegration.values()].map((c) => c.appToken));
214788
215131
  for (const group of groups.values()) {
214789
- const existing = [...this.connByIntegration.values()].find((c) => c.appToken === group.appToken);
215132
+ const existing = this.connections.find((c) => c.appToken === group.appToken && c.botToken === group.botToken);
214790
215133
  if (existing) {
214791
- 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`);
214792
215134
  let bound = false;
214793
215135
  for (const { integrationId } of group.integrations) if (this.connByIntegration.get(integrationId) !== existing) {
214794
215136
  this.botUserIds[integrationId] = existing.botUserId;
@@ -214848,12 +215190,6 @@ var Daemon = class Daemon {
214848
215190
  async openSharedSlackConnections(agents) {
214849
215191
  const groups = consolidateShared(agents);
214850
215192
  for (const group of groups.values()) {
214851
- const staleDirect = this.connections.find((c) => c.botToken === group.botToken && c.appToken !== "");
214852
- if (staleDirect) {
214853
- this.log.info(`slack: bot flipped to shared — stopping its direct socket (bot ${staleDirect.botUserId})`);
214854
- await staleDirect.stop().catch(() => {});
214855
- this.connections = this.connections.filter((c) => c !== staleDirect);
214856
- }
214857
215193
  let conn = this.sharedSlackConns.get(group.botToken);
214858
215194
  if (!conn) {
214859
215195
  conn = new SlackConnection({
@@ -214892,7 +215228,7 @@ var Daemon = class Daemon {
214892
215228
  * tgConnByIntegration for every integrationId on that token. A failed start
214893
215229
  * is logged and leaves existing connections intact (never throws out).
214894
215230
  * - integration reusing an ALREADY-OPEN token → bind it onto the live conn.
214895
- * - REMOVED token → NOT torn down here (same deferred-close reasoning as Slack).
215231
+ * - REMOVED token → closed by closeUnusedPlatformConnections before this opener.
214896
215232
  */
214897
215233
  async reconcileTelegramConnections() {
214898
215234
  const groups = consolidateTelegram([...this.agents.values()]);
@@ -214938,8 +215274,7 @@ var Daemon = class Daemon {
214938
215274
  * Parallel to reconcileTelegramConnections (one Gateway per bot token), but
214939
215275
  * mention-routing matches the bot's numeric user id (normalize's `mentionedBots`
214940
215276
  * are Discord user ids). A failed start is logged and leaves other connections
214941
- * intact (never throws out); a removed token is NOT torn down here (same deferred-
214942
- * close reasoning as Slack/Telegram).
215277
+ * intact (never throws out); removed tokens are closed by the shared close phase.
214943
215278
  */
214944
215279
  async reconcileDiscordConnections() {
214945
215280
  const groups = consolidateDiscord([...this.agents.values()]);
@@ -215037,9 +215372,26 @@ var Daemon = class Daemon {
215037
215372
  * at a slow, fixed interval — never gives up, so a temporary network outage
215038
215373
  * self-heals without manual daemon restart.
215039
215374
  */
215040
- async retrySlackConnection(group) {
215375
+ startSlackRetry(appToken) {
215376
+ if (this.slackRetryRuns.has(appToken)) return;
215377
+ const group = consolidate([...this.agents.values()]).get(appToken);
215378
+ if (!group) return;
215379
+ const run = this.retrySlackConnection(appToken).catch((err) => this.log.error(`slack: retry loop error: ${formatErr(err)}`)).finally(() => {
215380
+ if (this.slackRetryRuns.get(appToken)?.promise === run) this.slackRetryRuns.delete(appToken);
215381
+ });
215382
+ this.slackRetryRuns.set(appToken, {
215383
+ botToken: group.botToken,
215384
+ promise: run
215385
+ });
215386
+ }
215387
+ async retrySlackConnection(appToken) {
215041
215388
  if (this.draining) return;
215042
- if ([...this.connByIntegration.values()].some((c) => c.appToken === group.appToken)) {
215389
+ const group = consolidate([...this.agents.values()]).get(appToken);
215390
+ if (!group) {
215391
+ this.slackRetryTimers.delete(appToken);
215392
+ return;
215393
+ }
215394
+ if (this.connections.some((c) => c.appToken === group.appToken && c.botToken === group.botToken)) {
215043
215395
  this.slackRetryTimers.delete(group.appToken);
215044
215396
  return;
215045
215397
  }
@@ -215061,9 +215413,15 @@ var Daemon = class Daemon {
215061
215413
  });
215062
215414
  try {
215063
215415
  await conn.start();
215416
+ const currentGroup = consolidate([...this.agents.values()]).get(appToken);
215417
+ if (this.draining || !currentGroup || currentGroup.botToken !== group.botToken) {
215418
+ await conn.stop().catch(() => {});
215419
+ this.slackRetryTimers.delete(appToken);
215420
+ return;
215421
+ }
215064
215422
  this.log.info(`slack: background retry succeeded — connected as bot user ${conn.botUserId}`);
215065
215423
  this.slackRetryTimers.delete(group.appToken);
215066
- for (const { integrationId } of group.integrations) {
215424
+ for (const { integrationId } of currentGroup.integrations) {
215067
215425
  this.botUserIds[integrationId] = conn.botUserId;
215068
215426
  this.connByIntegration.set(integrationId, conn);
215069
215427
  }
@@ -215071,10 +215429,14 @@ var Daemon = class Daemon {
215071
215429
  this.refreshChannels(conn);
215072
215430
  } catch (err) {
215073
215431
  await conn.stop().catch(() => {});
215432
+ if (this.draining || !consolidate([...this.agents.values()]).has(appToken)) {
215433
+ this.slackRetryTimers.delete(appToken);
215434
+ return;
215435
+ }
215074
215436
  this.log.warn(`slack: background retry failed — scheduling next attempt in 60s: ${formatErr(err)}`);
215075
215437
  const timer = this.clock.setTimeout(() => {
215076
215438
  if (this.draining) return;
215077
- this.retrySlackConnection(group).catch((e) => this.log.error(`slack: retry loop error: ${formatErr(e)}`));
215439
+ this.startSlackRetry(appToken);
215078
215440
  }, 6e4);
215079
215441
  this.slackRetryTimers.set(group.appToken, timer);
215080
215442
  }
@@ -216587,6 +216949,45 @@ var Daemon = class Daemon {
216587
216949
  paused(agentId) {
216588
216950
  return this.agents.get(agentId)?.pause === true;
216589
216951
  }
216952
+ beginActiveDispatch(agentId) {
216953
+ let resolveDone;
216954
+ const done = new Promise((resolve) => resolveDone = resolve);
216955
+ const active = this.activeDispatchesByAgent.get(agentId) ?? /* @__PURE__ */ new Set();
216956
+ active.add(done);
216957
+ this.activeDispatchesByAgent.set(agentId, active);
216958
+ let released = false;
216959
+ return () => {
216960
+ if (released) return;
216961
+ released = true;
216962
+ active.delete(done);
216963
+ if (active.size === 0) this.activeDispatchesByAgent.delete(agentId);
216964
+ resolveDone();
216965
+ };
216966
+ }
216967
+ holdReplyConnection(conn) {
216968
+ if (!conn) return () => {};
216969
+ let resolveDone;
216970
+ const done = new Promise((resolve) => resolveDone = resolve);
216971
+ const uses = this.activeReplyConnectionUses.get(conn) ?? /* @__PURE__ */ new Set();
216972
+ uses.add(done);
216973
+ this.activeReplyConnectionUses.set(conn, uses);
216974
+ let released = false;
216975
+ return () => {
216976
+ if (released) return;
216977
+ released = true;
216978
+ uses.delete(done);
216979
+ if (uses.size === 0) this.activeReplyConnectionUses.delete(conn);
216980
+ resolveDone();
216981
+ };
216982
+ }
216983
+ async waitForConnectionUses(conn) {
216984
+ while (true) {
216985
+ const leases = [...this.activeReplyConnectionUses.get(conn) ?? []];
216986
+ const pending = [...this.pending.values()].filter((turn) => turn.conn === conn).map((turn) => turn.done);
216987
+ if (leases.length === 0 && pending.length === 0) return;
216988
+ await Promise.all([...leases, ...pending]);
216989
+ }
216990
+ }
216590
216991
  async runLoop(key, firstEntry) {
216591
216992
  let entry = firstEntry;
216592
216993
  while (entry) {
@@ -216605,7 +217006,13 @@ var Daemon = class Daemon {
216605
217006
  return;
216606
217007
  }
216607
217008
  try {
216608
- const sessionId = await this.dispatchOne(entry, key);
217009
+ const releaseDispatch = this.beginActiveDispatch(entry.agentId);
217010
+ let sessionId;
217011
+ try {
217012
+ sessionId = await this.dispatchOne(entry, key);
217013
+ } finally {
217014
+ releaseDispatch();
217015
+ }
216609
217016
  this.removeInbox(entry);
216610
217017
  entry.resolve(sessionId);
216611
217018
  } catch (err) {
@@ -216639,18 +217046,23 @@ var Daemon = class Daemon {
216639
217046
  const mode = this.store.getOutputModeOverride(key) ?? agent.output.mode;
216640
217047
  const conv = msg.platform === "telegram" ? new TelegramConverger(mode) : msg.platform === "discord" ? new DiscordConverger(mode) : new OutputConverger(mode);
216641
217048
  this.showActivity(replyConn, msg.channel, statusThread, wasRunning ? "is thinking…" : "is starting up…");
217049
+ const releaseReplyConn = this.holdReplyConnection(replyConn);
216642
217050
  let handled;
216643
217051
  try {
216644
217052
  handled = await this.sessions.handle(agentId, msg);
216645
217053
  } catch (err) {
216646
- this.store.setSessionState(key, "idle", this.clock.now());
216647
- this.surfaceTurnFailure(err, {
216648
- webchat,
216649
- replyConn,
216650
- channel: msg.channel,
216651
- thread: msg.thread,
216652
- statusThread
216653
- });
217054
+ try {
217055
+ this.store.setSessionState(key, "idle", this.clock.now());
217056
+ this.surfaceTurnFailure(err, {
217057
+ webchat,
217058
+ replyConn,
217059
+ channel: msg.channel,
217060
+ thread: msg.thread,
217061
+ statusThread
217062
+ });
217063
+ } finally {
217064
+ releaseReplyConn();
217065
+ }
216654
217066
  throw err;
216655
217067
  }
216656
217068
  const { sessionId, blocks, created } = handled;
@@ -216760,6 +217172,7 @@ var Daemon = class Daemon {
216760
217172
  finalPhase = "problem";
216761
217173
  throw err;
216762
217174
  } finally {
217175
+ releaseReplyConn();
216763
217176
  this.clearIdle(p);
216764
217177
  this.releasePermissions(sessionId);
216765
217178
  this.releaseElicits(sessionId);
@@ -217722,6 +218135,50 @@ var Daemon = class Daemon {
217722
218135
  agentId
217723
218136
  }, this.cfg.limits.shutdownDrainMs);
217724
218137
  await this.stopHost(agentId);
218138
+ while (this.activeDispatchesByAgent.get(agentId)?.size) await Promise.all([...this.activeDispatchesByAgent.get(agentId)]);
218139
+ await this.stopHost(agentId);
218140
+ }
218141
+ /** Await the single-flight reconcile and any coalesced trailing pass. Registry
218142
+ * callbacks fire a background reconcile before lifecycle handlers explicitly
218143
+ * await it, so one plain `await reconcile()` can otherwise observe only the
218144
+ * current pass rather than the queued final-roster pass. */
218145
+ async flushReconcile() {
218146
+ await this.reconcile();
218147
+ await Promise.resolve();
218148
+ while (this.reconcileRun || this.reconcilePending) {
218149
+ if (this.reconcileRun) await this.reconcileRun;
218150
+ else await this.reconcile();
218151
+ await Promise.resolve();
218152
+ }
218153
+ }
218154
+ queueAgentMove(kind, agentId, moveId, work) {
218155
+ const key = `${kind}:${agentId}:${moveId}`;
218156
+ const duplicate = this.agentMoveInFlight.get(key);
218157
+ if (duplicate) return duplicate;
218158
+ const run = (this.agentMoveTails.get(agentId) ?? Promise.resolve()).catch(() => {}).then(work);
218159
+ const tail = run.then(() => void 0, () => void 0);
218160
+ this.agentMoveInFlight.set(key, run);
218161
+ this.agentMoveTails.set(agentId, tail);
218162
+ run.then(() => {
218163
+ if (this.agentMoveInFlight.get(key) === run) this.agentMoveInFlight.delete(key);
218164
+ if (this.agentMoveTails.get(agentId) === tail) this.agentMoveTails.delete(agentId);
218165
+ }, () => {
218166
+ if (this.agentMoveInFlight.get(key) === run) this.agentMoveInFlight.delete(key);
218167
+ if (this.agentMoveTails.get(agentId) === tail) this.agentMoveTails.delete(agentId);
218168
+ });
218169
+ return run;
218170
+ }
218171
+ activationCapabilityError(agent) {
218172
+ const model = agent.runtimeOverrides?.model;
218173
+ const offeredModels = this.runtimeModels.get(agent.runtime) ?? [];
218174
+ if (model && offeredModels.length > 0 && !offeredModels.includes(model)) return `model "${model}" is not offered by runtime "${agent.runtime}"`;
218175
+ const caps = this.runtimeMcpCaps.get(agent.runtime);
218176
+ for (const name of agent.mcpServers) {
218177
+ if (name === "agentconnect") return `MCP server name "${name}" is reserved`;
218178
+ const def = this.mcpServerDefs[name];
218179
+ if (!def) return `MCP server "${name}" is not configured on this daemon`;
218180
+ if (def.transport !== "stdio" && caps && !caps[def.transport]) return `MCP server "${name}" needs unsupported ${def.transport} transport on runtime "${agent.runtime}"`;
218181
+ }
217725
218182
  }
217726
218183
  /** §2.5 SIGTERM / daemon shutdown: gate new turns, then await in-flight turns up
217727
218184
  * to `shutdownDrainMs`, cancelling stragglers. Safe to call repeatedly. */
@@ -217818,12 +218275,12 @@ var Daemon = class Daemon {
217818
218275
  if (ignored.length) this.log.warn(`cp: ignored config keys: ${ignored.join(", ")}`);
217819
218276
  },
217820
218277
  applyReconcileSnapshot: (snap) => {
217821
- this.cpAgents?.converge(snap.agents);
217822
- this.cpIntegrations?.converge(snap.integrations);
218278
+ this.cpAgents?.converge((snap.agents ?? []).filter((agent) => !this.moveStagedAgents.has(agent.agentId)));
218279
+ this.cpIntegrations?.converge((snap.integrations ?? []).filter((integration) => !this.moveStagedAgents.has(integration.agentId)));
217823
218280
  this.convergeRelays(snap.relays);
217824
218281
  this.cpCollab.replace(snap.collabRoutes);
217825
218282
  for (const id of snap.drop.crons) this.cpCrons?.remove(id);
217826
- this.cpCrons?.converge(snap.crons);
218283
+ this.cpCrons?.converge((snap.crons ?? []).filter((cron) => !this.moveStagedAgents.has(cron.agentId)));
217827
218284
  this.cpRouting?.converge({
217828
218285
  routingEpoch: snap.routingEpoch,
217829
218286
  assignments: snap.assignments,
@@ -217835,16 +218292,164 @@ var Daemon = class Daemon {
217835
218292
  if (snap.integrations?.length) this.log.debug(`cp: converged ${snap.integrations.length} integration(s)`);
217836
218293
  },
217837
218294
  applyAgentUpsert: (agentId, spec) => {
218295
+ if (this.moveStagedAgents.has(agentId)) return;
217838
218296
  this.gitCreds?.clearDenied(agentId);
217839
218297
  this.cpAgents?.upsert(agentId, spec);
217840
218298
  },
217841
218299
  applyAgentRemove: (agentId) => {
217842
218300
  this.gitCreds?.remove(agentId);
217843
218301
  this.cpAgents?.remove(agentId);
218302
+ this.moveStageMetadata.delete(agentId);
218303
+ this.moveStagedAgents.delete(agentId);
218304
+ this.drainingAgents.delete(agentId);
218305
+ },
218306
+ applyAgentDetach: (detach) => {
218307
+ const { agentId, moveId } = detach;
218308
+ return this.queueAgentMove("detach", agentId, moveId, async () => {
218309
+ if (this.opts.agentName) return {
218310
+ ok: false,
218311
+ reason: "agent move is unavailable in --agent single-agent mode"
218312
+ };
218313
+ const previous = this.moveStageMetadata.get(agentId);
218314
+ if (previous?.moveId === moveId && previous.state === "committed") return { ok: true };
218315
+ stageAgentMove(this.agentsDir, agentId, moveId);
218316
+ this.moveStageMetadata.set(agentId, {
218317
+ moveId,
218318
+ state: "staging"
218319
+ });
218320
+ this.moveStagedAgents.add(agentId);
218321
+ this.drainingAgents.add(agentId);
218322
+ await this.stopAgent(agentId);
218323
+ const fence = this.moveStageMetadata.get(agentId);
218324
+ if (fence?.moveId !== moveId || fence.state !== "staging") return {
218325
+ ok: false,
218326
+ reason: "agent/detach: move was superseded"
218327
+ };
218328
+ this.gitCreds?.remove(agentId);
218329
+ this.cpAgents?.detach(agentId);
218330
+ await this.flushReconcile();
218331
+ await this.closeUnusedPlatformConnections();
218332
+ const finalFence = this.moveStageMetadata.get(agentId);
218333
+ if (finalFence?.moveId !== moveId || finalFence.state !== "staging") return {
218334
+ ok: false,
218335
+ reason: "agent/detach: move was superseded"
218336
+ };
218337
+ return { ok: true };
218338
+ });
218339
+ },
218340
+ applyAgentActivate: (activate) => {
218341
+ const { agentId } = activate;
218342
+ return this.queueAgentMove("activate", agentId, activate.moveId, async () => {
218343
+ if (this.opts.agentName) return {
218344
+ ok: false,
218345
+ reason: "agent move is unavailable in --agent single-agent mode"
218346
+ };
218347
+ const stage = this.moveStageMetadata.get(agentId);
218348
+ if (stage?.moveId === activate.moveId && stage.state === "committed") return { ok: true };
218349
+ if (stage?.moveId !== activate.moveId || stage.state !== "staging" || !this.moveStagedAgents.has(agentId) || !this.drainingAgents.has(agentId)) return {
218350
+ ok: false,
218351
+ reason: "agent/activate: staging fence is missing or superseded"
218352
+ };
218353
+ const capacityUsed = this.agents.size + this.activatingAgents.size;
218354
+ if (this.cfg.limits.maxAgents > 0 && capacityUsed >= this.cfg.limits.maxAgents) return {
218355
+ ok: false,
218356
+ reason: `agent/activate: daemon capacity ${capacityUsed}/${this.cfg.limits.maxAgents} is full`
218357
+ };
218358
+ this.activatingAgents.add(agentId);
218359
+ try {
218360
+ if (activate.integrations.some((integration) => integration.agentId !== agentId)) return {
218361
+ ok: false,
218362
+ reason: "agent/activate: integration bundle contains a different agentId"
218363
+ };
218364
+ if (activate.crons.some((cron) => cron.agentId !== agentId)) return {
218365
+ ok: false,
218366
+ reason: "agent/activate: cron bundle contains a different agentId"
218367
+ };
218368
+ this.gitCreds?.clearDenied(agentId);
218369
+ this.cpAgents?.upsert(agentId, activate.spec);
218370
+ for (const integration of activate.integrations) this.cpIntegrations?.upsert(integration);
218371
+ for (const cron of activate.crons) this.cpCrons?.upsert(cron);
218372
+ if ((this.cpAgents?.activate(agentId, {
218373
+ integrationIds: activate.integrations.map((integration) => integration.integrationId),
218374
+ cronIds: activate.crons.map((cron) => cron.cronId)
218375
+ }) ?? "missing") === "missing") return {
218376
+ ok: false,
218377
+ reason: `agent/activate: unknown agent ${agentId}`
218378
+ };
218379
+ this.moveStagedAgents.delete(agentId);
218380
+ try {
218381
+ await this.flushReconcile();
218382
+ } catch (err) {
218383
+ this.moveStagedAgents.add(agentId);
218384
+ await this.stopHost(agentId).catch(() => {});
218385
+ await this.flushReconcile().catch(() => {});
218386
+ throw err;
218387
+ }
218388
+ const agent = this.agents.get(agentId);
218389
+ if (!agent) {
218390
+ this.moveStagedAgents.add(agentId);
218391
+ await this.flushReconcile();
218392
+ return {
218393
+ ok: false,
218394
+ reason: `agent/activate: agent ${agentId} did not reconcile`
218395
+ };
218396
+ }
218397
+ if (!this.runtimes[agent.runtime]) {
218398
+ this.moveStagedAgents.add(agentId);
218399
+ await this.flushReconcile();
218400
+ return {
218401
+ ok: false,
218402
+ reason: `agent/activate: runtime "${agent.runtime}" is unavailable`
218403
+ };
218404
+ }
218405
+ const capabilityError = this.activationCapabilityError(agent);
218406
+ if (capabilityError) {
218407
+ this.moveStagedAgents.add(agentId);
218408
+ await this.flushReconcile();
218409
+ return {
218410
+ ok: false,
218411
+ reason: `agent/activate: ${capabilityError}`
218412
+ };
218413
+ }
218414
+ try {
218415
+ await this.ensureHostAsync(agentId);
218416
+ } catch (err) {
218417
+ this.moveStagedAgents.add(agentId);
218418
+ await this.stopHost(agentId).catch(() => {});
218419
+ await this.flushReconcile().catch(() => {});
218420
+ return {
218421
+ ok: false,
218422
+ reason: `agent/activate: ${err.message}`
218423
+ };
218424
+ }
218425
+ try {
218426
+ commitAgentMove(this.agentsDir, agentId, activate.moveId);
218427
+ } catch (err) {
218428
+ this.moveStagedAgents.add(agentId);
218429
+ await this.flushReconcile().catch(() => {});
218430
+ return {
218431
+ ok: false,
218432
+ reason: `agent/activate: failed to commit staging fence: ${err.message}`
218433
+ };
218434
+ }
218435
+ this.moveStageMetadata.set(agentId, {
218436
+ moveId: activate.moveId,
218437
+ state: "committed"
218438
+ });
218439
+ this.drainingAgents.delete(agentId);
218440
+ return { ok: true };
218441
+ } finally {
218442
+ this.activatingAgents.delete(agentId);
218443
+ }
218444
+ });
218445
+ },
218446
+ applyIntegrationUpsert: (spec) => {
218447
+ if (!this.moveStagedAgents.has(spec.agentId)) this.cpIntegrations?.upsert(spec);
217844
218448
  },
217845
- applyIntegrationUpsert: (spec) => this.cpIntegrations?.upsert(spec),
217846
218449
  applyIntegrationRemove: (integrationId) => this.cpIntegrations?.remove(integrationId),
217847
- upsertCron: (cron) => this.cpCrons.upsert(cron),
218450
+ upsertCron: (cron) => {
218451
+ if (!this.moveStagedAgents.has(cron.agentId)) this.cpCrons.upsert(cron);
218452
+ },
217848
218453
  removeCron: (cronId) => this.cpCrons.remove(cronId),
217849
218454
  runCron: (cronId) => this.runCronNow(cronId),
217850
218455
  applyRouteAssign: (a) => this.cpRouting?.upsertAssign(a),
@@ -217852,6 +218457,7 @@ var Daemon = class Daemon {
217852
218457
  applyRelayRoster: (relays) => this.convergeRelays(relays),
217853
218458
  applyCollabRoutes: (snap) => this.cpCollab.replace(snap),
217854
218459
  applyAgentLaunch: async (launch) => {
218460
+ if (this.moveStagedAgents.has(launch.agentId)) throw new Error(`agent/launch: agent ${launch.agentId} is staged for a daemon move`);
217855
218461
  const agent = this.agents.get(launch.agentId);
217856
218462
  if (!agent) throw new Error(`agent/launch: unknown agent ${launch.agentId}`);
217857
218463
  this.drainingAgents.delete(launch.agentId);
@@ -218031,7 +218637,7 @@ var Daemon = class Daemon {
218031
218637
  ],
218032
218638
  runtimes: Object.keys(this.runtimes).map((id) => this.runtimeNames[id] ?? id),
218033
218639
  acp: true,
218034
- features: []
218640
+ features: this.opts.agentName ? [] : ["agent-move-v1"]
218035
218641
  }),
218036
218642
  runtimeProfiles: () => Object.keys(this.runtimes).map((id) => this.runtimeProfileFor(id)),
218037
218643
  mcpServerFacts: () => this.mcpServerFacts,
@@ -218165,7 +218771,9 @@ var Daemon = class Daemon {
218165
218771
  this.scheduler?.stop();
218166
218772
  await Promise.resolve(this.cpClient?.stop()).catch((e) => errors.push(e));
218167
218773
  await Promise.resolve(this.relays?.stop()).catch((e) => errors.push(e));
218774
+ for (const run of [...this.slackRetryRuns.values()]) await Promise.resolve(run.promise).catch((e) => errors.push(e));
218168
218775
  for (const c of this.connections) await Promise.resolve(c.stop()).catch((e) => errors.push(e));
218776
+ for (const c of this.sharedSlackConns.values()) await Promise.resolve(c.stop()).catch((e) => errors.push(e));
218169
218777
  for (const c of this.telegramConns) await Promise.resolve(c.stop()).catch((e) => errors.push(e));
218170
218778
  for (const c of this.discordConns) await Promise.resolve(c.stop()).catch((e) => errors.push(e));
218171
218779
  for (const h of this.hosts.values()) await Promise.resolve(h.stop()).catch((e) => errors.push(e));
@@ -218208,4 +218816,4 @@ async function runForeground(opts, deps = {}) {
218208
218816
  //#endregion
218209
218817
  export { ClientTransport as i, CP_SUBPROTOCOL as n, CP_WS_PATH as r, runForeground as t };
218210
218818
 
218211
- //# sourceMappingURL=run-foreground-D4TMcwTn.js.map
218819
+ //# sourceMappingURL=run-foreground-BMjqIzg4.js.map