@agentconnect.md/daemon 1.0.0-rc.65 → 1.0.0-rc.67

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()
@@ -13319,6 +13322,50 @@ const WorkspaceReadContent = object({
13319
13322
  nextOffset: number().int().nonnegative().optional(),
13320
13323
  truncated: boolean().optional()
13321
13324
  });
13325
+ /**
13326
+ * Agent workspace git ops (C→D REQ → REP) — the console's on-demand controls for
13327
+ * a git-repo workspace. Like list/read these run daemon-local; the CP proxies the
13328
+ * outcome and stores nothing.
13329
+ *
13330
+ * These are DATA-oriented: a non-repo workspace (`isRepo:false`), a dirty tree,
13331
+ * or a pull that can't fast-forward (offline, diverged, local edits) all come
13332
+ * back as a normal REP — NOT an `error` frame. Only an unknown agent
13333
+ * (`BAD_PAYLOAD`) or an unexpected git/fs failure (`INTERNAL`) is an error.
13334
+ */
13335
+ /** C→D REQ: report `git status` of the agent's workspace (is it clean?). */
13336
+ const WorkspaceGitStatusReq = object({ agentId: string().min(1) });
13337
+ /** One changed path in a `git status`. `index`/`workingDir` are git's per-file XY
13338
+ * status chars (' ', 'M', 'A', 'D', 'R', 'C', 'U', '?', …). */
13339
+ const WorkspaceGitFile = object({
13340
+ path: string(),
13341
+ index: string(),
13342
+ workingDir: string()
13343
+ });
13344
+ /** D→C REP (corr = the req id): the working-tree status. */
13345
+ const WorkspaceGitStatus = object({
13346
+ agentId: string(),
13347
+ isRepo: boolean(),
13348
+ clean: boolean(),
13349
+ branch: string().optional(),
13350
+ tracking: string().optional(),
13351
+ ahead: number().int().nonnegative().optional(),
13352
+ behind: number().int().nonnegative().optional(),
13353
+ files: array(WorkspaceGitFile).optional(),
13354
+ truncated: boolean().optional()
13355
+ });
13356
+ /** C→D REQ: force a fast-forward-only `git pull` in the agent's workspace now. */
13357
+ const WorkspaceGitPullReq = object({ agentId: string().min(1) });
13358
+ /** D→C REP (corr = the req id): the pull outcome. A failed pull is DATA
13359
+ * (`ok:false` + `detail`), not an error frame (see the section docblock). */
13360
+ const WorkspaceGitPullResult = object({
13361
+ agentId: string(),
13362
+ isRepo: boolean(),
13363
+ ok: boolean(),
13364
+ detail: string().optional(),
13365
+ changed: number().int().nonnegative().optional(),
13366
+ insertions: number().int().nonnegative().optional(),
13367
+ deletions: number().int().nonnegative().optional()
13368
+ });
13322
13369
  //#endregion
13323
13370
  //#region ../protocol/dist/frames/telemetry.js
