@agentconnect.md/daemon 1.0.0-rc.122 → 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
@@ -7651,7 +7651,6 @@ const SlackConfigSchema = object({
7651
7651
  signingSecret: string().optional(),
7652
7652
  botUserId: string().optional(),
7653
7653
  allowedUserIds: array(string()).default([]),
7654
- notificationChannelId: string().optional(),
7655
7654
  bindRules: array(BindRuleConfigSchema).default([])
7656
7655
  });
7657
7656
  const IntegrationSchema = object({
@@ -12856,7 +12855,11 @@ const AuthOk = object({
12856
12855
  * agent/*, and event/session. Its canonical string form is
12857
12856
  * `${platform}:${channel}:${thread ?? "-"}`.
12858
12857
  */
12859
- const Platform = _enum(["slack", "telegram"]);
12858
+ const Platform = _enum([
12859
+ "slack",
12860
+ "telegram",
12861
+ "webchat"
12862
+ ]);
12860
12863
  const SessionKey = object({
12861
12864
  platform: Platform,
12862
12865
  channel: string(),
@@ -13144,7 +13147,6 @@ const IntegrationSlackConfig = object({
13144
13147
  botToken: string(),
13145
13148
  appToken: string(),
13146
13149
  allowedUserIds: array(string()).default([]),
13147
- notificationChannelId: string().optional(),
13148
13150
  bindRules: array(IntegrationBindRule).default([])
13149
13151
  });
13150
13152
  /**
@@ -13511,6 +13513,99 @@ const FactsRuntimeProfile = object({
13511
13513
  acpProtocolVersion: number().int().optional(),
13512
13514
  toolCalling: boolean()
13513
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
+ ]);
13514
13609
  //#endregion
13515
13610
  //#region ../protocol/dist/frame.js
13516
13611
  /**
@@ -13537,6 +13632,24 @@ const FRAME_SCHEMAS = {
13537
13632
  "agent/remove": AgentRemove,
13538
13633
  "agent/activity": AgentActivity,
13539
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() }),
13540
13653
  "route/assign": RouteAssign,
13541
13654
  "route/assign/ack": RouteAssignAck,
13542
13655
  "route/update": RouteUpdate,
@@ -13558,7 +13671,7 @@ const FRAME_SCHEMAS = {
13558
13671
  "event/session": EventSession,
13559
13672
  "usage/report": UsageReport,
13560
13673
  "facts/runtime-profile": FactsRuntimeProfile,
13561
- "facts/daemon-runtimes": object({ runtimes: array(FactsRuntimeProfile) }),
13674
+ "facts/daemon-runtimes": DaemonRuntimes,
13562
13675
  "session/list": SessionListReq,
13563
13676
  "session/list/page": SessionListPage,
13564
13677
  "session/history": SessionHistoryReq,
@@ -13573,44 +13686,12 @@ const FRAME_SCHEMAS = {
13573
13686
  "workspace/gitstatus/result": WorkspaceGitStatus,
13574
13687
  "workspace/gitpull": WorkspaceGitPullReq,
13575
13688
  "workspace/gitpull/result": WorkspaceGitPullResult,
13576
- "config/push": object({ keys: record(string(), unknown()) }),
13577
- "daemon/restart": object({
13578
- reason: string(),
13579
- drainFirst: boolean().default(true)
13580
- }),
13581
- "daemon/upgrade": object({
13582
- targetVersion: string(),
13583
- drainFirst: boolean().default(true)
13584
- }),
13585
- "daemon/control/ack": object({
13586
- accepted: boolean(),
13587
- willDrainUntil: string().datetime().optional()
13588
- }),
13589
- ack: object({
13590
- ok: boolean(),
13591
- reason: string().optional()
13592
- }),
13593
- error: object({
13594
- code: _enum([
13595
- "UNKNOWN_FRAME",
13596
- "FRAME_TOO_LARGE",
13597
- "PROTOCOL_STATE",
13598
- "BAD_PAYLOAD",
13599
- "AUTH_FAILED",
13600
- "ATTESTATION_INVALID",
13601
- "STALE_EPOCH",
13602
- "STALE_LAUNCH",
13603
- "NO_SESSION",
13604
- "SCOPE_DENIED",
13605
- "LEASE_EXPIRED",
13606
- "LEASE_DENIED",
13607
- "RATE_LIMITED",
13608
- "INTERNAL"
13609
- ]),
13610
- message: string(),
13611
- retryable: boolean(),
13612
- details: record(string(), unknown()).optional()
13613
- })
13689
+ "config/push": ConfigPush,
13690
+ "daemon/restart": DaemonRestart,
13691
+ "daemon/upgrade": DaemonUpgrade,
13692
+ "daemon/control/ack": DaemonControlAck,
13693
+ ack: Ack,
13694
+ error: ErrorFrame
13614
13695
  };
13615
13696
  Object.keys(FRAME_SCHEMAS);
13616
13697
  /**
@@ -13640,6 +13721,11 @@ discriminatedUnion("type", [
13640
13721
  frame("agent/remove", FRAME_SCHEMAS["agent/remove"]),
13641
13722
  frame("agent/activity", FRAME_SCHEMAS["agent/activity"]),
13642
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"]),
13643
13729
  frame("route/assign", FRAME_SCHEMAS["route/assign"]),
13644
13730
  frame("route/assign/ack", FRAME_SCHEMAS["route/assign/ack"]),
13645
13731
  frame("route/update", FRAME_SCHEMAS["route/update"]),
@@ -24999,8 +25085,18 @@ const pickRule = (r, via) => ({
24999
25085
  * (2) thread affinity (highest after explicit @; bypasses the kind filter, gated on
25000
25086
  * reachable-bot count) → (3) CP per-sessionKey override → (4/5) kind precedence
25001
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.
25002
25093
  */
25003
- 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
+ };
25004
25100
  if (msg.sender.isBot) return null;
25005
25101
  const authz = (r) => !r.allowedUserIds || r.allowedUserIds.length === 0 || r.allowedUserIds.includes(msg.sender.id);
25006
25102
  const scopeCandidates = rules.filter((r) => scopeMatches(r, msg) && authz(r));
@@ -83055,6 +83151,22 @@ var CpClient = class {
83055
83151
  if (this.state !== "READY" && this.state !== "DRAINING") return;
83056
83152
  this.transport?.send(encode(buildEnvelope("cron/report", report)));
83057
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
+ }
83058
83170
  request(type, payload) {
83059
83171
  const frame = buildEnvelope(type, payload);
83060
83172
  return this.correlator.request(frame, (e) => this.transport.send(e));
@@ -83231,6 +83343,20 @@ var CpClient = class {
83231
83343
  case "workspace/gitpull":
83232
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));
83233
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;
83234
83360
  default:
83235
83361
  this.deps.log.debug(`cp: ignoring ${frame.type}`);
83236
83362
  return;
@@ -83951,7 +84077,6 @@ function toIntegration(spec) {
83951
84077
  botToken: spec.slack.botToken,
83952
84078
  appToken: spec.slack.appToken,
83953
84079
  allowedUserIds: spec.slack.allowedUserIds,
83954
- ...spec.slack.notificationChannelId ? { notificationChannelId: spec.slack.notificationChannelId } : {},
83955
84080
  bindRules: spec.slack.bindRules
83956
84081
  }
83957
84082
  };
@@ -84114,6 +84239,28 @@ function formatErr(err) {
84114
84239
  return e?.stack ?? String(err);
84115
84240
  }
84116
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
+ }
84117
84264
  const IDLE_FLUSH_MS = 2e3;
84118
84265
  /** Build the wire SessionKey (protocol §5) for a pending turn — what `drain/done`
84119
84266
  * reports as released so the CP may reassign it. Uses the real `thread` (absent for
@@ -84593,6 +84740,61 @@ var Daemon = class Daemon {
84593
84740
  this.log.info(`routing: ch=${msg.channel} → agent "${result.agentId}" (integration ${result.integrationId})`);
84594
84741
  this.dispatch(result.agentId, msg, result.integrationId).catch((err) => this.log.error(`dispatch failed for agent "${result.agentId}": ${formatErr(err)}`));
84595
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
+ }
84596
84798
  /** All of our own connections' bot identities (both `U…` user ids and `B…`
84597
84799
  * bot_ids), so we recognize self-echoes regardless of which field Slack sends. */
84598
84800
  ownBotIdentities() {
@@ -84736,14 +84938,14 @@ var Daemon = class Daemon {
84736
84938
  for (const cpRule of this.cpRouting?.effectiveRules() ?? []) if (!this.resolveCpAgent(cpRule.agentId)) out.add(cpRule.agentId);
84737
84939
  return [...out];
84738
84940
  }
84739
- async dispatch(agentId, msg, integrationId) {
84941
+ async dispatch(agentId, msg, integrationId, webchat) {
84740
84942
  if (this.draining || this.drainingAgents.has(agentId)) {
84741
84943
  this.log.debug(`dispatch: skipped for agent "${agentId}" (draining)`);
84742
84944
  return null;
84743
84945
  }
84744
84946
  const conv = new OutputConverger(this.agents.get(agentId).output.mode);
84745
84947
  const rec = new TranscriptRecorder();
84746
- const replyConn = msg.headless ? void 0 : this.replyConnFor(agentId, integrationId);
84948
+ const replyConn = msg.headless || webchat ? void 0 : this.replyConnFor(agentId, integrationId);
84747
84949
  const wasRunning = this.hostStarts.has(agentId);
84748
84950
  const statusThread = msg.thread ?? msg.msgId;
84749
84951
  const key = sessionKey(msg.platform, msg.channel, statusThread, agentId);
@@ -84770,13 +84972,18 @@ var Daemon = class Daemon {
84770
84972
  conn: replyConn,
84771
84973
  applyChain: Promise.resolve(),
84772
84974
  done,
84773
- resolveDone
84975
+ resolveDone,
84976
+ ...webchat ? { webchat: {
84977
+ ...webchat,
84978
+ index: 0,
84979
+ replyText: ""
84980
+ } } : {}
84774
84981
  };
84775
84982
  this.pending.set(sessionId, p);
84776
84983
  try {
84777
84984
  const host = await this.ensureHostAsync(agentId);
84778
84985
  if (!wasRunning) replyConn?.setStatus(msg.channel, statusThread, "is thinking…");
84779
- const { usage } = await host.prompt(sessionId, blocks);
84986
+ const { stopReason, usage } = await host.prompt(sessionId, blocks);
84780
84987
  if (usage) this.store.setTokenUsage(key, {
84781
84988
  totalTokens: usage.totalTokens,
84782
84989
  inputTokens: usage.inputTokens,
@@ -84799,8 +85006,25 @@ var Daemon = class Daemon {
84799
85006
  this.log.debug(`usage report emit failed (session ${sessionId}): ${err.message}`);
84800
85007
  }
84801
85008
  this.clearIdle(p);
84802
- const link = this.cfg.webAppUrl ? `${this.cfg.webAppUrl.replace(/\/$/, "")}/sessions/${sessionId}` : void 0;
84803
- 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
+ }
84804
85028
  for (const ev of rec.onFinal()) this.recordEvent(agentId, msg.channel, statusThread, ev);
84805
85029
  await p.applyChain;
84806
85030
  } finally {
@@ -84921,10 +85145,68 @@ var Daemon = class Daemon {
84921
85145
  costCurrency: update.cost?.currency ?? void 0
84922
85146
  });
84923
85147
  if (update?.sessionUpdate === "session_info_update" && update.title !== void 0) this.store.setSessionTitle(p.sessionKey, update.title);
84924
- for (const action of p.conv.onUpdate(update)) this.enqueueApply(p, action);
84925
- 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
+ }
84926
85153
  for (const ev of p.rec.onUpdate(update)) this.recordEvent(p.agentId, p.channel, p.statusThread, ev);
84927
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
+ }
84928
85210
  /** Persist one internal activity event (tool/reasoning). Ordered by row `seq`, so its
84929
85211
  * `ts` is just a wall-clock stamp for display — never used for replay/sorting. */
84930
85212
  recordEvent(agentId, channel, thread, ev) {
@@ -85333,6 +85615,8 @@ var Daemon = class Daemon {
85333
85615
  sessionRead: createSessionReader(this.store, (agentId) => this.replyConnFor(agentId)?.workspaceUrl),
85334
85616
  workspaceRead: createWorkspaceReader((id) => this.agents.get(id)?.workspace.path),
85335
85617
  workspaceGit: createWorkspaceGit((id) => this.agents.get(id)?.workspace.path),
85618
+ onWebchatMessage: (m) => this.handleWebchatMessage(m),
85619
+ onWebchatClose: (m) => this.handleWebchatClose(m),
85336
85620
  clock: systemClock,
85337
85621
  connect: () => ClientTransport.dial(url),
85338
85622
  log: this.log