@agentconnect.md/daemon 1.0.0-rc.79 → 1.0.0-rc.80

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
@@ -13257,6 +13257,19 @@ const SessionListItem = object({
13257
13257
  const SessionListReq = object({ agentId: string().uuid().optional() });
13258
13258
  /** D→C REP (corr = req id): the daemon's current sessions. */
13259
13259
  const SessionListPage = object({ sessions: array(SessionListItem) });
13260
+ object({
13261
+ toolCallId: string(),
13262
+ kind: string().optional(),
13263
+ status: string().optional(),
13264
+ rawInput: unknown().optional(),
13265
+ rawOutput: unknown().optional(),
13266
+ content: array(unknown()).optional(),
13267
+ locations: array(object({
13268
+ path: string(),
13269
+ line: number().int().optional()
13270
+ })).optional(),
13271
+ truncated: boolean().optional()
13272
+ });
13260
13273
  /** One message in a session transcript page (a body — returned only for display). */
13261
13274
  const SessionMessage = object({
13262
13275
  seq: number().int(),
@@ -13264,7 +13277,13 @@ const SessionMessage = object({
13264
13277
  senderName: string().optional(),
13265
13278
  ts: string(),
13266
13279
  kind: string(),
13267
- text: string()
13280
+ text: string(),
13281
+ toolCallId: string().optional(),
13282
+ toolStatus: string().optional(),
13283
+ toolKind: string().optional(),
13284
+ body: string().optional(),
13285
+ bodyTruncated: boolean().optional(),
13286
+ bodyBytes: number().int().optional()
13268
13287
  });
13269
13288
  /** C→D REQ: fetch one page of a session's history from the owning daemon. */
13270
13289
  const SessionHistoryReq = object({
@@ -13278,6 +13297,24 @@ const SessionHistoryPage = object({
13278
13297
  messages: array(SessionMessage),
13279
13298
  nextCursor: string().optional()
13280
13299
  });
13300
+ /**
13301
+ * C→D REQ: fetch the FULL (untruncated) ToolBody JSON for one tool call, chunked.
13302
+ * The inline `SessionMessage.body` preview is capped at 32 KiB; when a body is
13303
+ * bigger the console pages the whole thing back through this frame by offset.
13304
+ */
13305
+ const SessionToolBodyReq = object({
13306
+ sessionId: string(),
13307
+ toolCallId: string(),
13308
+ offset: number().int().nonnegative().default(0)
13309
+ });
13310
+ /** D→C REP (corr = the req id): one frame-budgeted byte slice of the full ToolBody JSON. */
13311
+ const SessionToolBodyChunk = object({
13312
+ sessionId: string(),
13313
+ toolCallId: string(),
13314
+ data: string(),
13315
+ totalBytes: number().int(),
13316
+ nextOffset: number().int().optional()
13317
+ });
13281
13318
  //#endregion
13282
13319
  //#region ../protocol/dist/frames/workspace.js
13283
13320
  /**
@@ -13518,6 +13555,8 @@ const FRAME_SCHEMAS = {
13518
13555
  "session/list/page": SessionListPage,
13519
13556
  "session/history": SessionHistoryReq,
13520
13557
  "session/history/page": SessionHistoryPage,
13558
+ "session/tool-body": SessionToolBodyReq,
13559
+ "session/tool-body/chunk": SessionToolBodyChunk,
13521
13560
  "workspace/list": WorkspaceListReq,
13522
13561
  "workspace/list/page": WorkspaceListPage,
13523
13562
  "workspace/read": WorkspaceReadReq,
@@ -13621,6 +13660,8 @@ discriminatedUnion("type", [
13621
13660
  frame("session/list/page", FRAME_SCHEMAS["session/list/page"]),
13622
13661
  frame("session/history", FRAME_SCHEMAS["session/history"]),
13623
13662
  frame("session/history/page", FRAME_SCHEMAS["session/history/page"]),
13663
+ frame("session/tool-body", FRAME_SCHEMAS["session/tool-body"]),
13664
+ frame("session/tool-body/chunk", FRAME_SCHEMAS["session/tool-body/chunk"]),
13624
13665
  frame("workspace/list", FRAME_SCHEMAS["workspace/list"]),
13625
13666
  frame("workspace/list/page", FRAME_SCHEMAS["workspace/list/page"]),
13626
13667
  frame("workspace/read", FRAME_SCHEMAS["workspace/read"]),
@@ -23865,13 +23906,17 @@ var LocalStore = class {
23865
23906
  CREATE TABLE IF NOT EXISTS transcript (
23866
23907
  seq INTEGER PRIMARY KEY AUTOINCREMENT,
23867
23908
  channel TEXT NOT NULL, thread TEXT NOT NULL, ts TEXT,
23868
- sender TEXT NOT NULL, kind TEXT NOT NULL, text TEXT NOT NULL
23909
+ sender TEXT NOT NULL, kind TEXT NOT NULL, text TEXT NOT NULL,
23910
+ tool_call_id TEXT, body TEXT
23869
23911
  );
23870
23912
  CREATE INDEX IF NOT EXISTS transcript_thread_seq ON transcript (channel, thread, seq);
23871
23913
  -- Dedup conversational rows by platform ts (double-fired inbound / redelivery);
23872
23914
  -- internal events have no platform ts and are intentionally never deduped here.
23873
23915
  CREATE UNIQUE INDEX IF NOT EXISTS transcript_text_ts
23874
23916
  ON transcript (channel, thread, ts) WHERE kind = 'text';
23917
+ -- transcript_tool_call (partial unique on tool_call_id) is created in
23918
+ -- migrateTranscriptToolBody, after the column is guaranteed to exist — a legacy
23919
+ -- rebuild in migrateTranscript recreates the table without the new columns.
23875
23920
  CREATE TABLE IF NOT EXISTS cp_routing (
23876
23921
  id INTEGER PRIMARY KEY CHECK (id = 1),
23877
23922
  routingEpoch INTEGER, assignments TEXT, globalRules TEXT
@@ -23883,6 +23928,18 @@ var LocalStore = class {
23883
23928
  );
23884
23929
  `);
23885
23930
  this.migrateSessionUsage();
23931
+ this.migrateTranscriptToolBody();
23932
+ }
23933
+ /** Add the `transcript.tool_call_id` + `transcript.body` columns (and the partial
23934
+ * unique index) to a pre-existing DB. CREATE TABLE IF NOT EXISTS above adds them for
23935
+ * fresh DBs but never alters an existing table, so upgrade in place. No-op once the
23936
+ * columns are present. */
23937
+ migrateTranscriptToolBody() {
23938
+ const cols = this.db.prepare("PRAGMA table_info(transcript)").all();
23939
+ if (!cols.some((c) => c.name === "tool_call_id")) this.db.exec("ALTER TABLE transcript ADD COLUMN tool_call_id TEXT");
23940
+ if (!cols.some((c) => c.name === "body")) this.db.exec("ALTER TABLE transcript ADD COLUMN body TEXT");
23941
+ this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS transcript_tool_call
23942
+ ON transcript (channel, thread, tool_call_id) WHERE tool_call_id IS NOT NULL`);
23886
23943
  }
23887
23944
  /** Add the `sessions.usage` (JSON token accounting), `sessions.muted` (`!stop`
23888
23945
  * thread mute) and `sessions.triggeredBy` columns to a pre-existing DB. CREATE
@@ -24032,6 +24089,32 @@ var LocalStore = class {
24032
24089
  appendTranscript(e) {
24033
24090
  this.db.prepare("INSERT OR IGNORE INTO transcript (channel, thread, ts, sender, kind, text) VALUES (@channel, @thread, @ts, @sender, @kind, @text)").run(e);
24034
24091
  }
24092
+ /** First sight of a tool call: insert its kind='tool' row (title in `text`, the
24093
+ * serialized ToolBody in `body`). INSERT OR IGNORE so a re-fired first update is a
24094
+ * no-op — the partial unique index on (channel, thread, tool_call_id) dedups. `seq`
24095
+ * is assigned here and stays stable across later `updateToolCall`s. */
24096
+ insertToolCall(e) {
24097
+ this.db.prepare(`INSERT OR IGNORE INTO transcript (channel, thread, ts, sender, kind, text, tool_call_id, body)
24098
+ VALUES (@channel, @thread, @ts, @sender, 'tool', @text, @toolCallId, @body)`).run({
24099
+ channel: e.channel,
24100
+ thread: e.thread,
24101
+ ts: e.ts,
24102
+ sender: e.sender,
24103
+ text: e.title,
24104
+ toolCallId: e.toolCallId,
24105
+ body: e.body
24106
+ });
24107
+ }
24108
+ /** Later update for a tool call: overwrite `text` (title) + `body` on the existing
24109
+ * row, keyed by (channel, thread, tool_call_id). `seq`/`ts` are untouched so the
24110
+ * row keeps its first-seen position. No-op if the row is absent. */
24111
+ updateToolCall(channel, thread, toolCallId, patch) {
24112
+ this.db.prepare("UPDATE transcript SET text = ?, body = ? WHERE channel = ? AND thread = ? AND tool_call_id = ?").run(patch.title, patch.body, channel, thread, toolCallId);
24113
+ }
24114
+ /** The full stored ToolBody JSON for one tool call, or undefined if unknown. */
24115
+ getToolBody(channel, thread, toolCallId) {
24116
+ return this.db.prepare("SELECT body FROM transcript WHERE channel = ? AND thread = ? AND tool_call_id = ?").get(channel, thread, toolCallId)?.body ?? void 0;
24117
+ }
24035
24118
  /**
24036
24119
  * §8.5 cross-agent catch-up: conversational (`text`) rows only — tool/reasoning rows
24037
24120
  * are audit/UI data and must never be replayed back into an agent's prompt. Ordered by
@@ -24294,6 +24377,9 @@ function transcriptCoords(msg) {
24294
24377
  }
24295
24378
  //#endregion
24296
24379
  //#region src/session/transcript-recorder.ts
24380
+ /** Single-body write ceiling (protects the daemon DB from a huge rawOutput/content);
24381
+ * separate from the read-side 32 KiB inline preview cap. */
24382
+ const MAX_TOOL_BODY_BYTES = 1024 * 1024;
24297
24383
  /**
24298
24384
  * Turns the raw ACP `session/update` stream into the *full* activity log — tool calls
24299
24385
  * and reasoning — independent of the agent's Slack `output.mode`. (Output mode only
@@ -24302,12 +24388,14 @@ function transcriptCoords(msg) {
24302
24388
  * It deliberately does NOT emit `text`: conversational rows are recorded at the
24303
24389
  * platform-send boundary instead, where the real message `ts` is known (so they sort
24304
24390
  * against the §8.5 replay marker). Reasoning chunks are coalesced into one block per
24305
- * run; each tool invocation is recorded once (keyed by toolCallId, so the burst of
24306
- * `tool_call_update`s for one call collapses to a single row).
24391
+ * run; each tool invocation is merged per `toolCallId` across its `tool_call` +
24392
+ * `tool_call_update` burst — emitting an `insert` on first sight and an `update` on
24393
+ * each later change, so the row's full body fills in as the call streams to completion.
24307
24394
  */
24308
24395
  var TranscriptRecorder = class {
24309
24396
  thought = "";
24310
- seenTools = /* @__PURE__ */ new Set();
24397
+ tools = /* @__PURE__ */ new Map();
24398
+ titles = /* @__PURE__ */ new Map();
24311
24399
  onUpdate(update) {
24312
24400
  switch (update.sessionUpdate) {
24313
24401
  case "agent_thought_chunk": {
@@ -24320,11 +24408,17 @@ var TranscriptRecorder = class {
24320
24408
  const flushed = this.flushThought();
24321
24409
  const u = update;
24322
24410
  const id = u.toolCallId ?? "";
24323
- if (id && this.seenTools.has(id)) return flushed;
24324
- if (id) this.seenTools.add(id);
24411
+ if (!id) return flushed;
24412
+ const first = !this.tools.has(id);
24413
+ const body = this.merge(id, u);
24414
+ const title = u.title ?? this.titles.get(id) ?? id;
24415
+ if (u.title !== void 0 && u.title !== null) this.titles.set(id, u.title);
24325
24416
  return [...flushed, {
24326
24417
  kind: "tool",
24327
- text: u.title ?? (id || "tool")
24418
+ op: first ? "insert" : "update",
24419
+ toolCallId: id,
24420
+ text: title,
24421
+ body: serializeBody(body)
24328
24422
  }];
24329
24423
  }
24330
24424
  case "agent_message_chunk": return this.flushThought();
@@ -24335,6 +24429,23 @@ var TranscriptRecorder = class {
24335
24429
  onFinal() {
24336
24430
  return this.flushThought();
24337
24431
  }
24432
+ /**
24433
+ * Merge one `tool_call`/`tool_call_update` into the accumulated ToolBody per ACP
24434
+ * semantics: present-and-non-null scalar fields (kind/status/rawInput/rawOutput)
24435
+ * overlay the prior value; `content`/`locations` are wholesale replacements when
24436
+ * present. Returns the updated body (also cached in `tools`).
24437
+ */
24438
+ merge(id, u) {
24439
+ const body = this.tools.get(id) ?? { toolCallId: id };
24440
+ if (u.kind !== void 0 && u.kind !== null) body.kind = u.kind;
24441
+ if (u.status !== void 0 && u.status !== null) body.status = u.status;
24442
+ if (u.rawInput !== void 0 && u.rawInput !== null) body.rawInput = u.rawInput;
24443
+ if (u.rawOutput !== void 0 && u.rawOutput !== null) body.rawOutput = u.rawOutput;
24444
+ if (u.content !== void 0 && u.content !== null) body.content = u.content;
24445
+ if (u.locations !== void 0 && u.locations !== null) body.locations = u.locations;
24446
+ this.tools.set(id, body);
24447
+ return body;
24448
+ }
24338
24449
  flushThought() {
24339
24450
  const text = this.thought.trim();
24340
24451
  this.thought = "";
@@ -24344,6 +24455,25 @@ var TranscriptRecorder = class {
24344
24455
  }] : [];
24345
24456
  }
24346
24457
  };
24458
+ /** Serialize a ToolBody, capping the encoded size at MAX_TOOL_BODY_BYTES. When over
24459
+ * cap, drop the biggest free-form tails (rawOutput first, then content) and flag
24460
+ * `truncated` so the read side / UI can show it was capped at write time. */
24461
+ function serializeBody(body) {
24462
+ let json = JSON.stringify(body);
24463
+ if (Buffer.byteLength(json) <= MAX_TOOL_BODY_BYTES) return json;
24464
+ const capped = {
24465
+ ...body,
24466
+ truncated: true
24467
+ };
24468
+ capped.rawOutput = void 0;
24469
+ json = JSON.stringify(capped);
24470
+ if (Buffer.byteLength(json) <= MAX_TOOL_BODY_BYTES) return json;
24471
+ capped.content = void 0;
24472
+ json = JSON.stringify(capped);
24473
+ if (Buffer.byteLength(json) <= MAX_TOOL_BODY_BYTES) return json;
24474
+ capped.rawInput = void 0;
24475
+ return JSON.stringify(capped);
24476
+ }
24347
24477
  //#endregion
24348
24478
  //#region src/mcp/ipc.ts
24349
24479
  /** Frame a message for the wire: compact JSON + a single trailing newline. */
@@ -82323,7 +82453,7 @@ function makeLogger(level = "info") {
82323
82453
  const SNIFF_BYTES = 8192;
82324
82454
  /** Encoded-payload ceiling, leaving headroom under MAX_FRAME_BYTES for the
82325
82455
  * envelope (id/ts/type/corr + fencing ext, well under 4 KiB). */
82326
- const REPLY_BUDGET = MAX_FRAME_BYTES - 4096;
82456
+ const REPLY_BUDGET$1 = MAX_FRAME_BYTES - 4096;
82327
82457
  /** Path-containment / bad-request violation → `BAD_PAYLOAD` on the wire. */
82328
82458
  var WorkspaceViolationError = class extends Error {
82329
82459
  constructor(message) {
@@ -82340,7 +82470,7 @@ function under(root, p) {
82340
82470
  /** Largest index ≤ `len` that lands on a UTF-8 character boundary — i.e. never
82341
82471
  * cuts a multi-byte sequence. If the slice's trailing char is incomplete, the
82342
82472
  * boundary is placed before that char (its bytes belong to the next slice). */
82343
- function utf8Boundary(buf, len) {
82473
+ function utf8Boundary$1(buf, len) {
82344
82474
  if (len >= buf.length) len = buf.length;
82345
82475
  if (len <= 0) return 0;
82346
82476
  let start = len - 1;
@@ -82351,7 +82481,7 @@ function utf8Boundary(buf, len) {
82351
82481
  }
82352
82482
  /** The encoded size of the payload the wire will carry (JSON.stringify matches
82353
82483
  * the codec's `encode`), measured in bytes. */
82354
- function encodedBytes(payload) {
82484
+ function encodedBytes$1(payload) {
82355
82485
  return Buffer.byteLength(JSON.stringify(payload));
82356
82486
  }
82357
82487
  function createWorkspaceReader(workspaceRootByAgent) {
@@ -82435,8 +82565,8 @@ function createWorkspaceReader(workspaceRootByAgent) {
82435
82565
  let acc = 0;
82436
82566
  for (let i = start; i < entries.length && page.length < req.limit; i++) {
82437
82567
  const e = entries[i];
82438
- const enc = encodedBytes(e) + 1;
82439
- if (page.length > 0 && acc + enc > REPLY_BUDGET) break;
82568
+ const enc = encodedBytes$1(e) + 1;
82569
+ if (page.length > 0 && acc + enc > REPLY_BUDGET$1) break;
82440
82570
  page.push(e);
82441
82571
  acc += enc;
82442
82572
  }
@@ -82490,12 +82620,12 @@ function createWorkspaceReader(workspaceRootByAgent) {
82490
82620
  const { bytesRead } = await fh.read(buf, 0, want, req.offset);
82491
82621
  slice = buf.subarray(0, bytesRead);
82492
82622
  }
82493
- let end = utf8Boundary(slice, slice.length);
82623
+ let end = utf8Boundary$1(slice, slice.length);
82494
82624
  let content = slice.toString("utf8", 0, end);
82495
- while (end > 0 && encodedBytes(content) > REPLY_BUDGET) {
82496
- const factor = REPLY_BUDGET / encodedBytes(content);
82625
+ while (end > 0 && encodedBytes$1(content) > REPLY_BUDGET$1) {
82626
+ const factor = REPLY_BUDGET$1 / encodedBytes$1(content);
82497
82627
  const shrunk = Math.max(1, Math.floor(end * factor * .9));
82498
- end = utf8Boundary(slice, Math.min(shrunk, end - 1));
82628
+ end = utf8Boundary$1(slice, Math.min(shrunk, end - 1));
82499
82629
  content = slice.toString("utf8", 0, end);
82500
82630
  }
82501
82631
  const nextOffset = req.offset + end;
@@ -82893,6 +83023,13 @@ var CpClient = class {
82893
83023
  this.sendError(frame.id, "INTERNAL", `session/history failed: ${err.message}`, false);
82894
83024
  }
82895
83025
  return;
83026
+ case "session/tool-body":
83027
+ try {
83028
+ this.reply(frame, "session/tool-body/chunk", this.deps.sessionRead.toolBody(frame.payload));
83029
+ } catch (err) {
83030
+ this.sendError(frame.id, "INTERNAL", `session/tool-body failed: ${err.message}`, false);
83031
+ }
83032
+ return;
82896
83033
  case "workspace/list":
82897
83034
  this.deps.workspaceRead.list(frame.payload).then((page) => this.reply(frame, "workspace/list/page", page)).catch((err) => this.workspaceError(frame.id, "workspace/list", err));
82898
83035
  return;
@@ -82935,6 +83072,40 @@ var CpClient = class {
82935
83072
  };
82936
83073
  //#endregion
82937
83074
  //#region src/cp/session-reader.ts
83075
+ /**
83076
+ * `SessionReader` — the read-only seam answering the CP's `session/list`,
83077
+ * `session/history` and `session/tool-body` REQs from the daemon's local store.
83078
+ * Bodies live only on the daemon (§1/§12); this projects store rows onto the
83079
+ * protocol shapes. The protocol `sessionId` IS the store's `acpSessionId`
83080
+ * (SessionManager returns it).
83081
+ *
83082
+ * Frame-size safety: every REP must serialise under the 256 KiB wire cap. A single
83083
+ * tool body can be large, so `history` caps each inline `body` at a 32 KiB VALID-JSON
83084
+ * preview and assembles a page under the per-frame budget (newest rows that fit);
83085
+ * the FULL body is pulled on demand via `toolBody`, chunked by offset.
83086
+ */
83087
+ /** Encoded-payload ceiling, leaving headroom under MAX_FRAME_BYTES for the
83088
+ * envelope (id/ts/type/corr + fencing ext, well under 4 KiB). */
83089
+ const REPLY_BUDGET = MAX_FRAME_BYTES - 4096;
83090
+ /** Inline `SessionMessage.body` preview cap. Bodies over this are shrunk to a
83091
+ * valid-JSON preview + `bodyTruncated`; the full body is fetched via `toolBody`. */
83092
+ const PREVIEW_CAP = 32 * 1024;
83093
+ /** The encoded size of the payload the wire will carry (JSON.stringify matches the
83094
+ * codec's `encode`), measured in bytes. */
83095
+ function encodedBytes(payload) {
83096
+ return Buffer.byteLength(JSON.stringify(payload));
83097
+ }
83098
+ /** Largest index ≤ `len` that lands on a UTF-8 character boundary — never cuts a
83099
+ * multi-byte sequence (mirrors workspace-reader's slicer). */
83100
+ function utf8Boundary(buf, len) {
83101
+ if (len >= buf.length) len = buf.length;
83102
+ if (len <= 0) return 0;
83103
+ let start = len - 1;
83104
+ while (start > 0 && ((buf[start] ?? 0) & 192) === 128) start--;
83105
+ const lead = buf[start] ?? 0;
83106
+ const seqLen = lead < 128 ? 1 : (lead & 224) === 192 ? 2 : (lead & 240) === 224 ? 3 : (lead & 248) === 240 ? 4 : 1;
83107
+ return start + seqLen <= len ? len : start;
83108
+ }
82938
83109
  /** Parse a session row's `usage` JSON into the wire shape. Returns undefined when
82939
83110
  * absent, empty, or unparseable so the optional field is simply omitted. */
82940
83111
  function parseUsage(raw) {
@@ -82946,6 +83117,46 @@ function parseUsage(raw) {
82946
83117
  return;
82947
83118
  }
82948
83119
  }
83120
+ /** Rough character budget to trim a free-form value to when shrinking a preview. A
83121
+ * string is truncated to this many chars; a non-string is stringified then truncated
83122
+ * (kept as a string — the preview is still valid JSON, just lossy on that field). */
83123
+ function shrinkField(value, chars) {
83124
+ if (value === void 0) return void 0;
83125
+ const s = typeof value === "string" ? value : JSON.stringify(value);
83126
+ if (s.length <= chars) return value;
83127
+ return s.slice(0, chars) + "…";
83128
+ }
83129
+ /**
83130
+ * Build a VALID-JSON preview of a tool body that encodes within PREVIEW_CAP. The
83131
+ * heavy free-form fields (rawOutput, rawInput, content) are trimmed progressively
83132
+ * until it fits; `truncated`/status/kind/locations are preserved. Always sets
83133
+ * `truncated:true` on the returned preview.
83134
+ */
83135
+ function previewBody(full) {
83136
+ const preview = {
83137
+ ...full,
83138
+ truncated: true
83139
+ };
83140
+ for (const chars of [
83141
+ 8192,
83142
+ 2048,
83143
+ 512,
83144
+ 128,
83145
+ 16
83146
+ ]) {
83147
+ preview.rawOutput = shrinkField(full.rawOutput, chars);
83148
+ preview.rawInput = shrinkField(full.rawInput, chars);
83149
+ if (full.content !== void 0) {
83150
+ const asStr = JSON.stringify(full.content);
83151
+ preview.content = asStr.length <= chars ? full.content : [shrinkField(asStr, chars)];
83152
+ }
83153
+ if (encodedBytes(preview) <= PREVIEW_CAP) return preview;
83154
+ }
83155
+ preview.rawOutput = void 0;
83156
+ preview.rawInput = void 0;
83157
+ preview.content = void 0;
83158
+ return preview;
83159
+ }
82949
83160
  function createSessionReader(store) {
82950
83161
  return {
82951
83162
  list(req) {
@@ -82982,9 +83193,9 @@ function createSessionReader(store) {
82982
83193
  const { rows, hasMore } = store.transcriptPage(rec.channel, rec.thread, before, req.limit);
82983
83194
  const ordered = rows.slice().reverse();
82984
83195
  const names = store.getDisplayNames(ordered.map((r) => r.sender));
82985
- const messages = ordered.map((r) => {
83196
+ const built = ordered.map((r) => {
82986
83197
  const senderName = names.get(r.sender);
82987
- return {
83198
+ const base = {
82988
83199
  seq: r.seq,
82989
83200
  sender: r.sender,
82990
83201
  ...senderName ? { senderName } : {},
@@ -82992,11 +83203,76 @@ function createSessionReader(store) {
82992
83203
  kind: r.kind,
82993
83204
  text: r.text
82994
83205
  };
83206
+ if (r.kind !== "tool" || !r.body) return base;
83207
+ let full;
83208
+ try {
83209
+ full = JSON.parse(r.body);
83210
+ } catch {
83211
+ return base;
83212
+ }
83213
+ const bytes = Buffer.byteLength(r.body);
83214
+ base.toolCallId = full.toolCallId ?? r.toolCallId ?? void 0;
83215
+ if (full.status !== void 0) base.toolStatus = full.status;
83216
+ if (full.kind !== void 0) base.toolKind = full.kind;
83217
+ if (bytes <= PREVIEW_CAP) base.body = r.body;
83218
+ else {
83219
+ base.body = JSON.stringify(previewBody(full));
83220
+ base.bodyTruncated = true;
83221
+ base.bodyBytes = bytes;
83222
+ }
83223
+ return base;
82995
83224
  });
83225
+ const kept = [];
83226
+ let acc = 0;
83227
+ let droppedToBudget = false;
83228
+ for (let i = built.length - 1; i >= 0; i--) {
83229
+ const m = built[i];
83230
+ const enc = encodedBytes(m) + 1;
83231
+ if (kept.length > 0 && acc + enc > REPLY_BUDGET) {
83232
+ droppedToBudget = true;
83233
+ break;
83234
+ }
83235
+ kept.push(m);
83236
+ acc += enc;
83237
+ }
83238
+ kept.reverse();
83239
+ const hasOlder = hasMore || droppedToBudget;
82996
83240
  return {
82997
83241
  sessionId: req.sessionId,
82998
- messages,
82999
- ...hasMore ? { nextCursor: String(rows[rows.length - 1].seq) } : {}
83242
+ messages: kept,
83243
+ ...hasOlder && kept.length > 0 ? { nextCursor: String(kept[0].seq) } : {}
83244
+ };
83245
+ },
83246
+ toolBody(req) {
83247
+ const rec = store.getSessionByAcpId(req.sessionId);
83248
+ const empty = {
83249
+ sessionId: req.sessionId,
83250
+ toolCallId: req.toolCallId,
83251
+ data: "",
83252
+ totalBytes: 0
83253
+ };
83254
+ if (!rec) return empty;
83255
+ const body = store.getToolBody(rec.channel, rec.thread, req.toolCallId);
83256
+ if (body === void 0) return empty;
83257
+ const buf = Buffer.from(body, "utf8");
83258
+ const totalBytes = buf.length;
83259
+ const offset = Math.min(req.offset, totalBytes);
83260
+ const remaining = buf.subarray(offset);
83261
+ let end = utf8Boundary(remaining, remaining.length);
83262
+ let data = remaining.toString("utf8", 0, end);
83263
+ while (end > 0 && encodedBytes(data) > REPLY_BUDGET) {
83264
+ const factor = REPLY_BUDGET / encodedBytes(data);
83265
+ const shrunk = Math.max(1, Math.floor(end * factor * .9));
83266
+ end = utf8Boundary(remaining, Math.min(shrunk, end - 1));
83267
+ data = remaining.toString("utf8", 0, end);
83268
+ }
83269
+ const nextOffset = offset + end;
83270
+ return {
83271
+ sessionId: req.sessionId,
83272
+ toolCallId: req.toolCallId,
83273
+ data,
83274
+ totalBytes,
83275
+ ...nextOffset < totalBytes ? { nextOffset } : {}
83000
83276
  };
83001
83277
  }
83002
83278
  };
@@ -84409,6 +84685,22 @@ var Daemon = class Daemon {
84409
84685
  /** Persist one internal activity event (tool/reasoning). Ordered by row `seq`, so its
84410
84686
  * `ts` is just a wall-clock stamp for display — never used for replay/sorting. */
84411
84687
  recordEvent(agentId, channel, thread, ev) {
84688
+ if (ev.kind === "tool") {
84689
+ if (ev.op === "insert") this.store.insertToolCall({
84690
+ channel,
84691
+ thread,
84692
+ ts: String(Date.now()),
84693
+ sender: agentId,
84694
+ toolCallId: ev.toolCallId,
84695
+ title: ev.text,
84696
+ body: ev.body
84697
+ });
84698
+ else this.store.updateToolCall(channel, thread, ev.toolCallId, {
84699
+ title: ev.text,
84700
+ body: ev.body
84701
+ });
84702
+ return;
84703
+ }
84412
84704
  this.store.appendTranscript({
84413
84705
  channel,
84414
84706
  thread,