13324
13371
  /**
@@ -13443,6 +13490,10 @@ const FRAME_SCHEMAS = {
13443
13490
  "workspace/list/page": WorkspaceListPage,
13444
13491
  "workspace/read": WorkspaceReadReq,
13445
13492
  "workspace/read/content": WorkspaceReadContent,
13493
+ "workspace/gitstatus": WorkspaceGitStatusReq,
13494
+ "workspace/gitstatus/result": WorkspaceGitStatus,
13495
+ "workspace/gitpull": WorkspaceGitPullReq,
13496
+ "workspace/gitpull/result": WorkspaceGitPullResult,
13446
13497
  "config/push": object({ keys: record(string(), unknown()) }),
13447
13498
  "daemon/restart": object({
13448
13499
  reason: string(),
@@ -13542,6 +13593,10 @@ discriminatedUnion("type", [
13542
13593
  frame("workspace/list/page", FRAME_SCHEMAS["workspace/list/page"]),
13543
13594
  frame("workspace/read", FRAME_SCHEMAS["workspace/read"]),
13544
13595
  frame("workspace/read/content", FRAME_SCHEMAS["workspace/read/content"]),
13596
+ frame("workspace/gitstatus", FRAME_SCHEMAS["workspace/gitstatus"]),
13597
+ frame("workspace/gitstatus/result", FRAME_SCHEMAS["workspace/gitstatus/result"]),
13598
+ frame("workspace/gitpull", FRAME_SCHEMAS["workspace/gitpull"]),
13599
+ frame("workspace/gitpull/result", FRAME_SCHEMAS["workspace/gitpull/result"]),
13545
13600
  frame("config/push", FRAME_SCHEMAS["config/push"]),
13546
13601
  frame("daemon/restart", FRAME_SCHEMAS["daemon/restart"]),
13547
13602
  frame("daemon/upgrade", FRAME_SCHEMAS["daemon/upgrade"]),
@@ -13668,7 +13723,7 @@ function isFrame(type) {
13668
13723
  }
13669
13724
  //#endregion
13670
13725
  //#region src/workspace/workspace-manager.ts
13671
- const PULL_TIMEOUT_MS = 4500;
13726
+ const PULL_TIMEOUT_MS$1 = 4500;
13672
13727
  const cloneInFlight = /* @__PURE__ */ new Map();
13673
13728
  async function prepareWorkspace(agent) {
13674
13729
  const cwd = agent.workspace.path;
@@ -13683,7 +13738,7 @@ async function prepareWorkspace(agent) {
13683
13738
  return cwd;
13684
13739
  }
13685
13740
  if (agent.workspace.pullOnNewSession) try {
13686
- await Promise.race([simpleGit(cwd).pull(["--ff-only"]), new Promise((_, rej) => setTimeout(() => rej(/* @__PURE__ */ new Error("pull timeout")), PULL_TIMEOUT_MS))]);
13741
+ await Promise.race([simpleGit(cwd).pull(["--ff-only"]), new Promise((_, rej) => setTimeout(() => rej(/* @__PURE__ */ new Error("pull timeout")), PULL_TIMEOUT_MS$1))]);
13687
13742
  } catch {}
13688
13743
  return cwd;
13689
13744
  }
