@agentconnect.md/daemon 1.0.0-rc.34 → 1.0.0-rc.36

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
@@ -23240,19 +23240,46 @@ var LocalStore = class {
23240
23240
  mkdirSync(dirname(dbPath), { recursive: true });
23241
23241
  this.db = new DatabaseSync(dbPath);
23242
23242
  this.db.exec("PRAGMA journal_mode = WAL");
23243
+ this.migrateTranscript();
23243
23244
  this.db.exec(`
23244
23245
  CREATE TABLE IF NOT EXISTS sessions (
23245
23246
  key TEXT PRIMARY KEY, agentId TEXT, platform TEXT, channel TEXT, thread TEXT,
23246
23247
  acpSessionId TEXT, state TEXT, lastDeliveredTs TEXT, updatedAt INTEGER
23247
23248
  );
23248
23249
  CREATE TABLE IF NOT EXISTS transcript (
23249
- channel TEXT, thread TEXT, ts TEXT, sender TEXT, text TEXT,
23250
- PRIMARY KEY (channel, thread, ts)
23250
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
23251
+ channel TEXT NOT NULL, thread TEXT NOT NULL, ts TEXT,
23252
+ sender TEXT NOT NULL, kind TEXT NOT NULL, text TEXT NOT NULL
23251
23253
  );
23254
+ CREATE INDEX IF NOT EXISTS transcript_thread_seq ON transcript (channel, thread, seq);
23255
+ -- Dedup conversational rows by platform ts (double-fired inbound / redelivery);
23256
+ -- internal events have no platform ts and are intentionally never deduped here.
23257
+ CREATE UNIQUE INDEX IF NOT EXISTS transcript_text_ts
23258
+ ON transcript (channel, thread, ts) WHERE kind = 'text';
23252
23259
  CREATE TABLE IF NOT EXISTS cp_routing (
23253
23260
  id INTEGER PRIMARY KEY CHECK (id = 1),
23254
23261
  routingEpoch INTEGER, assignments TEXT, globalRules TEXT
23255
23262
  );
23263
+ `);
23264
+ }
23265
+ /**
23266
+ * Pre-`kind` transcript tables keyed on (channel, thread, ts) hold only conversational
23267
+ * text. Rebuild them onto the `seq`/`kind` schema, tagging legacy rows `text`. No-op on
23268
+ * a fresh DB (CREATE handles it) or an already-migrated one.
23269
+ */
23270
+ migrateTranscript() {
23271
+ const cols = this.db.prepare("PRAGMA table_info(transcript)").all();
23272
+ if (cols.length === 0 || cols.some((c) => c.name === "kind")) return;
23273
+ this.db.exec(`
23274
+ ALTER TABLE transcript RENAME TO transcript_legacy;
23275
+ CREATE TABLE transcript (
23276
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
23277
+ channel TEXT NOT NULL, thread TEXT NOT NULL, ts TEXT,
23278
+ sender TEXT NOT NULL, kind TEXT NOT NULL, text TEXT NOT NULL
23279
+ );
23280
+ INSERT INTO transcript (channel, thread, ts, sender, kind, text)
23281
+ SELECT channel, thread, ts, sender, 'text', text FROM transcript_legacy ORDER BY ts ASC;
23282
+ DROP TABLE transcript_legacy;
23256
23283
  `);
23257
23284
  }
