@agentconnect.md/daemon 1.0.0-rc.280 → 1.0.0-rc.282

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,9 +3,9 @@ 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-Amic2B8b.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-DzCnR0cj.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-CrjtbS3A.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-C-4zFhF8.js";
9
9
  import { i as GitCredentialCache, n as gitcredSocketPath, r as writeGitcredShim, t as GitCredServer } from "./gitcred-server-CMC80O_1.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";
@@ -4516,6 +4516,9 @@ var CpClient = class {
4516
4516
  case "relay/roster":
4517
4517
  this.deps.configApply.applyRelayRoster(frame.payload.relays);
4518
4518
  return;
4519
+ case "collaboration/routes":
4520
+ this.deps.configApply.applyCollabRoutes(frame.payload);
4521
+ return;
4519
4522
  case "agent/upsert": {
4520
4523
  const u = frame.payload;
4521
4524
  this.deps.configApply.applyAgentUpsert(u.agentId, u.spec);
@@ -197213,6 +197216,18 @@ var RelayClient = class {
197213
197216
  isReady() {
197214
197217
  return this.state === "READY";
197215
197218
  }
197219
+ /**
197220
+ * Send a cross-daemon agent-call to the relay (`rd/agentmsg` REQ, agent-collaboration
197221
+ * §2.3/§6.4) and resolve with the relay's admission verdict. `claimedFromAgentId` is
197222
+ * this daemon's trusted caller — the relay re-validates it against the socket's
197223
+ * authenticated daemonId. Rejects (retryable) if the socket isn't READY.
197224
+ */
197225
+ async sendAgentMsg(payload) {
197226
+ if (this.state !== "READY") throw new WireError("INTERNAL", `rd link not ready (${this.state})`, true);
197227
+ const rep = await this.sendRequest(buildRelayDaemonFrame("rd/agentmsg", payload));
197228
+ if (rep.type !== "rd/agentmsg/ack") throw new WireError("INTERNAL", `expected rd/agentmsg/ack, got ${rep.type}`, false);
197229
+ return rep.payload;
197230
+ }
197216
197231
  async attemptConnect() {
197217
197232
  if (this.stopped || this.fatal) return;
197218
197233
  const daemonId = this.deps.daemonId();
@@ -197264,6 +197279,10 @@ var RelayClient = class {
197264
197279
  this.handleMsg(frame.id, frame.payload);
197265
197280
  return;
197266
197281
  }
197282
+ if (frame.type === "rd/agentmsg/fwd") {
197283
+ this.handleAgentMsgFwd(frame.id, frame.payload);
197284
+ return;
197285
+ }
197267
197286
  this.deps.log.debug(`relay(${this.relayId}): ignoring ${frame.type}`);
197268
197287
  }
197269
197288
  /** An inbound rd/msg: dispatch it, reply `rd/ack`; only a webchat op streams
@@ -197274,7 +197293,22 @@ var RelayClient = class {
197274
197293
  const ack = this.deps.onRelayMsg(msg, chat);
197275
197294
  this.transport?.send(JSON.stringify(buildRelayDaemonFrame("rd/ack", ack, { corr: reqId })));
197276
197295
  }
197277
- /** Send one reply chunk back over THIS relay's socket, stamping the per-chat `seq`. */
197296
+ /** A forwarded cross-daemon agent-call: terminal-verify + dispatch, reply the typed
197297
+ * admission verdict (`rd/agentmsg/ack`, corr = the relay's fwd id). */
197298
+ async handleAgentMsgFwd(reqId, msg) {
197299
+ let ack;
197300
+ try {
197301
+ ack = await this.deps.onRelayAgentMsg(msg);
197302
+ } catch (err) {
197303
+ this.deps.log.error(`relay(${this.relayId}): rd/agentmsg/fwd handler threw: ${err.message}`);
197304
+ ack = {
197305
+ deliveryId: msg.deliveryId,
197306
+ delivered: false,
197307
+ reason: "offline"
197308
+ };
197309
+ }
197310
+ this.transport?.send(JSON.stringify(buildRelayDaemonFrame("rd/agentmsg/ack", ack, { corr: reqId })));
197311
+ }
197278
197312
  sendChat(chatId, event) {
197279
197313
  const seq = (this.seqByChat.get(chatId) ?? 0) + 1;
197280
197314
  this.seqByChat.set(chatId, seq);
@@ -197346,6 +197380,45 @@ var RelayManager = class {
197346
197380
  size() {
197347
197381
  return this.clients.size;
197348
197382
  }
197383
+ /**
197384
+ * Send a cross-daemon agent-call over ANY READY relay (agent-collaboration §2.3/§6.4).
197385
+ * The all-to-all roster means any relay this daemon holds can route the call to the
197386
+ * target's owning daemon via the CP's collaboration snapshot — so we pick the first
197387
+ * READY client. Throws if none is READY (the caller reports the call undeliverable).
197388
+ */
197389
+ async sendAgentMsg(payload) {
197390
+ for (const client of this.clients.values()) if (client.isReady()) return client.sendAgentMsg(payload);
197391
+ throw new Error("no READY relay to route agent-call");
197392
+ }
197393
+ };
197394
+ //#endregion
197395
+ //#region src/cp/cp-collab-routes.ts
197396
+ var CpCollabRoutes = class {
197397
+ generation = -1;
197398
+ channels = /* @__PURE__ */ new Map();
197399
+ key(orgId, platform, channelId) {
197400
+ return orgId + "\0" + platform + "\0" + channelId;
197401
+ }
197402
+ /** FULL-REPLACE from a CP snapshot (converge-don't-diff); ignore an older generation.
197403
+ * Tolerates an absent snapshot (a pre-collab CP / hand-built test fixture) as a no-op. */
197404
+ replace(snap) {
197405
+ if (!snap) return;
197406
+ if (snap.generation < this.generation) return;
197407
+ this.generation = snap.generation;
197408
+ this.channels.clear();
197409
+ for (const ch of snap.channels) {
197410
+ const byAgent = /* @__PURE__ */ new Map();
197411
+ for (const a of ch.agents) byAgent.set(a.agentId, {
197412
+ ...a,
197413
+ orgId: ch.orgId
197414
+ });
197415
+ this.channels.set(this.key(ch.orgId, ch.platform, ch.channelId), byAgent);
197416
+ }
197417
+ }
197418
+ /** The placement of `agentId` in `(orgId,platform,channel)`, or undefined. */
197419
+ resolve(orgId, platform, channelId, agentId) {
197420
+ return this.channels.get(this.key(orgId, platform, channelId))?.get(agentId);
197421
+ }
197349
197422
  };
197350
197423
  //#endregion
197351
197424
  //#region src/slack/permalink.ts
@@ -213861,6 +213934,26 @@ function formatErr(err) {
213861
213934
  return e?.stack ?? String(err);
213862
213935
  }
213863
213936
  const MAX_QUEUED_PER_SESSION = 10;
213937
+ /** Thrown to a `dispatch()` caller when the per-session admission queue is at its depth
213938
+ * cap (§4.4 backpressure): the message is fast-failed, not buffered. Carries a stable
213939
+ * `reason` so an agent-call source (P1/P2) can surface a typed `queue_full`. */
213940
+ var QueueFullError = class extends Error {
213941
+ reason = "queue_full";
213942
+ constructor(sessionKey) {
213943
+ super(`admission queue full for session ${sessionKey}`);
213944
+ this.name = "QueueFullError";
213945
+ }
213946
+ };
213947
+ /** Thrown to the `dispatch()` callers of the messages still queued behind a turn that
213948
+ * FAILED (§6.9 #378 fail-stop): the daemon does not auto-run buffered work onto a broken
213949
+ * session, so each follow-up is rejected rather than silently dropped or force-run. */
213950
+ var FailStopError = class extends Error {
213951
+ reason = "fail_stop";
213952
+ constructor(sessionKey) {
213953
+ super(`turn failed for session ${sessionKey}; queued messages were not auto-run`);
213954
+ this.name = "FailStopError";
213955
+ }
213956
+ };
213864
213957
  const SELECT_KIND_CODE = {
213865
213958
  model: "m",
213866
213959
  effort: "e",
@@ -213897,6 +213990,10 @@ const IDLE_FLUSH_MS = 2e3;
213897
213990
  /** Public Web App console origin used for session deep links when neither a local
213898
213991
  * `webAppUrl` config nor a CP-provided one is set — so "View session" always resolves. */
213899
213992
  const DEFAULT_WEB_APP_URL = "https://app.agentconnect.md";
213993
+ /** Per-in-flight-turn rendering state, keyed by ACP sessionId in `this.pending`. */
213994
+ /** Cap on agent→agent hop depth (design §2.4/§4.5) — reject a `messageAgent` that
213995
+ * would push the outgoing hopCount past this, so an A↔B wake loop can't run away. */
213996
+ const MAX_AGENT_CALL_HOPS = 8;
213900
213997
  /** Build the wire SessionKey (protocol §5) for a pending turn — what `drain/done`
213901
213998
  * reports as released so the CP may reassign it. Uses the real `thread` (absent for
213902
213999
  * a channel-root message), NOT `statusThread` (which falls back to msgId): the CP
@@ -213972,6 +214069,7 @@ var Daemon = class Daemon {
213972
214069
  cpIntegrations;
213973
214070
  botUserIds = {};
213974
214071
  cpRouting;
214072
+ cpCollab = new CpCollabRoutes();
213975
214073
  clock;
213976
214074
  requestExit;
213977
214075
  draining = false;
@@ -214871,8 +214969,10 @@ var Daemon = class Daemon {
214871
214969
  * NO mute) shared by the Slack status-bar Cancel button. No-op if nothing is running. */
214872
214970
  cancelSessionByKey(key) {
214873
214971
  const rec = this.store.getSession(key);
214874
- if (!rec?.acpSessionId || !this.pending.has(rec.acpSessionId)) return;
214875
- this.interruptTurn(rec.agentId, rec.acpSessionId, key, "cancel");
214972
+ if (!this.inflight.has(key)) return;
214973
+ const agentId = rec?.agentId ?? this.serialQueue.get(key)?.[0]?.agentId;
214974
+ if (!agentId) return;
214975
+ this.interruptTurn(agentId, key, "cancel", rec?.acpSessionId ?? void 0);
214876
214976
  }
214877
214977
  /** Switch a session's reasoning effort by its local key — the effort counterpart of
214878
214978
  * {@link setModelByKey}. Records the sticky override and, if the ACP session is warm,
@@ -214944,9 +215044,16 @@ var Daemon = class Daemon {
214944
215044
  * Resolved by conversationId (the op carries no agentId). No-op when idle. */
214945
215045
  handleWebchatCancel(conversationId) {
214946
215046
  for (const [acpSessionId, p] of this.pending) if (p.webchat?.conversationId === conversationId) {
214947
- this.interruptTurn(p.agentId, acpSessionId, p.sessionKey, "cancel");
215047
+ this.interruptTurn(p.agentId, p.sessionKey, "cancel", acpSessionId);
214948
215048
  return;
214949
215049
  }
215050
+ for (const [key, entries] of this.serialQueue) {
215051
+ const hit = entries.find((e) => e.webchat?.conversationId === conversationId);
215052
+ if (hit) {
215053
+ this.interruptTurn(hit.agentId, key, "cancel");
215054
+ return;
215055
+ }
215056
+ }
214950
215057
  this.log.debug(`webchat cancel: no in-flight turn for conversation ${conversationId}`);
214951
215058
  }
214952
215059
  relayMsgAcks = /* @__PURE__ */ new Map();
@@ -214998,6 +215105,74 @@ var Daemon = class Daemon {
214998
215105
  accepted: true
214999
215106
  };
215000
215107
  }
215108
+ relayAgentMsgAcks = /* @__PURE__ */ new Map();
215109
+ /**
215110
+ * TARGET side of a cross-daemon `messageAgent` (agent-collaboration §2.3/§6.2, P2) —
215111
+ * the mirror of {@link handleRelayIm} for a relay-forwarded `rd/agentmsg/fwd`. The
215112
+ * relay already validated the caller and minted a TRUSTED claim
215113
+ * (`trustedFromAgentId` + org/channel assertion), but we TERMINAL-VERIFY it against
215114
+ * our OWN local snapshot/spec (defense in depth, §2.5 #4) before dispatching — never
215115
+ * blindly trusting the relay. The ACK is returned on durable ADMISSION (the P4-gate
215116
+ * enqueue), NOT after the model turn (§6.4).
215117
+ */
215118
+ async handleRelayAgentMsg(msg) {
215119
+ const nak = (reason) => ({
215120
+ deliveryId: msg.deliveryId,
215121
+ delivered: false,
215122
+ ...reason ? { reason } : {}
215123
+ });
215124
+ const dedupKey = `${msg.trustedFromAgentId}:${msg.deliveryId}`;
215125
+ const prior = this.relayAgentMsgAcks.get(dedupKey);
215126
+ if (prior) return prior;
215127
+ const record = (ack) => {
215128
+ if (this.relayAgentMsgAcks.size >= 2e3) this.relayAgentMsgAcks.clear();
215129
+ this.relayAgentMsgAcks.set(dedupKey, ack);
215130
+ return ack;
215131
+ };
215132
+ if (msg.hopCount > MAX_AGENT_CALL_HOPS) return record(nak("hop_limit"));
215133
+ const { platform, channel, thread } = msg.coords;
215134
+ if (!this.agents.get(msg.toAgentId)) return record(nak("not_found"));
215135
+ const target = this.cpCollab.resolve(msg.orgId, platform, channel, msg.toAgentId);
215136
+ const caller = this.cpCollab.resolve(msg.orgId, platform, channel, msg.trustedFromAgentId);
215137
+ if (!target || !caller) {
215138
+ this.log.warn(`relay: rd/agentmsg/fwd terminal-verify failed (no placement) for ${msg.toAgentId} — fail closed`);
215139
+ return record(nak("not_allowed"));
215140
+ }
215141
+ if (target.callPolicy === "selected" && !target.allowedCallerAgentIds.includes(msg.trustedFromAgentId)) {
215142
+ this.log.info(`relay: rd/agentmsg/fwd not_allowed — ${msg.trustedFromAgentId} → ${msg.toAgentId} (selected)`);
215143
+ return record(nak("not_allowed"));
215144
+ }
215145
+ const callMeta = {
215146
+ callFrom: msg.trustedFromAgentId,
215147
+ ...msg.correlationId !== void 0 ? { correlationId: msg.correlationId } : {},
215148
+ hopCount: msg.hopCount,
215149
+ deliveryId: msg.deliveryId
215150
+ };
215151
+ const narrowed = this.narrowPlatform(platform);
215152
+ const resolved = this.resolveCpAgent(msg.toAgentId);
215153
+ const integrationId = msg.integrationId ?? resolved?.integrationId;
215154
+ const normalized = {
215155
+ msgId: `agentcall:${channel}:${msg.deliveryId}`,
215156
+ traceId: msg.deliveryId,
215157
+ source: "agent",
215158
+ platform: narrowed,
215159
+ channel,
215160
+ ...thread !== void 0 ? { thread } : {},
215161
+ sender: {
215162
+ id: msg.trustedFromAgentId,
215163
+ isBot: true
215164
+ },
215165
+ text: `@${msg.trustedFromAgentId} asked:\n${msg.text}`,
215166
+ mentionedBots: resolved?.botUserId ? [resolved.botUserId] : [],
215167
+ isDm: false
215168
+ };
215169
+ this.dispatch(msg.toAgentId, normalized, integrationId, void 0, callMeta).catch((err) => this.log.error(`relay agentmsg dispatch failed for agent "${msg.toAgentId}": ${formatErr(err)}`));
215170
+ this.log.info(`relay: rd/agentmsg/fwd ${msg.trustedFromAgentId} → ${msg.toAgentId} delivery=${msg.deliveryId}`);
215171
+ return record({
215172
+ deliveryId: msg.deliveryId,
215173
+ delivered: true
215174
+ });
215175
+ }
215001
215176
  /**
215002
215177
  * Same-daemon agent→agent delivery — the impl behind the `messageAgent` MCP tool
215003
215178
  * (design §2.2/§2.3, P1). Wakes `toAgentId` by constructing a NormalizedMessage at
@@ -215032,11 +215207,16 @@ var Daemon = class Daemon {
215032
215207
  return result;
215033
215208
  };
215034
215209
  const target = this.agents.get(req.toAgentId);
215035
- if (!target) return record({
215036
- delivered: false,
215037
- targetSession,
215038
- reason: "not_local"
215039
- });
215210
+ if (!target) {
215211
+ const coordPlatform = platform === "hook" ? "slack" : platform;
215212
+ return record(await this.routeAgentMsgCrossDaemon(req, {
215213
+ platform: coordPlatform,
215214
+ channel,
215215
+ thread,
215216
+ deliveryId,
215217
+ targetSession
215218
+ }));
215219
+ }
215040
215220
  if (target.callPolicy === "selected" && !target.allowedCallerAgentIds.includes(req.callerAgentId)) {
215041
215221
  this.log.info(`messageAgent: ${req.callerAgentId} not allowed to call ${req.toAgentId} (policy=selected)`);
215042
215222
  return record({
@@ -215077,6 +215257,53 @@ var Daemon = class Daemon {
215077
215257
  targetSession
215078
215258
  });
215079
215259
  }
215260
+ /**
215261
+ * Route a cross-daemon `messageAgent` over the relay data plane (agent-collaboration
215262
+ * §2.3/§6.2/§6.4, P2) and map the relay's typed admission verdict to a
215263
+ * {@link MessageAgentResult}. The `claimedFromAgentId` is our trusted caller — the
215264
+ * relay re-validates it against our authenticated daemonId (a forged claim is
215265
+ * rejected there). `hopCount` is the source turn's depth (0 in P1's minimal
215266
+ * cross-turn model); the relay increments it (+1, cap 8). The body never touches
215267
+ * the CP — only the relay.
215268
+ */
215269
+ async routeAgentMsgCrossDaemon(req, ctx) {
215270
+ if (!this.relays) return {
215271
+ delivered: false,
215272
+ targetSession: ctx.targetSession,
215273
+ reason: "not_local"
215274
+ };
215275
+ try {
215276
+ const ack = await this.relays.sendAgentMsg({
215277
+ claimedFromAgentId: req.callerAgentId,
215278
+ toAgentId: req.toAgentId,
215279
+ text: req.text,
215280
+ coords: {
215281
+ platform: ctx.platform,
215282
+ channel: ctx.channel,
215283
+ ...ctx.thread !== void 0 ? { thread: ctx.thread } : {}
215284
+ },
215285
+ ...req.correlationId !== void 0 ? { correlationId: req.correlationId } : {},
215286
+ hopCount: 0,
215287
+ deliveryId: ctx.deliveryId
215288
+ });
215289
+ if (ack.delivered) return {
215290
+ delivered: true,
215291
+ targetSession: ctx.targetSession
215292
+ };
215293
+ return {
215294
+ delivered: false,
215295
+ targetSession: ctx.targetSession,
215296
+ ...ack.reason ? { reason: ack.reason } : {}
215297
+ };
215298
+ } catch (err) {
215299
+ this.log.warn(`messageAgent: cross-daemon route failed for ${req.toAgentId}: ${formatErr(err)}`);
215300
+ return {
215301
+ delivered: false,
215302
+ targetSession: ctx.targetSession,
215303
+ reason: "offline"
215304
+ };
215305
+ }
215306
+ }
215080
215307
  /** Narrow the trusted session-context platform string to the NormalizedMessage union;
215081
215308
  * falls back to 'slack' for an unrecognized value (coords still resolve — the union is
215082
215309
  * a routing/key detail, not the trust basis). */
@@ -215308,7 +215535,8 @@ var Daemon = class Daemon {
215308
215535
  };
215309
215536
  });
215310
215537
  }
215311
- queued = /* @__PURE__ */ new Map();
215538
+ inflight = /* @__PURE__ */ new Set();
215539
+ serialQueue = /* @__PURE__ */ new Map();
215312
215540
  /**
215313
215541
  * Resolve a command's target from the channel's latest session when the routing ladder
215314
215542
  * couldn't (no mention entity / thread / dm rule matched — e.g. a group `/status@bot`).
@@ -215357,7 +215585,7 @@ var Daemon = class Daemon {
215357
215585
  }
215358
215586
  }
215359
215587
  const acpSessionId = rec?.acpSessionId;
215360
- const inflight = !!(acpSessionId && this.pending.has(acpSessionId));
215588
+ const inflight = this.inflight.has(key);
215361
215589
  const tgReplyTo = this.telegramReplyTarget(msg);
215362
215590
  const reply = (text) => {
215363
215591
  if (!conn) return;
@@ -215371,7 +215599,7 @@ var Daemon = class Daemon {
215371
215599
  reply(rec ? `🔇 Nothing is running. ${muteNote}` : "Nothing is running to stop.");
215372
215600
  return;
215373
215601
  }
215374
- this.interruptTurn(target.agentId, acpSessionId, key, "stop");
215602
+ this.interruptTurn(target.agentId, key, "stop", acpSessionId ?? void 0);
215375
215603
  reply(`🛑 Stopped. ${muteNote}`);
215376
215604
  return;
215377
215605
  }
@@ -215380,7 +215608,7 @@ var Daemon = class Daemon {
215380
215608
  reply("Nothing is running to cancel.");
215381
215609
  return;
215382
215610
  }
215383
- this.interruptTurn(target.agentId, acpSessionId, key, "cancel");
215611
+ this.interruptTurn(target.agentId, key, "cancel", acpSessionId ?? void 0);
215384
215612
  reply("🛑 Cancelled.");
215385
215613
  return;
215386
215614
  }
@@ -215446,21 +215674,23 @@ var Daemon = class Daemon {
215446
215674
  text: command.text,
215447
215675
  thread
215448
215676
  };
215449
- if (!inflight) {
215450
- this.log.info(`command: queue → agent "${target.agentId}" idle, dispatching now`);
215451
- this.dispatch(target.agentId, payload, target.integrationId).catch((err) => this.log.error(`queued dispatch failed for agent "${target.agentId}": ${err.stack ?? err}`));
215452
- return;
215453
- }
215454
- const q = this.queued.get(acpSessionId) ?? [];
215455
- if (q.length >= MAX_QUEUED_PER_SESSION) {
215456
- this.log.warn(`command: queue → agent "${target.agentId}" session ${acpSessionId} full (${q.length}), rejected`);
215677
+ if (inflight && (this.serialQueue.get(key)?.length ?? 0) >= MAX_QUEUED_PER_SESSION) {
215678
+ this.log.warn(`command: queue → agent "${target.agentId}" session ${key} full, rejected`);
215457
215679
  reply(`Queue is full (${MAX_QUEUED_PER_SESSION} pending) — wait for the current turn to finish.`);
215458
215680
  return;
215459
215681
  }
215460
- q.push(payload);
215461
- this.queued.set(acpSessionId, q);
215462
- this.log.info(`command: queue → agent "${target.agentId}" session ${acpSessionId} (depth ${q.length})`);
215463
- reply(`📥 Queued (#${q.length}) — will run when the current turn finishes.`);
215682
+ this.dispatch(target.agentId, payload, target.integrationId, void 0, void 0, { isQueueCmd: true }).catch((err) => {
215683
+ if (err instanceof QueueFullError) return;
215684
+ this.log.error(`queued dispatch failed for agent "${target.agentId}": ${err.stack ?? err}`);
215685
+ });
215686
+ if (!inflight) {
215687
+ this.log.info(`command: queue → agent "${target.agentId}" idle, dispatching now`);
215688
+ reply(`▶️ Running now — the session was idle.`);
215689
+ } else {
215690
+ const depth = this.serialQueue.get(key)?.length ?? 0;
215691
+ this.log.info(`command: queue → agent "${target.agentId}" session ${key} (depth ${depth})`);
215692
+ reply(`📥 Queued (#${depth}) — will run when the current turn finishes.`);
215693
+ }
215464
215694
  }
215465
215695
  selectLabel(kind) {
215466
215696
  return kind === "model" ? "Model" : kind === "effort" ? "Reasoning effort" : "Permission mode";
@@ -215591,15 +215821,6 @@ var Daemon = class Daemon {
215591
215821
  acpSessionId: latest.acpSessionId ?? void 0
215592
215822
  };
215593
215823
  }
215594
- /** Drain one buffered message for a session whose turn just finished (FIFO). */
215595
- flushQueued(agentId, sessionId, integrationId) {
215596
- const q = this.queued.get(sessionId);
215597
- if (!q || q.length === 0) return;
215598
- const next = q.shift();
215599
- if (q.length === 0) this.queued.delete(sessionId);
215600
- this.log.info(`queue: dispatching buffered message to agent "${agentId}" session ${sessionId} (${q.length} left)`);
215601
- this.dispatch(agentId, next, integrationId).catch((err) => this.log.error(`queued dispatch failed for agent "${agentId}": ${err.stack ?? err}`));
215602
- }
215603
215824
  /** Local layer (agent.json) ∪ resolved CP layer; unservable CP rules are dropped + warn-logged. */
215604
215825
  mergedRules() {
215605
215826
  const local = [...this.agents.values()].flatMap((a) => rulesFromAgent(a, this.botUserIds));
@@ -215646,21 +215867,116 @@ var Daemon = class Daemon {
215646
215867
  ctx.replyConn.postMessage(ctx.channel, `⚠️ Agent failed to respond: ${reason}`, ctx.thread);
215647
215868
  }
215648
215869
  }
215649
- async dispatch(agentId, msg, integrationId, webchat, callMeta) {
215650
- if (this.draining || this.drainingAgents.has(agentId)) {
215651
- this.log.debug(`dispatch: skipped for agent "${agentId}" (draining)`);
215652
- return null;
215870
+ /**
215871
+ * Terminate a QUEUED entry's webchat sink when it is gate-dropped/rejected without ever
215872
+ * entering dispatchOne (drain/pause gate-drop, fail-stop of the queued rest, `!cancel`/`!stop`
215873
+ * queue drop, shutdown). A webchat turn signals its client via the WebchatSink terminal
215874
+ * `done` frame, NOT the dispatch() promise — so settling the promise alone leaves the browser
215875
+ * UI spinning forever. Mirrors surfaceTurnFailure's webchat branch. No-op for non-webchat
215876
+ * entries. Pass an `error` for reject/cancel/shutdown; omit it for a clean gate-drop.
215877
+ */
215878
+ terminateQueuedSink(entry, error) {
215879
+ if (!entry.webchat) return;
215880
+ entry.webchat.sink.done({
215881
+ conversationId: entry.webchat.conversationId,
215882
+ turnId: entry.webchat.turnId,
215883
+ ...error ? { error } : {}
215884
+ });
215885
+ }
215886
+ dispatch(agentId, msg, integrationId, webchat, callMeta, opts) {
215887
+ return new Promise((resolve, reject) => {
215888
+ if (this.draining || this.drainingAgents.has(agentId)) {
215889
+ this.log.debug(`dispatch: skipped for agent "${agentId}" (draining)`);
215890
+ resolve(null);
215891
+ return;
215892
+ }
215893
+ if (this.agents.get(agentId).pause) {
215894
+ this.log.info(`dispatch: skipped for agent "${agentId}" (paused)`);
215895
+ resolve(null);
215896
+ return;
215897
+ }
215898
+ const key = sessionKey(msg.platform, msg.channel, msg.thread ?? msg.msgId, agentId);
215899
+ const entry = {
215900
+ agentId,
215901
+ msg,
215902
+ ...integrationId !== void 0 ? { integrationId } : {},
215903
+ ...webchat ? { webchat } : {},
215904
+ ...callMeta ? { callMeta } : {},
215905
+ ...opts?.isQueueCmd ? { isQueueCmd: true } : {},
215906
+ resolve,
215907
+ reject
215908
+ };
215909
+ if (this.inflight.has(key)) {
215910
+ const q = this.serialQueue.get(key) ?? [];
215911
+ if (q.length >= MAX_QUEUED_PER_SESSION) {
215912
+ this.log.warn(`dispatch: queue full for session ${key} (${q.length}) — rejecting (queue_full)`);
215913
+ reject(new QueueFullError(key));
215914
+ return;
215915
+ }
215916
+ q.push(entry);
215917
+ this.serialQueue.set(key, q);
215918
+ this.log.debug(`dispatch: queued behind in-flight turn for session ${key} (depth ${q.length})`);
215919
+ return;
215920
+ }
215921
+ this.inflight.add(key);
215922
+ this.runLoop(key, entry);
215923
+ });
215924
+ }
215925
+ /**
215926
+ * Own a sessionKey's serial gate: run its head turn to completion, settle that entry's
215927
+ * own promise, then drain the queue in FIFO order WITHOUT releasing ownership between
215928
+ * turns (§6.9 #390 — deleting `inflight` then dequeuing would reopen the FIFO race).
215929
+ * Ownership is released only once the queue is confirmed empty in the same synchronous
215930
+ * tick. Fail-stop is the default (§6.9 #378): if a turn throws, the rest of the queue is
215931
+ * NOT auto-run — each remaining entry is rejected with a notice and the queue cleared,
215932
+ * so buffered work never chains onto a broken session.
215933
+ */
215934
+ paused(agentId) {
215935
+ return this.agents.get(agentId)?.pause === true;
215936
+ }
215937
+ async runLoop(key, firstEntry) {
215938
+ let entry = firstEntry;
215939
+ while (entry) {
215940
+ if (entry !== firstEntry && (this.draining || this.drainingAgents.has(entry.agentId) || this.paused(entry.agentId))) {
215941
+ this.terminateQueuedSink(entry);
215942
+ entry.resolve(null);
215943
+ const rest = this.serialQueue.get(key) ?? [];
215944
+ this.serialQueue.delete(key);
215945
+ for (const e of rest) {
215946
+ this.terminateQueuedSink(e);
215947
+ e.resolve(null);
215948
+ }
215949
+ this.inflight.delete(key);
215950
+ return;
215951
+ }
215952
+ try {
215953
+ const sessionId = await this.dispatchOne(entry, key);
215954
+ entry.resolve(sessionId);
215955
+ } catch (err) {
215956
+ entry.reject(err);
215957
+ const rest = this.serialQueue.get(key) ?? [];
215958
+ this.serialQueue.delete(key);
215959
+ for (const e of rest) {
215960
+ const failStop = new FailStopError(key);
215961
+ this.terminateQueuedSink(e, failStop.message);
215962
+ e.reject(failStop);
215963
+ }
215964
+ this.inflight.delete(key);
215965
+ return;
215966
+ }
215967
+ const q = this.serialQueue.get(key);
215968
+ entry = q?.shift();
215969
+ if (q && q.length === 0) this.serialQueue.delete(key);
215653
215970
  }
215971
+ this.inflight.delete(key);
215972
+ }
215973
+ async dispatchOne(entry, key) {
215974
+ const { agentId, msg, integrationId, webchat, callMeta } = entry;
215654
215975
  const agent = this.agents.get(agentId);
215655
- if (agent.pause) {
215656
- this.log.info(`dispatch: skipped for agent "${agentId}" (paused)`);
215657
- return null;
215658
- }
215659
215976
  const rec = new TranscriptRecorder();
215660
215977
  const replyConn = msg.headless || webchat ? void 0 : this.replyConnFor(agentId, integrationId);
215661
215978
  const wasRunning = this.hostStarts.has(agentId);
215662
215979
  const statusThread = msg.thread ?? msg.msgId;
215663
- const key = sessionKey(msg.platform, msg.channel, statusThread, agentId);
215664
215980
  const mode = this.store.getOutputModeOverride(key) ?? agent.output.mode;
215665
215981
  const conv = msg.platform === "telegram" ? new TelegramConverger(mode) : msg.platform === "discord" ? new DiscordConverger(mode) : new OutputConverger(mode);
215666
215982
  this.showActivity(replyConn, msg.channel, statusThread, wasRunning ? "is thinking…" : "is starting up…");
@@ -215799,18 +216115,28 @@ var Daemon = class Daemon {
215799
216115
  this.clearCancelBackstop(sessionId);
215800
216116
  p.resolveDone();
215801
216117
  }
215802
- this.flushQueued(agentId, sessionId, integrationId);
215803
216118
  return sessionId;
215804
216119
  }
215805
- /** Interrupt an in-flight turn (shared by `!stop`, `!cancel`, and webchat cancel):
215806
- * drop anything queued behind it, mark the session `cancelling`, send ACP
215807
- * `session/cancel`, and arm the force backstop. Muting is the CALLER's concern —
215808
- * `!stop` mutes, `!cancel`/webchat-cancel do not. `reason` is just for the log line. */
215809
- interruptTurn(agentId, acpSessionId, key, reason) {
215810
- this.queued.delete(acpSessionId);
216120
+ /** Interrupt a session by its LOGICAL sessionKey (shared by `!stop`, `!cancel`, and
216121
+ * webchat cancel): reject+drop anything queued behind it in the serial gate, then — if
216122
+ * a turn is actually in flight (ACP session live) — release any unanswered cards, mark
216123
+ * it `cancelling`, send ACP `session/cancel`, and arm the force backstop. Works for a
216124
+ * queued-but-not-yet-started or cold session too (§6.9 #390): draining the queue is
216125
+ * enough, no ACP id required. Muting is the CALLER's concern — `!stop` mutes,
216126
+ * `!cancel`/webchat-cancel do not. */
216127
+ interruptTurn(agentId, key, reason, acpSessionId) {
216128
+ const queued = this.serialQueue.get(key);
216129
+ if (queued && queued.length > 0) {
216130
+ this.serialQueue.delete(key);
216131
+ for (const e of queued) {
216132
+ this.terminateQueuedSink(e, reason);
216133
+ e.reject(new FailStopError(key));
216134
+ }
216135
+ }
216136
+ this.log.info(`command: ${reason} → agent "${agentId}" session ${key}${acpSessionId ? ` (${acpSessionId})` : ""}`);
216137
+ if (!acpSessionId || !this.pending.has(acpSessionId)) return;
215811
216138
  this.releasePermissions(acpSessionId);
215812
216139
  this.releaseElicits(acpSessionId);
215813
- this.log.info(`command: ${reason} → agent "${agentId}" session ${acpSessionId}`);
215814
216140
  this.store.setSessionState(key, "cancelling", this.clock.now());
215815
216141
  this.hosts.get(agentId)?.cancel(acpSessionId).catch((err) => this.log.error(`command ${reason}: cancel failed: ${err.message}`));
215816
216142
  this.armCancelBackstop(agentId, acpSessionId, key);
@@ -216723,13 +217049,24 @@ var Daemon = class Daemon {
216723
217049
  async drainForShutdown() {
216724
217050
  this.draining = true;
216725
217051
  const live = [...this.pending.values()];
216726
- if (live.length === 0) return;
216727
- const deadlineMs = this.cfg.limits.shutdownDrainMs;
216728
- this.log.info(`shutdown: draining ${live.length} in-flight turn(s) (deadline ${Math.round(deadlineMs / 1e3)}s)`);
216729
- if (await this.raceDeadline(Promise.all(live.map((p) => p.done)), deadlineMs) === "timeout") {
216730
- this.log.warn(`shutdown: deadline hit with ${this.pending.size} turn(s) still in flight — cancelling`);
216731
- for (const [sid, p] of this.pending) await this.hosts.get(p.agentId)?.cancel(sid).catch(() => {});
217052
+ if (live.length > 0) {
217053
+ const deadlineMs = this.cfg.limits.shutdownDrainMs;
217054
+ this.log.info(`shutdown: draining ${live.length} in-flight turn(s) (deadline ${Math.round(deadlineMs / 1e3)}s)`);
217055
+ if (await this.raceDeadline(Promise.all(live.map((p) => p.done)), deadlineMs) === "timeout") {
217056
+ this.log.warn(`shutdown: deadline hit with ${this.pending.size} turn(s) still in flight — cancelling`);
217057
+ for (const [sid, p] of this.pending) await this.hosts.get(p.agentId)?.cancel(sid).catch(() => {});
217058
+ }
217059
+ }
217060
+ this.settleQueuedForShutdown();
217061
+ }
217062
+ /** Reject and clear every entry still in the serial gate's queues (shutdown/teardown),
217063
+ * so no admitted-but-unrun message leaves an unsettled dispatch() promise. */
217064
+ settleQueuedForShutdown() {
217065
+ for (const [key, entries] of this.serialQueue) for (const e of entries) {
217066
+ this.terminateQueuedSink(e, "daemon shutting down");
217067
+ e.reject(new FailStopError(key));
216732
217068
  }
217069
+ this.serialQueue.clear();
216733
217070
  }
216734
217071
  /** daemon/restart + daemon/upgrade (§8.3): ack now, then drain + stop + exit so
216735
217072
  * the supervisor relaunches (the new binary, for upgrade). */
@@ -216762,6 +217099,7 @@ var Daemon = class Daemon {
216762
217099
  this.cpAgents?.converge(snap.agents);
216763
217100
  this.cpIntegrations?.converge(snap.integrations);
216764
217101
  this.convergeRelays(snap.relays);
217102
+ this.cpCollab.replace(snap.collabRoutes);
216765
217103
  for (const id of snap.drop.crons) this.cpCrons?.remove(id);
216766
217104
  this.cpCrons?.converge(snap.crons);
216767
217105
  this.cpRouting?.converge({
@@ -216790,6 +217128,7 @@ var Daemon = class Daemon {
216790
217128
  applyRouteAssign: (a) => this.cpRouting?.upsertAssign(a),
216791
217129
  applyRouteUpdate: (u) => this.cpRouting?.applyUpdate(u),
216792
217130
  applyRelayRoster: (relays) => this.convergeRelays(relays),
217131
+ applyCollabRoutes: (snap) => this.cpCollab.replace(snap),
216793
217132
  applyAgentLaunch: async (launch) => {
216794
217133
  const agent = this.agents.get(launch.agentId);
216795
217134
  if (!agent) throw new Error(`agent/launch: unknown agent ${launch.agentId}`);
@@ -216926,7 +217265,8 @@ var Daemon = class Daemon {
216926
217265
  path: RELAY_DAEMON_WS_PATH
216927
217266
  }),
216928
217267
  log: this.log,
216929
- onRelayMsg: (msg, chat) => this.handleRelayMsg(msg, chat)
217268
+ onRelayMsg: (msg, chat) => this.handleRelayMsg(msg, chat),
217269
+ onRelayAgentMsg: (msg) => this.handleRelayAgentMsg(msg)
216930
217270
  });
216931
217271
  if (this.cfg.relays.length) this.relays.converge(this.cfg.relays);
216932
217272
  this.cpClient = new CpClient({
@@ -217133,4 +217473,4 @@ async function runForeground(opts, deps = {}) {
217133
217473
  //#endregion
217134
217474
  export { ClientTransport as i, CP_SUBPROTOCOL as n, CP_WS_PATH as r, runForeground as t };
217135
217475
 
217136
- //# sourceMappingURL=run-foreground-1y1IF3Dc.js.map
217476
+ //# sourceMappingURL=run-foreground-BV2scayf.js.map