@agentconnect.md/daemon 1.0.0-rc.64 → 1.0.0-rc.66

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
@@ -13227,7 +13227,9 @@ const SessionListItem = object({
13227
13227
  status: string().optional(),
13228
13228
  lastActivityAt: string().optional(),
13229
13229
  usage: SessionUsage.optional(),
13230
- triggeredBy: string().optional()
13230
+ triggeredBy: string().optional(),
13231
+ channelName: string().optional(),
13232
+ triggeredByName: string().optional()
13231
13233
  });
13232
13234
  /** C→D REQ: list the daemon's sessions (optionally just one agent's). */
13233
13235
  const SessionListReq = object({ agentId: string().uuid().optional() });
@@ -13237,6 +13239,7 @@ const SessionListPage = object({ sessions: array(SessionListItem) });
13237
13239
  const SessionMessage = object({
13238
13240
  seq: number().int(),
13239
13241
  sender: string(),
13242
+ senderName: string().optional(),
13240
13243
  ts: string(),
13241
13244
  kind: string(),
13242
13245
  text: string()
@@ -23761,7 +23764,12 @@ var LocalStore = class {
23761
23764
  CREATE TABLE IF NOT EXISTS sessions (
23762
23765
  key TEXT PRIMARY KEY, agentId TEXT, platform TEXT, channel TEXT, thread TEXT,
23763
23766
  acpSessionId TEXT, state TEXT, lastDeliveredTs TEXT, updatedAt INTEGER,
23764
- usage TEXT, muted INTEGER
23767
+ usage TEXT, muted INTEGER, triggeredBy TEXT
23768
+ );
23769
+ -- Platform id → human display name (Slack channel/user names, daemon-resolved
23770
+ -- and cached here so session read-back can label ids without a Slack call).
23771
+ CREATE TABLE IF NOT EXISTS display_names (
23772
+ id TEXT PRIMARY KEY, name TEXT NOT NULL, updatedAt INTEGER
23765
23773
  );
23766
23774
  CREATE TABLE IF NOT EXISTS transcript (
23767
23775
  seq INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -23780,14 +23788,15 @@ var LocalStore = class {
23780
23788
  `);
23781
23789
  this.migrateSessionUsage();
23782
23790
  }
23783
- /** Add the `sessions.usage` (JSON token accounting) and `sessions.muted` (`!stop`
23784
- * thread mute) columns to a pre-existing DB. CREATE TABLE IF NOT EXISTS above adds
23785
- * them for fresh DBs but never alters an existing table, so upgrade in place.
23786
- * No-op once the columns are present. */
23791
+ /** Add the `sessions.usage` (JSON token accounting), `sessions.muted` (`!stop`
23792
+ * thread mute) and `sessions.triggeredBy` columns to a pre-existing DB. CREATE
23793
+ * TABLE IF NOT EXISTS above adds them for fresh DBs but never alters an existing
23794
+ * table, so upgrade in place. No-op once the columns are present. */
23787
23795
  migrateSessionUsage() {
23788
23796
  const cols = this.db.prepare("PRAGMA table_info(sessions)").all();
23789
23797
  if (!cols.some((c) => c.name === "usage")) this.db.exec("ALTER TABLE sessions ADD COLUMN usage TEXT");
23790
23798
  if (!cols.some((c) => c.name === "muted")) this.db.exec("ALTER TABLE sessions ADD COLUMN muted INTEGER");
23799
+ if (!cols.some((c) => c.name === "triggeredBy")) this.db.exec("ALTER TABLE sessions ADD COLUMN triggeredBy TEXT");
23791
23800
  }
23792
23801
  /**
23793
23802
  * Pre-`kind` transcript tables keyed on (channel, thread, ts) hold only conversational
@@ -23837,11 +23846,12 @@ var LocalStore = class {
23837
23846
  };
23838
23847
  }
23839
23848
  upsertSession(rec) {
23840
- this.db.prepare(`INSERT INTO sessions (key, agentId, platform, channel, thread, acpSessionId, state, lastDeliveredTs, updatedAt)
23841
- VALUES (@key, @agentId, @platform, @channel, @thread, @acpSessionId, @state, @lastDeliveredTs, @updatedAt)
23849
+ this.db.prepare(`INSERT INTO sessions (key, agentId, platform, channel, thread, acpSessionId, state, lastDeliveredTs, updatedAt, triggeredBy)
23850
+ VALUES (@key, @agentId, @platform, @channel, @thread, @acpSessionId, @state, @lastDeliveredTs, @updatedAt, @triggeredBy)
23842
23851
  ON CONFLICT(key) DO UPDATE SET
23843
23852
  acpSessionId=excluded.acpSessionId, state=excluded.state,
23844
- lastDeliveredTs=excluded.lastDeliveredTs, updatedAt=excluded.updatedAt`).run({
23853
+ lastDeliveredTs=excluded.lastDeliveredTs, updatedAt=excluded.updatedAt,
23854
+ triggeredBy=COALESCE(sessions.triggeredBy, excluded.triggeredBy)`).run({
23845
23855
  key: rec.key,
23846
23856
  agentId: rec.agentId,
23847
23857
  platform: rec.platform,
@@ -23850,7 +23860,8 @@ var LocalStore = class {
23850
23860
  acpSessionId: rec.acpSessionId,
23851
23861
  state: rec.state,
23852
23862
  lastDeliveredTs: rec.lastDeliveredTs,
23853
- updatedAt: rec.updatedAt
23863
+ updatedAt: rec.updatedAt,
23864
+ triggeredBy: rec.triggeredBy ?? null
23854
23865
  });
23855
23866
  }
23856
23867
  /** Targeted state transition for an existing session (§7.3), stamping `updatedAt`
@@ -23947,6 +23958,22 @@ var LocalStore = class {
23947
23958
  activeSessionCountSince(channel, thread, sinceTs) {
23948
23959
  return this.db.prepare("SELECT COUNT(*) AS n FROM sessions WHERE channel = ? AND thread = ? AND state != 'closed' AND updatedAt >= ?").get(channel, thread, sinceTs)?.n ?? 0;
23949
23960
  }
23961
+ /** Cache a platform id's human display name (channel or user; Slack ids don't
23962
+ * collide across the two). Latest-wins — renames overwrite. */
23963
+ setDisplayName(id, name, updatedAt) {
23964
+ this.db.prepare(`INSERT INTO display_names (id, name, updatedAt) VALUES (?, ?, ?)
23965
+ ON CONFLICT(id) DO UPDATE SET name=excluded.name, updatedAt=excluded.updatedAt`).run(id, name, updatedAt);
23966
+ }
23967
+ /** Display names for a set of platform ids — only the ids that have one.
23968
+ * One batched `IN (…)` query, not a round-trip per id. */
23969
+ getDisplayNames(ids) {
23970
+ const out = /* @__PURE__ */ new Map();
23971
+ const unique = [...new Set(ids)];
23972
+ if (unique.length === 0) return out;
23973
+ const rows = this.db.prepare(`SELECT id, name FROM display_names WHERE id IN (${unique.map(() => "?").join(",")})`).all(...unique);
23974
+ for (const r of rows) if (r.name) out.set(r.id, r.name);
23975
+ return out;
23976
+ }
23950
23977
  getCpRouting() {
23951
23978
  return this.db.prepare("SELECT routingEpoch, assignments, globalRules FROM cp_routing WHERE id = 1").get();
23952
23979
  }
@@ -24067,7 +24094,8 @@ var SessionManager = class {
24067
24094
  acpSessionId,
24068
24095
  state: "idle",
24069
24096
  lastDeliveredTs: null,
24070
- updatedAt: Date.now()
24097
+ updatedAt: Date.now(),
24098
+ triggeredBy: msg.sender.id
24071
24099
  };
24072
24100
  this.deps.store.upsertSession(rec);
24073
24101
  } else if (host.hasSession?.(rec.acpSessionId) === false) {
@@ -80959,6 +80987,74 @@ var SlackConnection = class {
80959
80987
  }
80960
80988
  };
80961
80989
  //#endregion
80990
+ //#region src/slack/name-resolver.ts
80991
+ /**
80992
+ * Resolves Slack ids (channel "C…", user "U…/W…") to human display names off the
80993
+ * inbound-message path and caches the results into the LocalStore `display_names`
80994
+ * table, so session read-back (`session/list` / `session/history`) can label ids
80995
+ * without ever calling Slack at read time.
80996
+ *
80997
+ * Design constraints:
80998
+ * - Fire-and-forget: never on the dispatch hot path, never throws into it.
80999
+ * - Rate-friendly: an in-memory attempt cache means each id hits the Web API at
81000
+ * most once per TTL (short TTL after a failure, long after success/no-name),
81001
+ * regardless of message volume.
81002
+ * - Names persist in SQLite, so a daemon restart keeps stale-but-useful names
81003
+ * while the cache warms back up (rename lag is bounded by OK_TTL_MS).
81004
+ */
81005
+ const OK_TTL_MS = 360 * 60 * 1e3;
81006
+ const FAIL_TTL_MS = 600 * 1e3;
81007
+ const MAX_TRACKED_IDS = 5e3;
81008
+ var SlackNameResolver = class {
81009
+ save;
81010
+ log;
81011
+ now;
81012
+ /** id → epoch ms until which we won't re-attempt a lookup. */
81013
+ nextAttemptAt = /* @__PURE__ */ new Map();
81014
+ constructor(save, log, now = Date.now) {
81015
+ this.save = save;
81016
+ this.log = log;
81017
+ this.now = now;
81018
+ }
81019
+ /** Kick off (cached) resolution for a message's channel + human sender. */
81020
+ noteMessage(conn, msg) {
81021
+ this.resolveChannel(conn, msg.channel);
81022
+ if (!msg.sender.isBot && /^[UW]/.test(msg.sender.id)) this.resolveUser(conn, msg.sender.id);
81023
+ }
81024
+ claim(id) {
81025
+ const at = this.nextAttemptAt.get(id);
81026
+ if (at !== void 0 && this.now() < at) return false;
81027
+ this.nextAttemptAt.delete(id);
81028
+ this.nextAttemptAt.set(id, this.now() + OK_TTL_MS);
81029
+ for (const key of this.nextAttemptAt.keys()) {
81030
+ if (this.nextAttemptAt.size <= MAX_TRACKED_IDS) break;
81031
+ this.nextAttemptAt.delete(key);
81032
+ }
81033
+ return true;
81034
+ }
81035
+ async resolveChannel(conn, channel) {
81036
+ if (!this.claim(channel)) return;
81037
+ try {
81038
+ const info = await conn.getChannelInfo(channel);
81039
+ if (info.name) this.save(channel, info.name);
81040
+ } catch (err) {
81041
+ this.nextAttemptAt.set(channel, this.now() + FAIL_TTL_MS);
81042
+ this.log?.debug(`slack: name lookup failed for channel ${channel}: ${err.message}`);
81043
+ }
81044
+ }
81045
+ async resolveUser(conn, user) {
81046
+ if (!this.claim(user)) return;
81047
+ try {
81048
+ const p = await conn.getUserProfile(user);
81049
+ const name = p.realName || p.name;
81050
+ if (name) this.save(user, name);
81051
+ } catch (err) {
81052
+ this.nextAttemptAt.set(user, this.now() + FAIL_TTL_MS);
81053
+ this.log?.debug(`slack: name lookup failed for user ${user}: ${err.message}`);
81054
+ }
81055
+ }
81056
+ };
81057
+ //#endregion
80962
81058
  //#region src/slack/formatter.ts
80963
81059
  /**
80964
81060
  * Last-mile Slack formatting (§9.1 / §9.3). Two pure helpers:
@@ -82711,8 +82807,12 @@ function parseUsage(raw) {
82711
82807
  function createSessionReader(store) {
82712
82808
  return {
82713
82809
  list(req) {
82714
- return { sessions: store.listSessions(req.agentId).map((r) => {
82810
+ const rows = store.listSessions(req.agentId);
82811
+ const names = store.getDisplayNames(rows.flatMap((r) => [r.channel, ...r.triggeredBy ? [r.triggeredBy] : []]));
82812
+ return { sessions: rows.map((r) => {
82715
82813
  const usage = parseUsage(r.usage);
82814
+ const channelName = names.get(r.channel);
82815
+ const triggeredByName = r.triggeredBy ? names.get(r.triggeredBy) : void 0;
82716
82816
  return {
82717
82817
  sessionId: r.acpSessionId,
82718
82818
  sessionKey: {
@@ -82723,7 +82823,10 @@ function createSessionReader(store) {
82723
82823
  agentId: r.agentId,
82724
82824
  status: r.state,
82725
82825
  lastActivityAt: new Date(r.updatedAt).toISOString(),
82726
- ...usage ? { usage } : {}
82826
+ ...usage ? { usage } : {},
82827
+ ...r.triggeredBy ? { triggeredBy: r.triggeredBy } : {},
82828
+ ...channelName ? { channelName } : {},
82829
+ ...triggeredByName ? { triggeredByName } : {}
82727
82830
  };
82728
82831
  }) };
82729
82832
  },
@@ -82735,13 +82838,19 @@ function createSessionReader(store) {
82735
82838
  };
82736
82839
  const before = req.cursor != null && Number.isFinite(Number(req.cursor)) ? Number(req.cursor) : null;
82737
82840
  const { rows, hasMore } = store.transcriptPage(rec.channel, rec.thread, before, req.limit);
82738
- const messages = rows.slice().reverse().map((r) => ({
82739
- seq: r.seq,
82740
- sender: r.sender,
82741
- ts: r.ts,
82742
- kind: r.kind,
82743
- text: r.text
82744
- }));
82841
+ const ordered = rows.slice().reverse();
82842
+ const names = store.getDisplayNames(ordered.map((r) => r.sender));
82843
+ const messages = ordered.map((r) => {
82844
+ const senderName = names.get(r.sender);
82845
+ return {
82846
+ seq: r.seq,
82847
+ sender: r.sender,
82848
+ ...senderName ? { senderName } : {},
82849
+ ts: r.ts,
82850
+ kind: r.kind,
82851
+ text: r.text
82852
+ };
82853
+ });
82745
82854
  return {
82746
82855
  sessionId: req.sessionId,
82747
82856
  messages,
@@ -83178,6 +83287,7 @@ var Daemon = class Daemon {
83178
83287
  fileAgents = /* @__PURE__ */ new Map();
83179
83288
  hosts = /* @__PURE__ */ new Map();
83180
83289
  connections = [];
83290
+ nameResolver;
83181
83291
  scheduler;
83182
83292
  sessions;
83183
83293
  connByIntegration = /* @__PURE__ */ new Map();
@@ -83247,6 +83357,7 @@ var Daemon = class Daemon {
83247
83357
  const skipped = Object.keys(resolvedRuntimes).filter((id) => !this.runtimes[id]);
83248
83358
  if (skipped.length) this.log.info(`runtimes not installed (skipped): ${skipped.join(", ")}`);
83249
83359
  this.store = new LocalStore(statePath(root));
83360
+ this.nameResolver = new SlackNameResolver((id, name) => this.store.setDisplayName(id, name, Date.now()), this.log);
83250
83361
  this.cpRouting = new CpRoutingLayer({
83251
83362
  load: () => {
83252
83363
  const row = this.store.getCpRouting();
@@ -83321,7 +83432,10 @@ var Daemon = class Daemon {
83321
83432
  const conn = new SlackConnection({
83322
83433
  group,
83323
83434
  newTraceId: () => randomUUID(),
83324
- onMessage: (msg) => this.onInbound(msg),
83435
+ onMessage: (msg) => {
83436
+ this.nameResolver?.noteMessage(conn, msg);
83437
+ this.onInbound(msg);
83438
+ },
83325
83439
  onChannelsChanged: () => void this.refreshChannels(conn),
83326
83440
  log: this.log,
83327
83441
  boltDebug: cfg.logging.level === "debug" || cfg.logging.level === "trace"
@@ -83495,7 +83609,10 @@ var Daemon = class Daemon {
83495
83609
  const conn = new SlackConnection({
83496
83610
  group,
83497
83611
  newTraceId: () => randomUUID(),
83498
- onMessage: (msg) => this.onInbound(msg),
83612
+ onMessage: (msg) => {
83613
+ this.nameResolver?.noteMessage(conn, msg);
83614
+ this.onInbound(msg);
83615
+ },
83499
83616
  onChannelsChanged: () => void this.refreshChannels(conn),
83500
83617
  log: this.log,
83501
83618
  boltDebug: this.cfg.logging.level === "debug" || this.cfg.logging.level === "trace"
@@ -83526,6 +83643,7 @@ var Daemon = class Daemon {
83526
83643
  try {
83527
83644
  const channels = await conn.listBotChannels();
83528
83645
  if (!channels) return;
83646
+ for (const c of channels) if (c.name) this.store.setDisplayName(c.id, c.name, Date.now());
83529
83647
  for (const [integrationId, c] of this.connByIntegration) {
83530
83648
  if (c !== conn) continue;
83531
83649
  this.channelSnapshots.set(integrationId, channels);