23258
23285
  getSession(key) {
@@ -23266,11 +23293,20 @@ var LocalStore = class {
23266
23293
  lastDeliveredTs=excluded.lastDeliveredTs, updatedAt=excluded.updatedAt`).run(rec);
23267
23294
  }
23268
23295
  appendTranscript(e) {
23269
- this.db.prepare("INSERT OR IGNORE INTO transcript (channel, thread, ts, sender, text) VALUES (@channel, @thread, @ts, @sender, @text)").run(e);
23296
+ this.db.prepare("INSERT OR IGNORE INTO transcript (channel, thread, ts, sender, kind, text) VALUES (@channel, @thread, @ts, @sender, @kind, @text)").run(e);
23270
23297
  }
23298
+ /**
23299
+ * §8.5 cross-agent catch-up: conversational (`text`) rows only — tool/reasoning rows
23300
+ * are audit/UI data and must never be replayed back into an agent's prompt. Ordered by
23301
+ * platform `ts` (every text row carries one), compared against the session marker.
23302
+ */
23271
23303
  transcriptSince(channel, thread, sinceTs) {
23272
- if (sinceTs === null) return this.db.prepare("SELECT * FROM transcript WHERE channel = ? AND thread = ? ORDER BY ts ASC").all(channel, thread);
23273
- return this.db.prepare("SELECT * FROM transcript WHERE channel = ? AND thread = ? AND ts > ? ORDER BY ts ASC").all(channel, thread, sinceTs);
23304
+ if (sinceTs === null) return this.db.prepare("SELECT * FROM transcript WHERE channel = ? AND thread = ? AND kind = 'text' ORDER BY ts ASC").all(channel, thread);
23305
+ return this.db.prepare("SELECT * FROM transcript WHERE channel = ? AND thread = ? AND kind = 'text' AND ts > ? ORDER BY ts ASC").all(channel, thread, sinceTs);
23306
+ }
23307
+ /** Full activity log for a thread (all kinds), in insertion order — for the Web UI. */
23308
+ threadTranscript(channel, thread) {
23309
+ return this.db.prepare("SELECT * FROM transcript WHERE channel = ? AND thread = ? ORDER BY seq ASC").all(channel, thread);
23274
23310
  }
23275
23311
  openSessionAgents(channel, thread) {
23276
23312
  return this.db.prepare("SELECT agentId FROM sessions WHERE channel = ? AND thread = ? AND state != 'closed'").all(channel, thread).map((r) => r.agentId);
@@ -23379,6 +23415,7 @@ var SessionManager = class {
23379
23415
  thread,
23380
23416
  ts,
23381
23417
  sender: msg.sender.id,
23418
+ kind: "text",
23382
23419
  text: mention ? `${msg.text}\n${mention}`.trim() : msg.text
23383
23420
  });
23384
23421
  let rec = this.deps.store.getSession(key);
@@ -23435,6 +23472,7 @@ var SessionManager = class {
23435
23472
  thread,
23436
23473
  ts: h.ts,
23437
23474
  sender: h.sender,
23475
+ kind: "text",
23438
23476
  text: h.text
23439
23477
  });
23440
23478
  }
@@ -23487,6 +23525,58 @@ function transcriptCoords(msg) {
23487
23525
  };
23488
23526
  }
23489
23527
  //#endregion
23528
+ //#region src/session/transcript-recorder.ts
23529
+ /**
23530
+ * Turns the raw ACP `session/update` stream into the *full* activity log — tool calls
23531
+ * and reasoning — independent of the agent's Slack `output.mode`. (Output mode only
23532
+ * decides what reaches the platform; the transcript records everything.)
23533
+ *
23534
+ * It deliberately does NOT emit `text`: conversational rows are recorded at the
23535
+ * platform-send boundary instead, where the real message `ts` is known (so they sort
23536
+ * against the §8.5 replay marker). Reasoning chunks are coalesced into one block per
23537
+ * run; each tool invocation is recorded once (keyed by toolCallId, so the burst of
23538
+ * `tool_call_update`s for one call collapses to a single row).
23539
+ */
23540
+ var TranscriptRecorder = class {
23541
+ thought = "";
23542
+ seenTools = /* @__PURE__ */ new Set();
23543
+ onUpdate(update) {
23544
+ switch (update.sessionUpdate) {
23545
+ case "agent_thought_chunk": {
23546
+ const content = update.content;
23547
+ this.thought += content?.text ?? "";
23548
+ return [];
23549
+ }
23550
+ case "tool_call":
23551
+ case "tool_call_update": {
23552
+ const flushed = this.flushThought();
23553
+ const u = update;
23554
+ const id = u.toolCallId ?? "";
23555
+ if (id && this.seenTools.has(id)) return flushed;
23556
+ if (id) this.seenTools.add(id);
23557
+ return [...flushed, {
23558
+ kind: "tool",
23559
+ text: u.title ?? (id || "tool")
23560
+ }];
23561
+ }
23562
+ case "agent_message_chunk": return this.flushThought();
23563
+ default: return [];
23564
+ }
23565
+ }
23566
+ /** Flush any trailing reasoning when the turn ends. */
23567
+ onFinal() {
23568
+ return this.flushThought();
23569
+ }
23570
+ flushThought() {
23571
+ const text = this.thought.trim();
23572
+ this.thought = "";
23573
+ return text ? [{
23574
+ kind: "reasoning",
23575
+ text
23576
+ }] : [];
23577
+ }
23578
+ };
23579
+ //#endregion
23490
23580
  //#region src/mcp/ipc.ts
23491
23581
  /** Frame a message for the wire: compact JSON + a single trailing newline. */
23492
23582
  function encodeFrame(msg) {
@@ -80400,7 +80490,7 @@ var OutputConverger = class {
80400
80490
  };
80401
80491
  if (this.mode === "low") return [...this.flush(), clear];
80402
80492
  const footer = link ? [{
80403
- kind: "post",
80493
+ kind: "notice",
80404
80494
  text: `:white_check_mark: done — <${link}|details>`
80405
80495
  }] : [];
80406
80496
  return [
@@ -81977,6 +82067,7 @@ var Daemon = class {
81977
82067
  thread: thread ?? ctx.thread,
81978
82068
  ts,
81979
82069
  sender: ctx.agentId,
82070
+ kind: "text",
81980
82071
  text
81981
82072
  }),
81982
82073
  maxAttachmentBytes: cfg.limits.maxAttachmentBytes
@@ -82272,6 +82363,7 @@ var Daemon = class {
82272
82363
  thread,
82273
82364
  ts,
82274
82365
  sender: msg.sender.id,
82366
+ kind: "text",
82275
82367
  text: msg.text
82276
82368
  });
82277
82369
  this.log.debug(`transcript: recorded unrouted msg ch=${msg.channel} thread=${thread} ts=${ts} (live session)`);
@@ -82383,6 +82475,7 @@ var Daemon = class {
82383
82475
  }
82384
82476
  async dispatch(agentId, msg, integrationId) {
82385
82477
  const conv = new OutputConverger(this.agents.get(agentId).output.mode);
82478
+ const rec = new TranscriptRecorder();
82386
82479
  const replyConn = this.replyConnFor(agentId, integrationId);
82387
82480
  const wasRunning = this.hostStarts.has(agentId);
82388
82481
  const statusThread = msg.thread ?? msg.msgId;
@@ -82390,6 +82483,8 @@ var Daemon = class {
82390
82483
  const { sessionId, blocks } = await this.sessions.handle(agentId, msg);
82391
82484
  const p = {
82392
82485
  conv,
82486
+ rec,
82487
+ agentId,
82393
82488
  channel: msg.channel,
82394
82489
  thread: msg.thread,
82395
82490
  statusThread,
@@ -82404,6 +82499,7 @@ var Daemon = class {
82404
82499
  this.clearIdle(p);
82405
82500
  const link = this.cfg.webAppUrl ? `${this.cfg.webAppUrl.replace(/\/$/, "")}/sessions/${sessionId}` : void 0;
82406
82501
  for (const action of conv.onFinal(link)) this.enqueueApply(p, action);
82502
+ for (const ev of rec.onFinal()) this.recordEvent(agentId, msg.channel, statusThread, ev);
82407
82503
  await p.applyChain;
82408
82504
  } finally {
82409
82505
  this.clearIdle(p);
@@ -82414,7 +82510,10 @@ var Daemon = class {
82414
82510
  /**
82415
82511
  * Apply one converger action against the session's Slack connection:
82416
82512
  * - set-status → assistant.threads.setStatus (best-effort; '' clears)
82417
- * - post → a new thread message
82513
+ * - post → a new thread message; ALSO recorded to the transcript as the
82514
+ * agent's reply text (sender = agentId), so other agents replaying the thread
82515
+ * see what it said. Keyed on `statusThread` (the §8.5 replay key).
82516
+ * - notice → posted to the thread but NOT recorded (system chrome, e.g. footer).
82418
82517
  * - progress / plan → the single in-place message of that kind, posted once
82419
82518
  * then chat.update-ed (§9.1 就地更新). The first post's ts is remembered on `p`.
82420
82519
  */
@@ -82425,7 +82524,19 @@ var Daemon = class {
82425
82524
  case "set-status":
82426
82525
  if (p.statusThread) await conn.setStatus(p.channel, p.statusThread, action.text, action.loadingMessages);
82427
82526
  return;
82428
- case "post":
82527
+ case "post": {
82528
+ const ts = await conn.postMessage(p.channel, action.text, p.thread);
82529
+ this.store.appendTranscript({
82530
+ channel: p.channel,
82531
+ thread: p.statusThread,
82532
+ ts: ts ?? `local-${Date.now()}`,
82533
+ sender: p.agentId,
82534
+ kind: "text",
82535
+ text: action.text
82536
+ });
82537
+ return;
82538
+ }
82539
+ case "notice":
82429
82540
  await conn.postMessage(p.channel, action.text, p.thread);
82430
82541
  return;
82431
82542
  case "progress":
@@ -82469,6 +82580,19 @@ var Daemon = class {
82469
82580
  if (!p) return;
82470
82581
  for (const action of p.conv.onUpdate(update)) this.enqueueApply(p, action);
82471
82582
  this.armIdle(p);
82583
+ for (const ev of p.rec.onUpdate(update)) this.recordEvent(p.agentId, p.channel, p.statusThread, ev);
82584
+ }
82585
+ /** Persist one internal activity event (tool/reasoning). Ordered by row `seq`, so its
82586
+ * `ts` is just a wall-clock stamp for display — never used for replay/sorting. */
82587
+ recordEvent(agentId, channel, thread, ev) {
82588
+ this.store.appendTranscript({
82589
+ channel,
82590
+ thread,
82591
+ ts: String(Date.now()),
82592
+ sender: agentId,
82593
+ kind: ev.kind,
82594
+ text: ev.text
82595
+ });
82472
82596
  }
82473
82597
  async ensureHostAsync(agentId) {
82474
82598
  const host = this.ensureHost(agentId, this.cfg);