@@ -23761,7 +23816,12 @@ var LocalStore = class {
23761
23816
  CREATE TABLE IF NOT EXISTS sessions (
23762
23817
  key TEXT PRIMARY KEY, agentId TEXT, platform TEXT, channel TEXT, thread TEXT,
23763
23818
  acpSessionId TEXT, state TEXT, lastDeliveredTs TEXT, updatedAt INTEGER,
23764
- usage TEXT, muted INTEGER
23819
+ usage TEXT, muted INTEGER, triggeredBy TEXT
23820
+ );
23821
+ -- Platform id → human display name (Slack channel/user names, daemon-resolved
23822
+ -- and cached here so session read-back can label ids without a Slack call).
23823
+ CREATE TABLE IF NOT EXISTS display_names (
23824
+ id TEXT PRIMARY KEY, name TEXT NOT NULL, updatedAt INTEGER
23765
23825
  );
23766
23826
  CREATE TABLE IF NOT EXISTS transcript (
23767
23827
  seq INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -23780,14 +23840,15 @@ var LocalStore = class {
23780
23840
  `);
23781
23841
  this.migrateSessionUsage();
23782
23842
  }
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. */
23843
+ /** Add the `sessions.usage` (JSON token accounting), `sessions.muted` (`!stop`
23844
+ * thread mute) and `sessions.triggeredBy` columns to a pre-existing DB. CREATE
23845
+ * TABLE IF NOT EXISTS above adds them for fresh DBs but never alters an existing
23846
+ * table, so upgrade in place. No-op once the columns are present. */
23787
23847
  migrateSessionUsage() {
23788
23848
  const cols = this.db.prepare("PRAGMA table_info(sessions)").all();
23789
23849
  if (!cols.some((c) => c.name === "usage")) this.db.exec("ALTER TABLE sessions ADD COLUMN usage TEXT");
23790
23850
  if (!cols.some((c) => c.name === "muted")) this.db.exec("ALTER TABLE sessions ADD COLUMN muted INTEGER");
23851
+ if (!cols.some((c) => c.name === "triggeredBy")) this.db.exec("ALTER TABLE sessions ADD COLUMN triggeredBy TEXT");
23791
23852
  }
23792
23853
  /**
23793
23854
  * Pre-`kind` transcript tables keyed on (channel, thread, ts) hold only conversational
@@ -23837,11 +23898,12 @@ var LocalStore = class {
23837
23898
  };
23838
23899
  }
23839
23900
  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)
23901
+ this.db.prepare(`INSERT INTO sessions (key, agentId, platform, channel, thread, acpSessionId, state, lastDeliveredTs, updatedAt, triggeredBy)
23902
+ VALUES (@key, @agentId, @platform, @channel, @thread, @acpSessionId, @state, @lastDeliveredTs, @updatedAt, @triggeredBy)
23842
23903
  ON CONFLICT(key) DO UPDATE SET
23843
23904
  acpSessionId=excluded.acpSessionId, state=excluded.state,
23844
- lastDeliveredTs=excluded.lastDeliveredTs, updatedAt=excluded.updatedAt`).run({
23905
+ lastDeliveredTs=excluded.lastDeliveredTs, updatedAt=excluded.updatedAt,
23906
+ triggeredBy=COALESCE(sessions.triggeredBy, excluded.triggeredBy)`).run({
23845
23907
  key: rec.key,
23846
23908
  agentId: rec.agentId,
23847
23909
  platform: rec.platform,
@@ -23850,7 +23912,8 @@ var LocalStore = class {
23850
23912
  acpSessionId: rec.acpSessionId,
23851
23913
  state: rec.state,
23852
23914
  lastDeliveredTs: rec.lastDeliveredTs,
23853
- updatedAt: rec.updatedAt
23915
+ updatedAt: rec.updatedAt,
23916
+ triggeredBy: rec.triggeredBy ?? null
23854
23917
  });
23855
23918
  }
23856
23919
  /** Targeted state transition for an existing session (§7.3), stamping `updatedAt`
@@ -23947,6 +24010,22 @@ var LocalStore = class {
23947
24010
  activeSessionCountSince(channel, thread, sinceTs) {
23948
24011
  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
24012
  }
24013
+ /** Cache a platform id's human display name (channel or user; Slack ids don't
24014
+ * collide across the two). Latest-wins — renames overwrite. */
24015
+ setDisplayName(id, name, updatedAt) {
24016
+ this.db.prepare(`INSERT INTO display_names (id, name, updatedAt) VALUES (?, ?, ?)
24017
+ ON CONFLICT(id) DO UPDATE SET name=excluded.name, updatedAt=excluded.updatedAt`).run(id, name, updatedAt);
24018
+ }
24019
+ /** Display names for a set of platform ids — only the ids that have one.
24020
+ * One batched `IN (…)` query, not a round-trip per id. */
24021
+ getDisplayNames(ids) {
24022
+ const out = /* @__PURE__ */ new Map();
24023
+ const unique = [...new Set(ids)];
24024
+ if (unique.length === 0) return out;
24025
+ const rows = this.db.prepare(`SELECT id, name FROM display_names WHERE id IN (${unique.map(() => "?").join(",")})`).all(...unique);
24026
+ for (const r of rows) if (r.name) out.set(r.id, r.name);
24027
+ return out;
24028
+ }
23950
24029
  getCpRouting() {
23951
24030
  return this.db.prepare("SELECT routingEpoch, assignments, globalRules FROM cp_routing WHERE id = 1").get();
23952
24031
  }
@@ -24067,7 +24146,8 @@ var SessionManager = class {
24067
24146
  acpSessionId,
24068
24147
  state: "idle",
24069
24148
  lastDeliveredTs: null,
24070
- updatedAt: Date.now()
24149
+ updatedAt: Date.now(),
24150
+ triggeredBy: msg.sender.id
24071
24151
  };
24072
24152
  this.deps.store.upsertSession(rec);
24073
24153
  } else if (host.hasSession?.(rec.acpSessionId) === false) {
@@ -80959,6 +81039,74 @@ var SlackConnection = class {
80959
81039
  }
80960
81040
  };
80961
81041
  //#endregion
81042
+ //#region src/slack/name-resolver.ts
81043
+ /**
81044
+ * Resolves Slack ids (channel "C…", user "U…/W…") to human display names off the
81045
+ * inbound-message path and caches the results into the LocalStore `display_names`
81046
+ * table, so session read-back (`session/list` / `session/history`) can label ids
81047
+ * without ever calling Slack at read time.
81048
+ *
81049
+ * Design constraints:
81050
+ * - Fire-and-forget: never on the dispatch hot path, never throws into it.
81051
+ * - Rate-friendly: an in-memory attempt cache means each id hits the Web API at
81052
+ * most once per TTL (short TTL after a failure, long after success/no-name),
81053
+ * regardless of message volume.
81054
+ * - Names persist in SQLite, so a daemon restart keeps stale-but-useful names
81055
+ * while the cache warms back up (rename lag is bounded by OK_TTL_MS).
81056
+ */
81057
+ const OK_TTL_MS = 360 * 60 * 1e3;
81058
+ const FAIL_TTL_MS = 600 * 1e3;
81059
+ const MAX_TRACKED_IDS = 5e3;
81060
+ var SlackNameResolver = class {
81061
+ save;
81062
+ log;
81063
+ now;
81064
+ /** id → epoch ms until which we won't re-attempt a lookup. */
81065
+ nextAttemptAt = /* @__PURE__ */ new Map();
81066
+ constructor(save, log, now = Date.now) {
81067
+ this.save = save;
81068
+ this.log = log;
81069
+ this.now = now;
81070
+ }
81071
+ /** Kick off (cached) resolution for a message's channel + human sender. */
81072
+ noteMessage(conn, msg) {
81073
+ this.resolveChannel(conn, msg.channel);
81074
+ if (!msg.sender.isBot && /^[UW]/.test(msg.sender.id)) this.resolveUser(conn, msg.sender.id);
81075
+ }
81076
+ claim(id) {
81077
+ const at = this.nextAttemptAt.get(id);
81078
+ if (at !== void 0 && this.now() < at) return false;
81079
+ this.nextAttemptAt.delete(id);
81080
+ this.nextAttemptAt.set(id, this.now() + OK_TTL_MS);
81081
+ for (const key of this.nextAttemptAt.keys()) {
81082
+ if (this.nextAttemptAt.size <= MAX_TRACKED_IDS) break;
81083
+ this.nextAttemptAt.delete(key);
81084
+ }
81085
+ return true;
81086
+ }
81087
+ async resolveChannel(conn, channel) {
81088
+ if (!this.claim(channel)) return;
81089
+ try {
81090
+ const info = await conn.getChannelInfo(channel);
81091
+ if (info.name) this.save(channel, info.name);
81092
+ } catch (err) {
81093
+ this.nextAttemptAt.set(channel, this.now() + FAIL_TTL_MS);
81094
+ this.log?.debug(`slack: name lookup failed for channel ${channel}: ${err.message}`);
81095
+ }
81096
+ }
81097
+ async resolveUser(conn, user) {
81098
+ if (!this.claim(user)) return;
81099
+ try {
81100
+ const p = await conn.getUserProfile(user);
81101
+ const name = p.realName || p.name;
81102
+ if (name) this.save(user, name);
81103
+ } catch (err) {
81104
+ this.nextAttemptAt.set(user, this.now() + FAIL_TTL_MS);
81105
+ this.log?.debug(`slack: name lookup failed for user ${user}: ${err.message}`);
81106
+ }
81107
+ }
81108
+ };
81109
+ //#endregion
80962
81110
  //#region src/slack/formatter.ts
80963
81111
  /**
80964
81112
  * Last-mile Slack formatting (§9.1 / §9.3). Two pure helpers:
@@ -82667,6 +82815,12 @@ var CpClient = class {
82667
82815
  case "workspace/read":
82668
82816
  this.deps.workspaceRead.read(frame.payload).then((content) => this.reply(frame, "workspace/read/content", content)).catch((err) => this.workspaceError(frame.id, "workspace/read", err));
82669
82817
  return;
82818
+ case "workspace/gitstatus":
82819
+ this.deps.workspaceGit.status(frame.payload.agentId).then((status) => this.reply(frame, "workspace/gitstatus/result", status)).catch((err) => this.workspaceError(frame.id, "workspace/gitstatus", err));
82820
+ return;
82821
+ case "workspace/gitpull":
82822
+ 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));
82823
+ return;
82670
82824
  case "agent/prompt":
82671
82825
  this.sendError(frame.id, "INTERNAL", "agent/prompt not implemented", false);
82672
82826
  return;
@@ -82711,8 +82865,12 @@ function parseUsage(raw) {
82711
82865
  function createSessionReader(store) {
82712
82866
  return {
82713
82867
  list(req) {
82714
- return { sessions: store.listSessions(req.agentId).map((r) => {
82868
+ const rows = store.listSessions(req.agentId);
82869
+ const names = store.getDisplayNames(rows.flatMap((r) => [r.channel, ...r.triggeredBy ? [r.triggeredBy] : []]));
82870
+ return { sessions: rows.map((r) => {
82715
82871
  const usage = parseUsage(r.usage);
82872
+ const channelName = names.get(r.channel);
82873
+ const triggeredByName = r.triggeredBy ? names.get(r.triggeredBy) : void 0;
82716
82874
  return {
82717
82875
  sessionId: r.acpSessionId,
82718
82876
  sessionKey: {
@@ -82723,7 +82881,10 @@ function createSessionReader(store) {
82723
82881
  agentId: r.agentId,
82724
82882
  status: r.state,
82725
82883
  lastActivityAt: new Date(r.updatedAt).toISOString(),
82726
- ...usage ? { usage } : {}
82884
+ ...usage ? { usage } : {},
82885
+ ...r.triggeredBy ? { triggeredBy: r.triggeredBy } : {},
82886
+ ...channelName ? { channelName } : {},
82887
+ ...triggeredByName ? { triggeredByName } : {}
82727
82888
  };
82728
82889
  }) };
82729
82890
  },
@@ -82735,13 +82896,19 @@ function createSessionReader(store) {
82735
82896
  };
82736
82897
  const before = req.cursor != null && Number.isFinite(Number(req.cursor)) ? Number(req.cursor) : null;
82737
82898
  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
- }));
82899
+ const ordered = rows.slice().reverse();
82900
+ const names = store.getDisplayNames(ordered.map((r) => r.sender));
82901
+ const messages = ordered.map((r) => {
82902
+ const senderName = names.get(r.sender);
82903
+ return {
82904
+ seq: r.seq,
82905
+ sender: r.sender,
82906
+ ...senderName ? { senderName } : {},
82907
+ ts: r.ts,
82908
+ kind: r.kind,
82909
+ text: r.text
82910
+ };
82911
+ });
82745
82912
  return {
82746
82913
  sessionId: req.sessionId,
82747
82914
  messages,
@@ -82751,6 +82918,104 @@ function createSessionReader(store) {
82751
82918
  };
82752
82919
  }
82753
82920
  //#endregion
82921
+ //#region src/cp/workspace-git.ts
82922
+ /**
82923
+ * `WorkspaceGit` — the seam answering the CP's `workspace/gitstatus` and
82924
+ * `workspace/gitpull` REQs against an agent's local git-repo workspace. Sibling
82925
+ * to {@link WorkspaceReader}: git runs daemon-local and only the outcome is
82926
+ * proxied to the console (§1/§12), never the repo.
82927
+ *
82928
+ * DATA vs error: a from-scratch workspace (`isRepo:false`), a dirty tree, or a
82929
+ * pull that can't fast-forward (offline, diverged, would clobber local edits) are
82930
+ * all normal REPs — the caller renders them. Only an unknown agentId throws
82931
+ * ({@link WorkspaceViolationError} → `BAD_PAYLOAD`); an unexpected git/fs failure
82932
+ * on `status` propagates (→ `INTERNAL`). Git-provided text is scrubbed of the
82933
+ * absolute workspace path before it leaves the daemon (raw git errors embed host
82934
+ * paths that must not reach the CP/UI).
82935
+ */
82936
+ /** On-demand pull is interactive, so allow more headroom than the 4.5s
82937
+ * best-effort pull at session start (workspace-manager.ts). */
82938
+ const PULL_TIMEOUT_MS = 2e4;
82939
+ /** Cap the changed-file list so a huge working tree can't overflow the frame. */
82940
+ const MAX_STATUS_FILES = 500;
82941
+ function createWorkspaceGit(workspaceRootByAgent) {
82942
+ function rootFor(agentId) {
82943
+ const root = workspaceRootByAgent(agentId);
82944
+ if (!root) throw new WorkspaceViolationError(`unknown agent "${agentId}"`);
82945
+ return root;
82946
+ }
82947
+ /** A git-repo workspace has a `.git` at its root; from-scratch does not. */
82948
+ function isRepo(root) {
82949
+ return existsSync(join(root, ".git"));
82950
+ }
82951
+ /** Strip the absolute workspace path out of git-provided text so no host path
82952
+ * leaks to the CP/UI. Repo-relative paths in the message are kept. */
82953
+ function scrub(root, msg) {
82954
+ return msg.split(root).join("<workspace>").trim();
82955
+ }
82956
+ return {
82957
+ async status(agentId) {
82958
+ const root = rootFor(agentId);
82959
+ if (!isRepo(root)) return {
82960
+ agentId,
82961
+ isRepo: false,
82962
+ clean: true
82963
+ };
82964
+ const s = await simpleGit(root).status();
82965
+ const files = s.files.slice(0, MAX_STATUS_FILES).map((f) => ({
82966
+ path: f.path,
82967
+ index: f.index,
82968
+ workingDir: f.working_dir
82969
+ }));
82970
+ return {
82971
+ agentId,
82972
+ isRepo: true,
82973
+ clean: s.isClean(),
82974
+ ...s.current ? { branch: s.current } : {},
82975
+ ...s.tracking ? { tracking: s.tracking } : {},
82976
+ ahead: s.ahead,
82977
+ behind: s.behind,
82978
+ ...files.length ? { files } : {},
82979
+ ...s.files.length > files.length ? { truncated: true } : {}
82980
+ };
82981
+ },
82982
+ async pull(agentId) {
82983
+ const root = rootFor(agentId);
82984
+ if (!isRepo(root)) return {
82985
+ agentId,
82986
+ isRepo: false,
82987
+ ok: false,
82988
+ detail: "workspace is not a git checkout"
82989
+ };
82990
+ let timer;
82991
+ try {
82992
+ const res = await Promise.race([simpleGit(root).pull(["--ff-only"]), new Promise((_, rej) => {
82993
+ timer = setTimeout(() => rej(/* @__PURE__ */ new Error("pull timed out")), PULL_TIMEOUT_MS);
82994
+ })]);
82995
+ const changed = res.files.length;
82996
+ return {
82997
+ agentId,
82998
+ isRepo: true,
82999
+ ok: true,
83000
+ changed,
83001
+ insertions: res.summary.insertions,
83002
+ deletions: res.summary.deletions,
83003
+ detail: changed > 0 ? `Fast-forwarded — updated ${changed} file${changed === 1 ? "" : "s"}.` : "Already up to date."
83004
+ };
83005
+ } catch (err) {
83006
+ return {
83007
+ agentId,
83008
+ isRepo: true,
83009
+ ok: false,
83010
+ detail: scrub(root, err?.message ?? "pull failed")
83011
+ };
83012
+ } finally {
83013
+ if (timer) clearTimeout(timer);
83014
+ }
83015
+ }
83016
+ };
83017
+ }
83018
+ //#endregion
82754
83019
  //#region src/cp/cp-cron.ts
82755
83020
  /**
82756
83021
  * `CpCronRegistry` — the daemon's set of CP-pushed crons, keyed by `cronId`
@@ -83178,6 +83443,7 @@ var Daemon = class Daemon {
83178
83443
  fileAgents = /* @__PURE__ */ new Map();
83179
83444
  hosts = /* @__PURE__ */ new Map();
83180
83445
  connections = [];
83446
+ nameResolver;
83181
83447
  scheduler;
83182
83448
  sessions;
83183
83449
  connByIntegration = /* @__PURE__ */ new Map();
@@ -83247,6 +83513,7 @@ var Daemon = class Daemon {
83247
83513
  const skipped = Object.keys(resolvedRuntimes).filter((id) => !this.runtimes[id]);
83248
83514
  if (skipped.length) this.log.info(`runtimes not installed (skipped): ${skipped.join(", ")}`);
83249
83515
  this.store = new LocalStore(statePath(root));
83516
+ this.nameResolver = new SlackNameResolver((id, name) => this.store.setDisplayName(id, name, Date.now()), this.log);
83250
83517
  this.cpRouting = new CpRoutingLayer({
83251
83518
  load: () => {
83252
83519
  const row = this.store.getCpRouting();
@@ -83321,7 +83588,10 @@ var Daemon = class Daemon {
83321
83588
  const conn = new SlackConnection({
83322
83589
  group,
83323
83590
  newTraceId: () => randomUUID(),
83324
- onMessage: (msg) => this.onInbound(msg),
83591
+ onMessage: (msg) => {
83592
+ this.nameResolver?.noteMessage(conn, msg);
83593
+ this.onInbound(msg);
83594
+ },
83325
83595
  onChannelsChanged: () => void this.refreshChannels(conn),
83326
83596
  log: this.log,
83327
83597
  boltDebug: cfg.logging.level === "debug" || cfg.logging.level === "trace"
@@ -83495,7 +83765,10 @@ var Daemon = class Daemon {
83495
83765
  const conn = new SlackConnection({
83496
83766
  group,
83497
83767
  newTraceId: () => randomUUID(),
83498
- onMessage: (msg) => this.onInbound(msg),
83768
+ onMessage: (msg) => {
83769
+ this.nameResolver?.noteMessage(conn, msg);
83770
+ this.onInbound(msg);
83771
+ },
83499
83772
  onChannelsChanged: () => void this.refreshChannels(conn),
83500
83773
  log: this.log,
83501
83774
  boltDebug: this.cfg.logging.level === "debug" || this.cfg.logging.level === "trace"
@@ -83526,6 +83799,7 @@ var Daemon = class Daemon {
83526
83799
  try {
83527
83800
  const channels = await conn.listBotChannels();
83528
83801
  if (!channels) return;
83802
+ for (const c of channels) if (c.name) this.store.setDisplayName(c.id, c.name, Date.now());
83529
83803
  for (const [integrationId, c] of this.connByIntegration) {
83530
83804
  if (c !== conn) continue;
83531
83805
  this.channelSnapshots.set(integrationId, channels);
@@ -84250,6 +84524,7 @@ var Daemon = class Daemon {
84250
84524
  configApply: this.cpConfigApply(),
84251
84525
  sessionRead: createSessionReader(this.store),
84252
84526
  workspaceRead: createWorkspaceReader((id) => this.agents.get(id)?.workspace.path),
84527
+ workspaceGit: createWorkspaceGit((id) => this.agents.get(id)?.workspace.path),
84253
84528
  clock: systemClock,
84254
84529
  connect: () => ClientTransport.dial(url),
84255
84530
  log: this.log