@agentconnect.md/daemon 1.0.0-rc.123 → 1.0.0-rc.124

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12855,7 +12855,11 @@ const AuthOk = object({
12855
12855
  * agent/*, and event/session. Its canonical string form is
12856
12856
  * `${platform}:${channel}:${thread ?? "-"}`.
12857
12857
  */
12858
- const Platform = _enum(["slack", "telegram"]);
12858
+ const Platform = _enum([
12859
+ "slack",
12860
+ "telegram",
12861
+ "webchat"
12862
+ ]);
12859
12863
  const SessionKey = object({
12860
12864
  platform: Platform,
12861
12865
  channel: string(),
@@ -13509,6 +13513,99 @@ const FactsRuntimeProfile = object({
13509
13513
  acpProtocolVersion: number().int().optional(),
13510
13514
  toolCalling: boolean()
13511
13515
  });
13516
+ /**
13517
+ * Full snapshot of the daemon's installed runtimes — D→C EVT (protocol §7.3a),
13518
+ * emitted on each register and again once the background probe sweep completes
13519
+ * (with the learned `models[]`). REPLACE semantics: the CP reconciles its
13520
+ * stored runtime list for the daemon to exactly `runtimes[]`, pruning runtimes
13521
+ * that are no longer installed. Idempotent (latest-wins). Supersedes the
13522
+ * per-runtime `facts/runtime-profile` upsert.
13523
+ */
13524
+ const DaemonRuntimes = object({ runtimes: array(FactsRuntimeProfile) });
13525
+ /** Non-secret config update — C→D EVT, protocol §8.3 / frame #26. */
13526
+ const ConfigPush = object({ keys: record(string(), unknown()) });
13527
+ /** Fleet: drain+exit, supervisor restarts — C→D REQ, protocol §8.3 / frame #27. */
13528
+ const DaemonRestart = object({
13529
+ reason: string(),
13530
+ drainFirst: boolean().default(true)
13531
+ });
13532
+ /** Fleet: drain+exit for version bump — C→D REQ, protocol §8.3 / frame #28. */
13533
+ const DaemonUpgrade = object({
13534
+ targetVersion: string(),
13535
+ drainFirst: boolean().default(true)
13536
+ });
13537
+ /** Reply to a fleet restart/upgrade REQ (protocol §8.3). */
13538
+ const DaemonControlAck = object({
13539
+ accepted: boolean(),
13540
+ willDrainUntil: string().datetime().optional()
13541
+ });
13542
+ /**
13543
+ * Generic acknowledgement for REQs whose reply is just `ack` in the frame index
13544
+ * (cron/upsert, cron/remove, agent/stop). Kept permissive but typed.
13545
+ */
13546
+ const Ack = object({
13547
+ ok: boolean(),
13548
+ reason: string().optional()
13549
+ });
13550
+ const ErrorFrame = object({
13551
+ code: _enum([
13552
+ "UNKNOWN_FRAME",
13553
+ "FRAME_TOO_LARGE",
13554
+ "PROTOCOL_STATE",
13555
+ "BAD_PAYLOAD",
13556
+ "AUTH_FAILED",
13557
+ "ATTESTATION_INVALID",
13558
+ "STALE_EPOCH",
13559
+ "STALE_LAUNCH",
13560
+ "NO_SESSION",
13561
+ "SCOPE_DENIED",
13562
+ "LEASE_EXPIRED",
13563
+ "LEASE_DENIED",
13564
+ "RATE_LIMITED",
13565
+ "INTERNAL"
13566
+ ]),
13567
+ message: string(),
13568
+ retryable: boolean(),
13569
+ details: record(string(), unknown()).optional()
13570
+ });
13571
+ //#endregion
13572
+ //#region ../protocol/dist/frames/webchat.js
13573
+ const WebchatMessage = object({
13574
+ conversationId: string().uuid(),
13575
+ agentId: string().uuid(),
13576
+ text: string(),
13577
+ user: string().optional()
13578
+ });
13579
+ const WebchatAck = object({
13580
+ accepted: boolean(),
13581
+ turnId: string().uuid(),
13582
+ reason: _enum([
13583
+ "queued",
13584
+ "no_agent",
13585
+ "busy"
13586
+ ]).optional()
13587
+ });
13588
+ const WebchatEvent = discriminatedUnion("kind", [
13589
+ object({
13590
+ kind: literal("message"),
13591
+ text: string()
13592
+ }),
13593
+ object({
13594
+ kind: literal("thinking"),
13595
+ text: string()
13596
+ }),
13597
+ object({
13598
+ kind: literal("tool_call"),
13599
+ toolCallId: string(),
13600
+ title: string(),
13601
+ status: string()
13602
+ }),
13603
+ object({
13604
+ kind: literal("tool_update"),
13605
+ toolCallId: string(),
13606
+ status: string()
13607
+ })
13608
+ ]);
13512
13609
  //#endregion
13513
13610
  //#region ../protocol/dist/frame.js
13514
13611
  /**
@@ -13535,6 +13632,24 @@ const FRAME_SCHEMAS = {
13535
13632
  "agent/remove": AgentRemove,
13536
13633
  "agent/activity": AgentActivity,
13537
13634
  "agent/scope-denied": AgentScopeDenied,
13635
+ "webchat/message": WebchatMessage,
13636
+ "webchat/ack": WebchatAck,
13637
+ "webchat/output": object({
13638
+ conversationId: string().uuid(),
13639
+ turnId: string().uuid(),
13640
+ index: number().int(),
13641
+ event: WebchatEvent
13642
+ }),
13643
+ "webchat/done": object({
13644
+ conversationId: string().uuid(),
13645
+ turnId: string().uuid(),
13646
+ stopReason: string().optional(),
13647
+ usage: object({
13648
+ used: number().int().optional(),
13649
+ cost: number().optional()
13650
+ }).optional()
13651
+ }),
13652
+ "webchat/close": object({ conversationId: string().uuid() }),
13538
13653
  "route/assign": RouteAssign,
13539
13654
  "route/assign/ack": RouteAssignAck,
13540
13655
  "route/update": RouteUpdate,
@@ -13556,7 +13671,7 @@ const FRAME_SCHEMAS = {
13556
13671
  "event/session": EventSession,
13557
13672
  "usage/report": UsageReport,
13558
13673
  "facts/runtime-profile": FactsRuntimeProfile,
13559
- "facts/daemon-runtimes": object({ runtimes: array(FactsRuntimeProfile) }),
13674
+ "facts/daemon-runtimes": DaemonRuntimes,
13560
13675
  "session/list": SessionListReq,
13561
13676
  "session/list/page": SessionListPage,
13562
13677
  "session/history": SessionHistoryReq,
@@ -13571,44 +13686,12 @@ const FRAME_SCHEMAS = {
13571
13686
  "workspace/gitstatus/result": WorkspaceGitStatus,
13572
13687
  "workspace/gitpull": WorkspaceGitPullReq,
13573
13688
  "workspace/gitpull/result": WorkspaceGitPullResult,
13574
- "config/push": object({ keys: record(string(), unknown()) }),
13575
- "daemon/restart": object({
13576
- reason: string(),
13577
- drainFirst: boolean().default(true)
13578
- }),
13579
- "daemon/upgrade": object({
13580
- targetVersion: string(),
13581
- drainFirst: boolean().default(true)
13582
- }),
13583
- "daemon/control/ack": object({
13584
- accepted: boolean(),
13585
- willDrainUntil: string().datetime().optional()
13586
- }),
13587
- ack: object({
13588
- ok: boolean(),
13589
- reason: string().optional()
13590
- }),
13591
- error: object({
13592
- code: _enum([
13593
- "UNKNOWN_FRAME",
13594
- "FRAME_TOO_LARGE",
13595
- "PROTOCOL_STATE",
13596
- "BAD_PAYLOAD",
13597
- "AUTH_FAILED",
13598
- "ATTESTATION_INVALID",
13599
- "STALE_EPOCH",
13600
- "STALE_LAUNCH",
13601
- "NO_SESSION",
13602
- "SCOPE_DENIED",
13603
- "LEASE_EXPIRED",
13604
- "LEASE_DENIED",
13605
- "RATE_LIMITED",
13606
- "INTERNAL"
13607
- ]),
13608
- message: string(),
13609
- retryable: boolean(),
13610
- details: record(string(), unknown()).optional()
13611
- })
13689
+ "config/push": ConfigPush,
13690
+ "daemon/restart": DaemonRestart,
13691
+ "daemon/upgrade": DaemonUpgrade,
13692
+ "daemon/control/ack": DaemonControlAck,
13693
+ ack: Ack,
13694
+ error: ErrorFrame
13612
13695
  };
13613
13696
  Object.keys(FRAME_SCHEMAS);
13614
13697
  /**
@@ -13638,6 +13721,11 @@ discriminatedUnion("type", [
13638
13721
  frame("agent/remove", FRAME_SCHEMAS["agent/remove"]),
13639
13722
  frame("agent/activity", FRAME_SCHEMAS["agent/activity"]),
13640
13723
  frame("agent/scope-denied", FRAME_SCHEMAS["agent/scope-denied"]),
13724
+ frame("webchat/message", FRAME_SCHEMAS["webchat/message"]),
13725
+ frame("webchat/ack", FRAME_SCHEMAS["webchat/ack"]),
13726
+ frame("webchat/output", FRAME_SCHEMAS["webchat/output"]),
13727
+ frame("webchat/done", FRAME_SCHEMAS["webchat/done"]),
13728
+ frame("webchat/close", FRAME_SCHEMAS["webchat/close"]),
13641
13729
  frame("route/assign", FRAME_SCHEMAS["route/assign"]),
13642
13730
  frame("route/assign/ack", FRAME_SCHEMAS["route/assign/ack"]),
13643
13731
  frame("route/update", FRAME_SCHEMAS["route/update"]),
@@ -24997,8 +25085,18 @@ const pickRule = (r, via) => ({
24997
25085
  * (2) thread affinity (highest after explicit @; bypasses the kind filter, gated on
24998
25086
  * reachable-bot count) → (3) CP per-sessionKey override → (4/5) kind precedence
24999
25087
  * mention > dm > keyword > auto within the chosen layer.
25088
+ *
25089
+ * `explicitAgentId` short-circuits the whole ladder: a webchat turn names its target
25090
+ * agent (`webchat/message.agentId`), so there is nothing to arbitrate — it routes
25091
+ * directly to that agent (integrationId '' — webchat's data plane is the control WS,
25092
+ * not a platform integration). Null if that agentId isn't a servable rule here.
25000
25093
  */
25001
- function routeRules(msg, rules, threadOwner) {
25094
+ function routeRules(msg, rules, threadOwner, explicitAgentId) {
25095
+ if (explicitAgentId !== void 0) return {
25096
+ agentId: explicitAgentId,
25097
+ integrationId: "",
25098
+ via: "mention"
25099
+ };
25002
25100
  if (msg.sender.isBot) return null;
25003
25101
  const authz = (r) => !r.allowedUserIds || r.allowedUserIds.length === 0 || r.allowedUserIds.includes(msg.sender.id);
25004
25102
  const scopeCandidates = rules.filter((r) => scopeMatches(r, msg) && authz(r));
@@ -83053,6 +83151,22 @@ var CpClient = class {
83053
83151
  if (this.state !== "READY" && this.state !== "DRAINING") return;
83054
83152
  this.transport?.send(encode(buildEnvelope("cron/report", report)));
83055
83153
  }
83154
+ /**
83155
+ * Emit one chunk of a webchat turn's reply stream (D→C `webchat/output` EVT).
83156
+ * Ordered per-connection (TCP); `output.index` is the per-turn assembly counter.
83157
+ * No-op unless READY/DRAINING — a dropped chunk is lost (unlike latest-wins
83158
+ * telemetry), so the daemon only emits while connected. Content is inline, bounded
83159
+ * by the 256 KiB frame cap; the caller chunks any oversize single event.
83160
+ */
83161
+ emitWebchatOutput(output) {
83162
+ if (this.state !== "READY" && this.state !== "DRAINING") return;
83163
+ this.transport?.send(encode(buildEnvelope("webchat/output", output)));
83164
+ }
83165
+ /** Emit a webchat turn's completion (D→C `webchat/done` EVT). No-op unless READY/DRAINING. */
83166
+ emitWebchatDone(done) {
83167
+ if (this.state !== "READY" && this.state !== "DRAINING") return;
83168
+ this.transport?.send(encode(buildEnvelope("webchat/done", done)));
83169
+ }
83056
83170
  request(type, payload) {
83057
83171
  const frame = buildEnvelope(type, payload);
83058
83172
  return this.correlator.request(frame, (e) => this.transport.send(e));
@@ -83229,6 +83343,20 @@ var CpClient = class {
83229
83343
  case "workspace/gitpull":
83230
83344
  this.deps.workspaceGit.pull(frame.payload.agentId).then((result) => this.reply(frame, "workspace/gitpull/result", result)).catch((err) => this.workspaceError(frame.id, "workspace/gitpull", err));
83231
83345
  return;
83346
+ case "webchat/message":
83347
+ if (!this.deps.onWebchatMessage) {
83348
+ this.sendError(frame.id, "INTERNAL", "webchat not supported", false);
83349
+ return;
83350
+ }
83351
+ try {
83352
+ this.reply(frame, "webchat/ack", this.deps.onWebchatMessage(frame.payload));
83353
+ } catch (err) {
83354
+ this.sendError(frame.id, "INTERNAL", `webchat/message failed: ${err.message}`, false);
83355
+ }
83356
+ return;
83357
+ case "webchat/close":
83358
+ this.deps.onWebchatClose?.(frame.payload);
83359
+ return;
83232
83360
  default:
83233
83361
  this.deps.log.debug(`cp: ignoring ${frame.type}`);
83234
83362
  return;
@@ -84111,6 +84239,28 @@ function formatErr(err) {
84111
84239
  return e?.stack ?? String(err);
84112
84240
  }
84113
84241
  const MAX_QUEUED_PER_SESSION = 10;
84242
+ const WEBCHAT_CHUNK_BYTES = 32 * 1024;
84243
+ /** Split `text` into pieces whose UTF-8 byte length each stays under WEBCHAT_CHUNK_BYTES
84244
+ * so no single webchat/output frame exceeds the 256 KiB cap. Splits on byte budget by
84245
+ * character (never mid-code-point); short text returns a single piece. */
84246
+ function chunkText(text) {
84247
+ if (Buffer.byteLength(text) <= WEBCHAT_CHUNK_BYTES) return [text];
84248
+ const out = [];
84249
+ let buf = "";
84250
+ let bytes = 0;
84251
+ for (const ch of text) {
84252
+ const w = Buffer.byteLength(ch);
84253
+ if (bytes + w > WEBCHAT_CHUNK_BYTES && buf) {
84254
+ out.push(buf);
84255
+ buf = "";
84256
+ bytes = 0;
84257
+ }
84258
+ buf += ch;
84259
+ bytes += w;
84260
+ }
84261
+ if (buf) out.push(buf);
84262
+ return out;
84263
+ }
84114
84264
  const IDLE_FLUSH_MS = 2e3;
84115
84265
  /** Build the wire SessionKey (protocol §5) for a pending turn — what `drain/done`
84116
84266
  * reports as released so the CP may reassign it. Uses the real `thread` (absent for
@@ -84590,6 +84740,61 @@ var Daemon = class Daemon {
84590
84740
  this.log.info(`routing: ch=${msg.channel} → agent "${result.agentId}" (integration ${result.integrationId})`);
84591
84741
  this.dispatch(result.agentId, msg, result.integrationId).catch((err) => this.log.error(`dispatch failed for agent "${result.agentId}": ${formatErr(err)}`));
84592
84742
  }
84743
+ /**
84744
+ * Handle an inbound webchat turn (C→D `webchat/message`). webchat is the one
84745
+ * integration whose data plane is the daemon↔CP control WS itself — the far end is
84746
+ * the CP/browser, so the body arrives inline and the reply streams back over the
84747
+ * same WS. The message names its target agent explicitly (no trigger matching), so
84748
+ * this normalizes the turn and hands it to the same dispatch path as any session:
84749
+ * the turn is a REAL session (recorded to the transcript, visible in session/list),
84750
+ * only its transport differs. Returns the ack: accepted + a fresh turnId that
84751
+ * correlates the streamed webchat/output, or accepted:false / no_agent when the
84752
+ * named agent isn't runnable here.
84753
+ */
84754
+ handleWebchatMessage(m) {
84755
+ const turnId = randomUUID();
84756
+ const result = routeRules({
84757
+ platform: "webchat",
84758
+ channel: m.conversationId
84759
+ }, this.mergedRules(), () => null, m.agentId);
84760
+ if (!result || !this.agents.has(result.agentId)) {
84761
+ this.log.warn(`webchat: no agent "${m.agentId}" on this daemon — rejecting turn`);
84762
+ return {
84763
+ accepted: false,
84764
+ turnId,
84765
+ reason: "no_agent"
84766
+ };
84767
+ }
84768
+ const msg = {
84769
+ msgId: `webchat:${m.conversationId}`,
84770
+ traceId: turnId,
84771
+ source: "user",
84772
+ platform: "webchat",
84773
+ channel: m.conversationId,
84774
+ sender: {
84775
+ id: m.user ?? "webchat",
84776
+ isBot: false
84777
+ },
84778
+ text: m.text,
84779
+ mentionedBots: [],
84780
+ isDm: true,
84781
+ trigger: "dm"
84782
+ };
84783
+ this.dispatch(result.agentId, msg, void 0, {
84784
+ conversationId: m.conversationId,
84785
+ turnId
84786
+ }).catch((err) => this.log.error(`webchat dispatch failed for agent "${result.agentId}": ${formatErr(err)}`));
84787
+ return {
84788
+ accepted: true,
84789
+ turnId
84790
+ };
84791
+ }
84792
+ /** Handle a webchat conversation close (C→D `webchat/close`). No live resources are
84793
+ * bound per-conversation (the session TTL-closes like any other), so this is
84794
+ * currently just observability — the in-flight turn, if any, runs to completion. */
84795
+ handleWebchatClose(m) {
84796
+ this.log.debug(`webchat: conversation ${m.conversationId} closed by client`);
84797
+ }
84593
84798
  /** All of our own connections' bot identities (both `U…` user ids and `B…`
84594
84799
  * bot_ids), so we recognize self-echoes regardless of which field Slack sends. */
84595
84800
  ownBotIdentities() {
@@ -84733,14 +84938,14 @@ var Daemon = class Daemon {
84733
84938
  for (const cpRule of this.cpRouting?.effectiveRules() ?? []) if (!this.resolveCpAgent(cpRule.agentId)) out.add(cpRule.agentId);
84734
84939
  return [...out];
84735
84940
  }
84736
- async dispatch(agentId, msg, integrationId) {
84941
+ async dispatch(agentId, msg, integrationId, webchat) {
84737
84942
  if (this.draining || this.drainingAgents.has(agentId)) {
84738
84943
  this.log.debug(`dispatch: skipped for agent "${agentId}" (draining)`);
84739
84944
  return null;
84740
84945
  }
84741
84946
  const conv = new OutputConverger(this.agents.get(agentId).output.mode);
84742
84947
  const rec = new TranscriptRecorder();
84743
- const replyConn = msg.headless ? void 0 : this.replyConnFor(agentId, integrationId);
84948
+ const replyConn = msg.headless || webchat ? void 0 : this.replyConnFor(agentId, integrationId);
84744
84949
  const wasRunning = this.hostStarts.has(agentId);
84745
84950
  const statusThread = msg.thread ?? msg.msgId;
84746
84951
  const key = sessionKey(msg.platform, msg.channel, statusThread, agentId);
@@ -84767,13 +84972,18 @@ var Daemon = class Daemon {
84767
84972
  conn: replyConn,
84768
84973
  applyChain: Promise.resolve(),
84769
84974
  done,
84770
- resolveDone
84975
+ resolveDone,
84976
+ ...webchat ? { webchat: {
84977
+ ...webchat,
84978
+ index: 0,
84979
+ replyText: ""
84980
+ } } : {}
84771
84981
  };
84772
84982
  this.pending.set(sessionId, p);
84773
84983
  try {
84774
84984
  const host = await this.ensureHostAsync(agentId);
84775
84985
  if (!wasRunning) replyConn?.setStatus(msg.channel, statusThread, "is thinking…");
84776
- const { usage } = await host.prompt(sessionId, blocks);
84986
+ const { stopReason, usage } = await host.prompt(sessionId, blocks);
84777
84987
  if (usage) this.store.setTokenUsage(key, {
84778
84988
  totalTokens: usage.totalTokens,
84779
84989
  inputTokens: usage.inputTokens,
@@ -84796,8 +85006,25 @@ var Daemon = class Daemon {
84796
85006
  this.log.debug(`usage report emit failed (session ${sessionId}): ${err.message}`);
84797
85007
  }
84798
85008
  this.clearIdle(p);
84799
- const link = this.cfg.webAppUrl ? `${this.cfg.webAppUrl.replace(/\/$/, "")}/sessions/${sessionId}` : void 0;
84800
- for (const action of conv.onFinal(link)) this.enqueueApply(p, action);
85009
+ if (p.webchat) {
85010
+ if (p.webchat.replyText.trim()) this.store.appendTranscript({
85011
+ channel: p.channel,
85012
+ thread: statusThread,
85013
+ ts: String(Date.now()),
85014
+ sender: agentId,
85015
+ kind: "text",
85016
+ text: p.webchat.replyText
85017
+ });
85018
+ this.cpClient?.emitWebchatDone({
85019
+ conversationId: p.webchat.conversationId,
85020
+ turnId: p.webchat.turnId,
85021
+ ...stopReason ? { stopReason } : {},
85022
+ ...usage?.totalTokens !== void 0 ? { usage: { used: usage.totalTokens } } : {}
85023
+ });
85024
+ } else {
85025
+ const link = this.cfg.webAppUrl ? `${this.cfg.webAppUrl.replace(/\/$/, "")}/sessions/${sessionId}` : void 0;
85026
+ for (const action of conv.onFinal(link)) this.enqueueApply(p, action);
85027
+ }
84801
85028
  for (const ev of rec.onFinal()) this.recordEvent(agentId, msg.channel, statusThread, ev);
84802
85029
  await p.applyChain;
84803
85030
  } finally {
@@ -84918,10 +85145,68 @@ var Daemon = class Daemon {
84918
85145
  costCurrency: update.cost?.currency ?? void 0
84919
85146
  });
84920
85147
  if (update?.sessionUpdate === "session_info_update" && update.title !== void 0) this.store.setSessionTitle(p.sessionKey, update.title);
84921
- for (const action of p.conv.onUpdate(update)) this.enqueueApply(p, action);
84922
- this.armIdle(p);
85148
+ if (p.webchat) this.emitWebchatUpdate(p, update);
85149
+ else {
85150
+ for (const action of p.conv.onUpdate(update)) this.enqueueApply(p, action);
85151
+ this.armIdle(p);
85152
+ }
84923
85153
  for (const ev of p.rec.onUpdate(update)) this.recordEvent(p.agentId, p.channel, p.statusThread, ev);
84924
85154
  }
85155
+ /**
85156
+ * Map one ACP SessionUpdate to a WebchatEvent and stream it to the CP as a
85157
+ * `webchat/output` EVT (webchat's "send"). Only the streamable chunk kinds map;
85158
+ * usage/plan/session_info are handled elsewhere or dropped. A single event whose
85159
+ * inline text would blow the 256 KiB frame cap is split across multiple chunks,
85160
+ * each with its own monotonic `index`.
85161
+ */
85162
+ emitWebchatUpdate(p, update) {
85163
+ const wc = p.webchat;
85164
+ const emit = (event) => {
85165
+ this.cpClient?.emitWebchatOutput({
85166
+ conversationId: wc.conversationId,
85167
+ turnId: wc.turnId,
85168
+ index: wc.index++,
85169
+ event
85170
+ });
85171
+ };
85172
+ switch (update?.sessionUpdate) {
85173
+ case "agent_message_chunk": {
85174
+ const text = update.content?.type === "text" ? update.content.text ?? "" : "";
85175
+ if (text) {
85176
+ wc.replyText += text;
85177
+ for (const t of chunkText(text)) emit({
85178
+ kind: "message",
85179
+ text: t
85180
+ });
85181
+ }
85182
+ return;
85183
+ }
85184
+ case "agent_thought_chunk": {
85185
+ const text = update.content?.text ?? "";
85186
+ if (text) for (const t of chunkText(text)) emit({
85187
+ kind: "thinking",
85188
+ text: t
85189
+ });
85190
+ return;
85191
+ }
85192
+ case "tool_call":
85193
+ emit({
85194
+ kind: "tool_call",
85195
+ toolCallId: String(update.toolCallId ?? ""),
85196
+ title: String(update.title ?? update.toolCallId ?? "tool"),
85197
+ status: String(update.status ?? "pending")
85198
+ });
85199
+ return;
85200
+ case "tool_call_update":
85201
+ emit({
85202
+ kind: "tool_update",
85203
+ toolCallId: String(update.toolCallId ?? ""),
85204
+ status: String(update.status ?? "")
85205
+ });
85206
+ return;
85207
+ default: return;
85208
+ }
85209
+ }
84925
85210
  /** Persist one internal activity event (tool/reasoning). Ordered by row `seq`, so its
84926
85211
  * `ts` is just a wall-clock stamp for display — never used for replay/sorting. */
84927
85212
  recordEvent(agentId, channel, thread, ev) {
@@ -85330,6 +85615,8 @@ var Daemon = class Daemon {
85330
85615
  sessionRead: createSessionReader(this.store, (agentId) => this.replyConnFor(agentId)?.workspaceUrl),
85331
85616
  workspaceRead: createWorkspaceReader((id) => this.agents.get(id)?.workspace.path),
85332
85617
  workspaceGit: createWorkspaceGit((id) => this.agents.get(id)?.workspace.path),
85618
+ onWebchatMessage: (m) => this.handleWebchatMessage(m),
85619
+ onWebchatClose: (m) => this.handleWebchatClose(m),
85333
85620
  clock: systemClock,
85334
85621
  connect: () => ClientTransport.dial(url),
85335
85622
  log: this.log