@integrity-labs/agt-cli 0.28.56 → 0.28.58

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.
@@ -15448,15 +15448,15 @@ import {
15448
15448
  createWriteStream,
15449
15449
  existsSync as existsSync7,
15450
15450
  ftruncateSync,
15451
- mkdirSync as mkdirSync5,
15451
+ mkdirSync as mkdirSync6,
15452
15452
  openSync,
15453
- readFileSync as readFileSync8,
15453
+ readFileSync as readFileSync9,
15454
15454
  readdirSync as readdirSync3,
15455
15455
  renameSync as renameSync3,
15456
15456
  statSync as statSync2,
15457
- unlinkSync as unlinkSync4,
15457
+ unlinkSync as unlinkSync5,
15458
15458
  watch,
15459
- writeFileSync as writeFileSync6,
15459
+ writeFileSync as writeFileSync7,
15460
15460
  writeSync
15461
15461
  } from "fs";
15462
15462
  import { basename, join as join7, resolve as resolve2 } from "path";
@@ -15575,6 +15575,156 @@ function isThreadEntry(value) {
15575
15575
  return (v.involvement === "started" || v.involvement === "mentioned") && typeof v.last_seen_at === "string";
15576
15576
  }
15577
15577
 
15578
+ // src/dm-restart-notice.ts
15579
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "fs";
15580
+ import { dirname as dirname3 } from "path";
15581
+ var RECENT_DM_VERSION = 1;
15582
+ var DEFAULT_RECENT_DM_TTL_MS = 30 * 60 * 1e3;
15583
+ var DEFAULT_DM_NOTICE_WINDOW_MS = 5 * 60 * 1e3;
15584
+ var DEFAULT_MIN_INTERVAL_MS2 = 1e3;
15585
+ function recordDmActivity(map, dmKey, now = /* @__PURE__ */ new Date()) {
15586
+ if (!dmKey) return;
15587
+ map.set(dmKey, { last_activity_at: now.toISOString() });
15588
+ }
15589
+ function recentlyActiveDmKeys(map, now = /* @__PURE__ */ new Date(), windowMs = DEFAULT_DM_NOTICE_WINDOW_MS) {
15590
+ const cutoff = now.getTime() - windowMs;
15591
+ return [...map.entries()].map(([key2, e]) => ({ key: key2, ts: Date.parse(e.last_activity_at) })).filter(({ ts }) => Number.isFinite(ts) && ts >= cutoff).sort((a, b) => b.ts - a.ts).map(({ key: key2 }) => key2);
15592
+ }
15593
+ function serializeRecentDms(map) {
15594
+ const out = { version: RECENT_DM_VERSION, dms: {} };
15595
+ for (const [key2, e] of map) out.dms[key2] = e;
15596
+ return JSON.stringify(out, null, 2);
15597
+ }
15598
+ function loadRecentDms(filePath, opts = {}) {
15599
+ const now = opts.now ?? /* @__PURE__ */ new Date();
15600
+ const ttlMs = opts.ttlMs ?? DEFAULT_RECENT_DM_TTL_MS;
15601
+ let raw;
15602
+ try {
15603
+ raw = readFileSync6(filePath, "utf-8");
15604
+ } catch (err) {
15605
+ const code = err.code;
15606
+ if (code !== "ENOENT") {
15607
+ opts.onError?.(`dm-restart-notice: failed to read recent-DM file: ${err.message}`);
15608
+ }
15609
+ return /* @__PURE__ */ new Map();
15610
+ }
15611
+ let parsed;
15612
+ try {
15613
+ parsed = JSON.parse(raw);
15614
+ } catch (err) {
15615
+ opts.onError?.(`dm-restart-notice: malformed recent-DM JSON (${err.message}) \u2014 ignoring`);
15616
+ return /* @__PURE__ */ new Map();
15617
+ }
15618
+ if (typeof parsed !== "object" || parsed === null || // CodeRabbit: reject unknown versions so a future/legacy on-disk format can't
15619
+ // be read with this version's (possibly incompatible) semantics.
15620
+ parsed.version !== RECENT_DM_VERSION || typeof parsed.dms !== "object" || parsed.dms === null) {
15621
+ opts.onError?.("dm-restart-notice: unexpected recent-DM shape/version \u2014 ignoring");
15622
+ return /* @__PURE__ */ new Map();
15623
+ }
15624
+ const map = /* @__PURE__ */ new Map();
15625
+ const cutoff = now.getTime() - ttlMs;
15626
+ for (const [key2, val] of Object.entries(parsed.dms)) {
15627
+ const at = val?.last_activity_at;
15628
+ if (typeof at !== "string") continue;
15629
+ const ts = Date.parse(at);
15630
+ if (!Number.isFinite(ts) || ts < cutoff) continue;
15631
+ map.set(key2, { last_activity_at: at });
15632
+ }
15633
+ return map;
15634
+ }
15635
+ function createRecentDmPersister(opts) {
15636
+ const interval = opts.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS2;
15637
+ let lastWriteAt = 0;
15638
+ let pendingTimer = null;
15639
+ let pendingSnapshot = null;
15640
+ const writeNow = (snap) => {
15641
+ try {
15642
+ mkdirSync3(dirname3(opts.filePath), { recursive: true });
15643
+ writeFileSync4(opts.filePath, serializeRecentDms(snap), "utf-8");
15644
+ lastWriteAt = Date.now();
15645
+ } catch (err) {
15646
+ opts.onError?.(`dm-restart-notice: failed to persist recent-DMs: ${err.message}`);
15647
+ }
15648
+ };
15649
+ return {
15650
+ schedule(map) {
15651
+ pendingSnapshot = new Map(map);
15652
+ const sinceLast = Date.now() - lastWriteAt;
15653
+ if (sinceLast >= interval) {
15654
+ const snap = pendingSnapshot;
15655
+ pendingSnapshot = null;
15656
+ writeNow(snap);
15657
+ return;
15658
+ }
15659
+ if (pendingTimer) return;
15660
+ pendingTimer = setTimeout(() => {
15661
+ pendingTimer = null;
15662
+ const snap = pendingSnapshot;
15663
+ pendingSnapshot = null;
15664
+ if (snap) writeNow(snap);
15665
+ }, interval - sinceLast);
15666
+ pendingTimer.unref?.();
15667
+ },
15668
+ flush(map) {
15669
+ if (pendingTimer) {
15670
+ clearTimeout(pendingTimer);
15671
+ pendingTimer = null;
15672
+ pendingSnapshot = null;
15673
+ }
15674
+ writeNow(new Map(map));
15675
+ },
15676
+ dispose() {
15677
+ if (pendingTimer) clearTimeout(pendingTimer);
15678
+ pendingTimer = null;
15679
+ pendingSnapshot = null;
15680
+ }
15681
+ };
15682
+ }
15683
+ var CHANNEL_ADD_RESTART_VERSION = 1;
15684
+ var CHANNEL_ADD_RESTART_MAX_AGE_MS = 15 * 60 * 1e3;
15685
+ function readChannelAddRestartMarker(filePath) {
15686
+ let raw;
15687
+ try {
15688
+ raw = readFileSync6(filePath, "utf-8");
15689
+ } catch {
15690
+ return null;
15691
+ }
15692
+ try {
15693
+ const parsed = JSON.parse(raw);
15694
+ if (typeof parsed !== "object" || parsed === null || parsed.version !== CHANNEL_ADD_RESTART_VERSION || typeof parsed.at !== "string") {
15695
+ return null;
15696
+ }
15697
+ const rawAdded = parsed.added;
15698
+ const added = Array.isArray(rawAdded) ? rawAdded.filter((x) => typeof x === "string") : [];
15699
+ return { version: CHANNEL_ADD_RESTART_VERSION, at: parsed.at, added };
15700
+ } catch {
15701
+ return null;
15702
+ }
15703
+ }
15704
+ function shouldPostChannelAddNotice(marker, bootMs, maxAgeMs = CHANNEL_ADD_RESTART_MAX_AGE_MS) {
15705
+ if (!marker) return false;
15706
+ const at = Date.parse(marker.at);
15707
+ if (!Number.isFinite(at)) return false;
15708
+ const age = bootMs - at;
15709
+ return age <= maxAgeMs && age >= -6e4;
15710
+ }
15711
+ function clearChannelAddRestartMarker(filePath) {
15712
+ try {
15713
+ unlinkSync3(filePath);
15714
+ } catch {
15715
+ }
15716
+ }
15717
+ function buildChannelAddNoticeText(added) {
15718
+ const list = added.filter(Boolean);
15719
+ if (list.length === 1) {
15720
+ return `Quick heads-up \u2014 I briefly restarted to bring a new channel (${list[0]}) online. I'm back now; please re-send anything I might have missed.`;
15721
+ }
15722
+ if (list.length > 1) {
15723
+ return `Quick heads-up \u2014 I briefly restarted to bring new channels (${list.join(", ")}) online. I'm back now; please re-send anything I might have missed.`;
15724
+ }
15725
+ return `Quick heads-up \u2014 I briefly restarted to update my configuration. I'm back now; please re-send anything I might have missed.`;
15726
+ }
15727
+
15578
15728
  // src/safe-async.ts
15579
15729
  async function runOrRetry(fn, opts) {
15580
15730
  try {
@@ -15593,8 +15743,8 @@ async function runOrRetry(fn, opts) {
15593
15743
  }
15594
15744
 
15595
15745
  // src/slack-bot-photo.ts
15596
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
15597
- import { dirname as dirname3 } from "path";
15746
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
15747
+ import { dirname as dirname4 } from "path";
15598
15748
  async function applyBotPhoto(opts) {
15599
15749
  const fetchImpl = opts.fetchImpl ?? fetch;
15600
15750
  const log = opts.log ?? ((m) => {
@@ -15603,7 +15753,7 @@ async function applyBotPhoto(opts) {
15603
15753
  const { token, avatarUrl, markerPath } = opts;
15604
15754
  if (markerPath && existsSync5(markerPath)) {
15605
15755
  try {
15606
- if (readFileSync6(markerPath, "utf-8").trim() === avatarUrl) {
15756
+ if (readFileSync7(markerPath, "utf-8").trim() === avatarUrl) {
15607
15757
  return { status: "skipped-unchanged" };
15608
15758
  }
15609
15759
  } catch {
@@ -15644,8 +15794,8 @@ async function applyBotPhoto(opts) {
15644
15794
  }
15645
15795
  if (markerPath) {
15646
15796
  try {
15647
- mkdirSync3(dirname3(markerPath), { recursive: true, mode: 448 });
15648
- writeFileSync4(markerPath, avatarUrl, { mode: 384 });
15797
+ mkdirSync4(dirname4(markerPath), { recursive: true, mode: 448 });
15798
+ writeFileSync5(markerPath, avatarUrl, { mode: 384 });
15649
15799
  } catch {
15650
15800
  }
15651
15801
  }
@@ -16428,11 +16578,11 @@ function createSlackBotUserIdClient(args) {
16428
16578
  // src/mcp-spawn-lock.ts
16429
16579
  import {
16430
16580
  existsSync as existsSync6,
16431
- mkdirSync as mkdirSync4,
16432
- readFileSync as readFileSync7,
16581
+ mkdirSync as mkdirSync5,
16582
+ readFileSync as readFileSync8,
16433
16583
  renameSync as renameSync2,
16434
- unlinkSync as unlinkSync3,
16435
- writeFileSync as writeFileSync5
16584
+ unlinkSync as unlinkSync4,
16585
+ writeFileSync as writeFileSync6
16436
16586
  } from "fs";
16437
16587
  import { join as join6 } from "path";
16438
16588
  function defaultIsPidAlive(pid) {
@@ -16462,10 +16612,10 @@ function acquireMcpSpawnLock(args) {
16462
16612
  return { kind: "blocked", path, holder: existing };
16463
16613
  }
16464
16614
  }
16465
- mkdirSync4(agentDir, { recursive: true, mode: 448 });
16615
+ mkdirSync5(agentDir, { recursive: true, mode: 448 });
16466
16616
  const tmpPath = `${path}.${selfPid}.tmp`;
16467
16617
  const payload = { pid: selfPid, started_at: now() };
16468
- writeFileSync5(tmpPath, JSON.stringify(payload), { mode: 384 });
16618
+ writeFileSync6(tmpPath, JSON.stringify(payload), { mode: 384 });
16469
16619
  renameSync2(tmpPath, path);
16470
16620
  return { kind: "acquired", path };
16471
16621
  }
@@ -16476,14 +16626,14 @@ function releaseMcpSpawnLock(lockPath, opts = {}) {
16476
16626
  if (!existing) return;
16477
16627
  if (existing.pid !== selfPid) return;
16478
16628
  try {
16479
- unlinkSync3(lockPath);
16629
+ unlinkSync4(lockPath);
16480
16630
  } catch {
16481
16631
  }
16482
16632
  }
16483
16633
  function readLockHolder(path) {
16484
16634
  if (!existsSync6(path)) return null;
16485
16635
  try {
16486
- const raw = readFileSync7(path, "utf8");
16636
+ const raw = readFileSync8(path, "utf8");
16487
16637
  const parsed = JSON.parse(raw);
16488
16638
  const pid = typeof parsed.pid === "number" ? parsed.pid : Number(parsed.pid);
16489
16639
  if (!Number.isFinite(pid) || pid <= 0) return null;
@@ -16672,7 +16822,7 @@ function readLiveAllowedUsers() {
16672
16822
  return liveAllowedUsersCache.value;
16673
16823
  }
16674
16824
  const value = extractAllowedUsersFromMcpJson(
16675
- readFileSync8(SLACK_MCP_CONFIG_PATH, "utf-8")
16825
+ readFileSync9(SLACK_MCP_CONFIG_PATH, "utf-8")
16676
16826
  );
16677
16827
  if (value === null) return null;
16678
16828
  liveAllowedUsersCache = { mtimeMs, value };
@@ -16687,6 +16837,8 @@ function getEffectiveAllowedUsers() {
16687
16837
  var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ? join7(SLACK_AGENT_DIR, "slack-pending-inbound") : null;
16688
16838
  var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ? join7(SLACK_AGENT_DIR, "slack-recovery-outbox") : null;
16689
16839
  var SLACK_RESTART_CONFIRM_FILE = SLACK_AGENT_DIR ? join7(SLACK_AGENT_DIR, "slack-restart-confirm.json") : null;
16840
+ var SLACK_RECENT_DMS_FILE = SLACK_AGENT_DIR ? join7(SLACK_AGENT_DIR, "slack-recent-dms.json") : null;
16841
+ var SLACK_CHANNEL_ADD_RESTART_FILE = SLACK_AGENT_DIR ? join7(SLACK_AGENT_DIR, "slack-channel-add-restart.json") : null;
16690
16842
  var SLACK_MAX_RECOVERY_ATTEMPTS = 3;
16691
16843
  var SLACK_AVATAR_MARKER_PATH = SLACK_AGENT_DIR ? join7(SLACK_AGENT_DIR, "slack-avatar-applied") : null;
16692
16844
  function redactSlackId(id) {
@@ -16717,8 +16869,8 @@ function writeSlackPendingInboundMarker(channel, threadTs, messageTs, undelivera
16717
16869
  ...payload ? { payload } : {}
16718
16870
  };
16719
16871
  try {
16720
- mkdirSync5(SLACK_PENDING_INBOUND_DIR, { recursive: true, mode: 448 });
16721
- writeFileSync6(path, JSON.stringify(marker), { mode: 384 });
16872
+ mkdirSync6(SLACK_PENDING_INBOUND_DIR, { recursive: true, mode: 448 });
16873
+ writeFileSync7(path, JSON.stringify(marker), { mode: 384 });
16722
16874
  } catch (err) {
16723
16875
  process.stderr.write(
16724
16876
  `slack-channel(${AGENT_CODE_NAME}): pending-inbound marker write failed: ${err.message}
@@ -16748,7 +16900,7 @@ function attachSlackReplayPayload(channel, threadTs, messageTs, payload) {
16748
16900
  if (!path) return;
16749
16901
  let marker;
16750
16902
  try {
16751
- marker = JSON.parse(readFileSync8(path, "utf-8"));
16903
+ marker = JSON.parse(readFileSync9(path, "utf-8"));
16752
16904
  } catch {
16753
16905
  return;
16754
16906
  }
@@ -16759,7 +16911,7 @@ function readSlackPendingInboundMarker(channel, threadTs, messageTs) {
16759
16911
  const path = slackPendingInboundPath(channel, threadTs, messageTs);
16760
16912
  if (!path || !existsSync7(path)) return null;
16761
16913
  try {
16762
- return JSON.parse(readFileSync8(path, "utf-8"));
16914
+ return JSON.parse(readFileSync9(path, "utf-8"));
16763
16915
  } catch {
16764
16916
  return null;
16765
16917
  }
@@ -16887,7 +17039,7 @@ function __resetSlackBusyAckNoticeThrottle() {
16887
17039
  function clearSlackMarkerFileWithHeal(fullPath) {
16888
17040
  let marker = null;
16889
17041
  try {
16890
- marker = JSON.parse(readFileSync8(fullPath, "utf-8"));
17042
+ marker = JSON.parse(readFileSync9(fullPath, "utf-8"));
16891
17043
  } catch {
16892
17044
  }
16893
17045
  if (marker && decideRecoveryHeal({
@@ -16897,7 +17049,7 @@ function clearSlackMarkerFileWithHeal(fullPath) {
16897
17049
  healSlackUndeliverable(marker.channel, marker.message_ts);
16898
17050
  }
16899
17051
  try {
16900
- if (existsSync7(fullPath)) unlinkSync4(fullPath);
17052
+ if (existsSync7(fullPath)) unlinkSync5(fullPath);
16901
17053
  } catch {
16902
17054
  }
16903
17055
  }
@@ -16941,7 +17093,7 @@ async function processSlackRecoveryOutboxFile(filename) {
16941
17093
  const fullPath = join7(SLACK_RECOVERY_OUTBOX_DIR, filename);
16942
17094
  let payload;
16943
17095
  try {
16944
- payload = JSON.parse(readFileSync8(fullPath, "utf-8"));
17096
+ payload = JSON.parse(readFileSync9(fullPath, "utf-8"));
16945
17097
  } catch (err) {
16946
17098
  process.stderr.write(
16947
17099
  `slack-channel(${AGENT_CODE_NAME}): recovery outbox parse failed (${filename}): ${err.message}
@@ -17009,7 +17161,7 @@ ${payload.text}`;
17009
17161
  }
17010
17162
  if (sendSucceeded) {
17011
17163
  try {
17012
- unlinkSync4(fullPath);
17164
+ unlinkSync5(fullPath);
17013
17165
  } catch {
17014
17166
  }
17015
17167
  return;
@@ -17068,7 +17220,7 @@ function scanSlackRecoveryRetries() {
17068
17220
  function startSlackRecoveryOutboxWatcher() {
17069
17221
  if (!SLACK_RECOVERY_OUTBOX_DIR) return;
17070
17222
  try {
17071
- mkdirSync5(SLACK_RECOVERY_OUTBOX_DIR, { recursive: true, mode: 448 });
17223
+ mkdirSync6(SLACK_RECOVERY_OUTBOX_DIR, { recursive: true, mode: 448 });
17072
17224
  } catch (err) {
17073
17225
  process.stderr.write(
17074
17226
  `slack-channel(${AGENT_CODE_NAME}): recovery outbox mkdir failed: ${err.message}
@@ -17127,14 +17279,14 @@ function sweepSlackStaleMarkers(thresholdMs) {
17127
17279
  const fullPath = join7(SLACK_PENDING_INBOUND_DIR, filename);
17128
17280
  let marker;
17129
17281
  try {
17130
- marker = JSON.parse(readFileSync8(fullPath, "utf-8"));
17282
+ marker = JSON.parse(readFileSync9(fullPath, "utf-8"));
17131
17283
  } catch (err) {
17132
17284
  process.stderr.write(
17133
17285
  `slack-channel(${AGENT_CODE_NAME}): stale-marker parse failed for ${redactSlackId(filename)}: ${err.message}
17134
17286
  `
17135
17287
  );
17136
17288
  try {
17137
- unlinkSync4(fullPath);
17289
+ unlinkSync5(fullPath);
17138
17290
  } catch {
17139
17291
  }
17140
17292
  cleared++;
@@ -17149,7 +17301,7 @@ function sweepSlackStaleMarkers(thresholdMs) {
17149
17301
  recordChannelDeflection(SLACK_AGENT_DIR, "slack", "replay_orphaned");
17150
17302
  }
17151
17303
  try {
17152
- unlinkSync4(fullPath);
17304
+ unlinkSync5(fullPath);
17153
17305
  } catch {
17154
17306
  }
17155
17307
  cleared++;
@@ -17179,7 +17331,7 @@ function listPendingSlackConversations() {
17179
17331
  if (!name.endsWith(".json")) continue;
17180
17332
  try {
17181
17333
  const marker = JSON.parse(
17182
- readFileSync8(join7(SLACK_PENDING_INBOUND_DIR, name), "utf8")
17334
+ readFileSync9(join7(SLACK_PENDING_INBOUND_DIR, name), "utf8")
17183
17335
  );
17184
17336
  if (typeof marker.channel !== "string" || !marker.channel) continue;
17185
17337
  if (typeof marker.thread_ts !== "string" || !marker.thread_ts) continue;
@@ -17273,7 +17425,7 @@ async function notifyStrandedInboundsOnFirstConnect() {
17273
17425
  const fullPath = join7(SLACK_PENDING_INBOUND_DIR, filename);
17274
17426
  let marker;
17275
17427
  try {
17276
- marker = JSON.parse(readFileSync8(fullPath, "utf-8"));
17428
+ marker = JSON.parse(readFileSync9(fullPath, "utf-8"));
17277
17429
  } catch {
17278
17430
  continue;
17279
17431
  }
@@ -17286,7 +17438,7 @@ async function notifyStrandedInboundsOnFirstConnect() {
17286
17438
  const key2 = `${channel}__${thread_ts}`;
17287
17439
  if (seen.has(key2)) {
17288
17440
  try {
17289
- unlinkSync4(fullPath);
17441
+ unlinkSync5(fullPath);
17290
17442
  } catch {
17291
17443
  }
17292
17444
  continue;
@@ -17300,7 +17452,7 @@ async function notifyStrandedInboundsOnFirstConnect() {
17300
17452
  if (res.ok) {
17301
17453
  notified += 1;
17302
17454
  try {
17303
- unlinkSync4(fullPath);
17455
+ unlinkSync5(fullPath);
17304
17456
  } catch {
17305
17457
  }
17306
17458
  } else {
@@ -17372,6 +17524,49 @@ async function notifyRestartCompleteOnFirstConnect() {
17372
17524
  restartConfirmNoticeInFlight = false;
17373
17525
  }
17374
17526
  }
17527
+ var dmChannelAddNoticeFired = false;
17528
+ var dmChannelAddNoticeInFlight = false;
17529
+ async function notifyDmUsersOnChannelAddRestart() {
17530
+ if (dmChannelAddNoticeFired || dmChannelAddNoticeInFlight) return;
17531
+ if (!SLACK_CHANNEL_ADD_RESTART_FILE) return;
17532
+ dmChannelAddNoticeInFlight = true;
17533
+ try {
17534
+ const marker = readChannelAddRestartMarker(SLACK_CHANNEL_ADD_RESTART_FILE);
17535
+ if (!shouldPostChannelAddNotice(marker, SLACK_PROCESS_BOOT_MS)) {
17536
+ if (marker) clearChannelAddRestartMarker(SLACK_CHANNEL_ADD_RESTART_FILE);
17537
+ return;
17538
+ }
17539
+ const dmChannels = recentlyActiveDmKeys(recentDms);
17540
+ const text = buildChannelAddNoticeText(marker.added);
17541
+ let notified = 0;
17542
+ for (const channel of dmChannels) {
17543
+ const res = await postSlackMessage({ channel, text });
17544
+ if (res.ok) {
17545
+ notified += 1;
17546
+ } else {
17547
+ process.stderr.write(
17548
+ `slack-channel(${AGENT_CODE_NAME}): channel-add DM notice failed channel=${redactSlackId(channel)} error=${res.error}
17549
+ `
17550
+ );
17551
+ }
17552
+ }
17553
+ clearChannelAddRestartMarker(SLACK_CHANNEL_ADD_RESTART_FILE);
17554
+ if (notified > 0) {
17555
+ process.stderr.write(
17556
+ `slack-channel(${AGENT_CODE_NAME}): notified ${notified} recently-active DM(s) after channel-add restart
17557
+ `
17558
+ );
17559
+ }
17560
+ } catch (err) {
17561
+ process.stderr.write(
17562
+ `slack-channel(${AGENT_CODE_NAME}): channel-add DM notice threw: ${redactAugmentedPaths2(err.message)}
17563
+ `
17564
+ );
17565
+ } finally {
17566
+ dmChannelAddNoticeFired = true;
17567
+ dmChannelAddNoticeInFlight = false;
17568
+ }
17569
+ }
17375
17570
  function writeSlackRestartConfirm(reply, requesterName) {
17376
17571
  if (!SLACK_RESTART_CONFIRM_FILE) return;
17377
17572
  const marker = {
@@ -17470,6 +17665,7 @@ function buildSlackHelpMessage(codeName) {
17470
17665
  "All commands are real Slack slash commands (autocomplete via `/`). For backward compatibility, `/help` and the restart command can also be typed as plain messages in any channel where the bot is present:",
17471
17666
  `\u2022 \`${agentSlashCommand("/help")}\` (or type \`/help\`) \u2014 show this help`,
17472
17667
  `\u2022 \`${agentSlashCommand("/restart")}\` \u2014 restart this agent`,
17668
+ `\u2022 \`${agentSlashCommand("/resume-onboarding")}\` \u2014 re-run this agent's onboarding interview (allowlisted users)`,
17473
17669
  `\u2022 \`${agentSlashCommand("/status")}\` \u2014 this agent's model, session origin, uptime + connectivity`,
17474
17670
  "\u2022 `/watch <google-doc-url> [duration]` (type it in chat) \u2014 watch a Google Doc for comments that mention me (default 2h, max 7d; auto-pauses when the window ends). In a shared channel, address me as `/watch-<my-name>`.",
17475
17671
  "\u2022 `/kill` \u2014 silence all agents in this thread for 6h (use as a thread reply)",
@@ -17805,7 +18001,7 @@ async function handleSlashCommandEnvelope(payload) {
17805
18001
  }
17806
18002
  try {
17807
18003
  if (!existsSync7(RESTART_FLAGS_DIR)) {
17808
- mkdirSync5(RESTART_FLAGS_DIR, { recursive: true });
18004
+ mkdirSync6(RESTART_FLAGS_DIR, { recursive: true });
17809
18005
  }
17810
18006
  const flagPath = join7(RESTART_FLAGS_DIR, `${codeName}.flag`);
17811
18007
  writeSlackRestartConfirm(
@@ -17825,7 +18021,7 @@ async function handleSlashCommandEnvelope(payload) {
17825
18021
  }
17826
18022
  };
17827
18023
  const tmpPath = `${flagPath}.${process.pid}.${randomUUID2()}.tmp`;
17828
- writeFileSync6(tmpPath, JSON.stringify(flag) + "\n", "utf8");
18024
+ writeFileSync7(tmpPath, JSON.stringify(flag) + "\n", "utf8");
17829
18025
  renameSync3(tmpPath, flagPath);
17830
18026
  process.stderr.write(
17831
18027
  `slack-channel(${codeName}): /restart slash-command queued from channel ${hashChannelId(payload.channel_id)}
@@ -17849,6 +18045,50 @@ async function handleSlashCommandEnvelope(payload) {
17849
18045
  }
17850
18046
  return;
17851
18047
  }
18048
+ if (matchesAgentCommand(command, "/resume-onboarding")) {
18049
+ const allowed = getEffectiveAllowedUsers();
18050
+ if (allowed.size > 0 && (!payload.user_id || !allowed.has(payload.user_id))) {
18051
+ await postEphemeralViaResponseUrl(
18052
+ responseUrl,
18053
+ `\u{1F6AB} \`/resume-onboarding\` denied \u2014 your Slack user is not in the allowlist for \`${codeName}\`.`,
18054
+ codeName
18055
+ );
18056
+ return;
18057
+ }
18058
+ if (!AGT_HOST || !AGT_API_KEY || !AGT_AGENT_ID) {
18059
+ await postEphemeralViaResponseUrl(
18060
+ responseUrl,
18061
+ ":warning: This agent has no host API wiring \u2014 `/resume-onboarding` needs the host runtime to be reachable.",
18062
+ codeName
18063
+ );
18064
+ return;
18065
+ }
18066
+ try {
18067
+ const res = await fetch(`${AGT_HOST}/host/onboarding/reset`, {
18068
+ method: "POST",
18069
+ headers: {
18070
+ "Content-Type": "application/json; charset=utf-8",
18071
+ Authorization: `Bearer ${AGT_API_KEY}`
18072
+ },
18073
+ body: JSON.stringify({ agent_id: AGT_AGENT_ID }),
18074
+ signal: AbortSignal.timeout(SLACK_DOWNLOAD_TIMEOUT_MS)
18075
+ });
18076
+ const data = await res.json();
18077
+ const text = data.ok ? `\u{1F504} ${data.message ?? "Onboarding reset \u2014 I\u2019ll re-interview your manager."}` : `:x: \`/resume-onboarding\` failed${data.error ? `: ${data.error}` : "."}`;
18078
+ await postEphemeralViaResponseUrl(responseUrl, text, codeName);
18079
+ } catch (err) {
18080
+ process.stderr.write(
18081
+ `slack-channel(${codeName}): /resume-onboarding forward failed: ${err.message}
18082
+ `
18083
+ );
18084
+ await postEphemeralViaResponseUrl(
18085
+ responseUrl,
18086
+ ":x: `/resume-onboarding` forwarding failed \u2014 host runtime unreachable. Try again in a moment.",
18087
+ codeName
18088
+ );
18089
+ }
18090
+ return;
18091
+ }
17852
18092
  if (command === "/debug" || matchesAgentCommand(command, "/investigate")) {
17853
18093
  await handleDebugSlashCommand(payload, responseUrl);
17854
18094
  return;
@@ -17923,7 +18163,7 @@ async function handleRestartCommand(opts) {
17923
18163
  const codeName = AGENT_CODE_NAME ?? "unknown";
17924
18164
  try {
17925
18165
  if (!existsSync7(RESTART_FLAGS_DIR)) {
17926
- mkdirSync5(RESTART_FLAGS_DIR, { recursive: true });
18166
+ mkdirSync6(RESTART_FLAGS_DIR, { recursive: true });
17927
18167
  }
17928
18168
  const flagPath = join7(RESTART_FLAGS_DIR, `${codeName}.flag`);
17929
18169
  writeSlackRestartConfirm(
@@ -17944,7 +18184,7 @@ async function handleRestartCommand(opts) {
17944
18184
  }
17945
18185
  };
17946
18186
  const tmpPath = `${flagPath}.${process.pid}.${randomUUID2()}.tmp`;
17947
- writeFileSync6(tmpPath, JSON.stringify(flag) + "\n", "utf8");
18187
+ writeFileSync7(tmpPath, JSON.stringify(flag) + "\n", "utf8");
17948
18188
  renameSync3(tmpPath, flagPath);
17949
18189
  process.stderr.write(
17950
18190
  `slack-channel(${codeName}): /restart queued from channel ${hashChannelId(opts.channel)}
@@ -18040,6 +18280,8 @@ async function denyUnauthorizedWatch(opts) {
18040
18280
  ...opts.threadTs ? { thread_ts: opts.threadTs } : {}
18041
18281
  });
18042
18282
  }
18283
+ var recentDms = /* @__PURE__ */ new Map();
18284
+ var recentDmPersister = null;
18043
18285
  var trackedThreads = /* @__PURE__ */ new Map();
18044
18286
  var THREAD_STORE_PATH = resolveThreadStorePath();
18045
18287
  var THREAD_STORE_TTL_DAYS = parseTtlDays(process.env.SLACK_THREAD_FOLLOW_TTL_DAYS);
@@ -18086,7 +18328,7 @@ var slackStderrLogStream = null;
18086
18328
  if (AGENT_CODE_NAME) {
18087
18329
  try {
18088
18330
  const logDir = join7(homedir3(), ".augmented", AGENT_CODE_NAME);
18089
- mkdirSync5(logDir, { recursive: true });
18331
+ mkdirSync6(logDir, { recursive: true });
18090
18332
  slackStderrLogStream = createWriteStream(join7(logDir, "slack-channel-stderr.log"), {
18091
18333
  flags: "a",
18092
18334
  mode: 384
@@ -18667,7 +18909,7 @@ ${result.formatted}` : "Thread is empty or not found."
18667
18909
  };
18668
18910
  }
18669
18911
  size = stat2.size;
18670
- bytes = readFileSync8(resolvedPath);
18912
+ bytes = readFileSync9(resolvedPath);
18671
18913
  } catch (err) {
18672
18914
  return {
18673
18915
  content: [{ type: "text", text: `Failed to read file: ${err.message}` }],
@@ -19305,8 +19547,8 @@ async function downloadSlackFile(fileId, codeName) {
19305
19547
  if (!isPathInside(savedPath, dir)) {
19306
19548
  throw new Error(`refusing to write ${savedPath} outside ${dir}`);
19307
19549
  }
19308
- mkdirSync5(dir, { recursive: true });
19309
- writeFileSync6(savedPath, bytes, { mode: 384 });
19550
+ mkdirSync6(dir, { recursive: true });
19551
+ writeFileSync7(savedPath, bytes, { mode: 384 });
19310
19552
  try {
19311
19553
  chmodSync(savedPath, 384);
19312
19554
  } catch {
@@ -19385,7 +19627,7 @@ async function replayPendingSlackMarkers() {
19385
19627
  const fullPath = join7(SLACK_PENDING_INBOUND_DIR, name);
19386
19628
  let marker;
19387
19629
  try {
19388
- marker = JSON.parse(readFileSync8(fullPath, "utf-8"));
19630
+ marker = JSON.parse(readFileSync9(fullPath, "utf-8"));
19389
19631
  } catch {
19390
19632
  continue;
19391
19633
  }
@@ -19512,6 +19754,7 @@ async function connectSocketMode() {
19512
19754
  void notifyStrandedInboundsOnFirstConnect();
19513
19755
  }
19514
19756
  void notifyRestartCompleteOnFirstConnect();
19757
+ void notifyDmUsersOnChannelAddRestart();
19515
19758
  void applyBotPhotoOnFirstConnect();
19516
19759
  };
19517
19760
  ws.onmessage = async (event) => {
@@ -19709,6 +19952,14 @@ async function connectSocketMode() {
19709
19952
  const isThreadReply = !!evt.thread_ts && evt.thread_ts !== evt.ts;
19710
19953
  const trackTs = evt.thread_ts ?? evt.ts ?? "";
19711
19954
  const threadKey = buildThreadKey(evt.channel, trackTs);
19955
+ if (isDirectMessage && !isBot && evt.channel && evt.user && evt.user !== botUserId) {
19956
+ recordDmActivity(recentDms, evt.channel);
19957
+ if (isShuttingDown) {
19958
+ recentDmPersister?.flush(recentDms);
19959
+ } else {
19960
+ recentDmPersister?.schedule(recentDms);
19961
+ }
19962
+ }
19712
19963
  if (isBot && peerClassification?.kind === "peer-ingress") {
19713
19964
  const peerGate = peerClassification.peer.gate_path;
19714
19965
  if (peerGate === "intra_org_unrestricted") {
@@ -19944,6 +20195,14 @@ function shutdown(reason, exitCode = 0) {
19944
20195
  threadPersister?.dispose();
19945
20196
  } catch {
19946
20197
  }
20198
+ try {
20199
+ recentDmPersister?.flush(recentDms);
20200
+ } catch {
20201
+ }
20202
+ try {
20203
+ recentDmPersister?.dispose();
20204
+ } catch {
20205
+ }
19947
20206
  process.stderr.write(`slack-channel: ${reason} \u2014 closing Socket Mode and exiting
19948
20207
  `);
19949
20208
  try {
@@ -19987,7 +20246,12 @@ process.on("unhandledRejection", (reason) => {
19987
20246
  }
19988
20247
  if (THREAD_STORE_PATH) {
19989
20248
  const threadStoreLabel = "slack-tracked-threads.json";
19990
- const redact = (msg) => msg.replaceAll(THREAD_STORE_PATH, threadStoreLabel);
20249
+ const recentDmsLabel = "slack-recent-dms.json";
20250
+ const redact = (msg) => {
20251
+ let out = msg.replaceAll(THREAD_STORE_PATH, threadStoreLabel);
20252
+ if (SLACK_RECENT_DMS_FILE) out = out.replaceAll(SLACK_RECENT_DMS_FILE, recentDmsLabel);
20253
+ return out;
20254
+ };
19991
20255
  const { threads, pruned } = loadThreadStore(THREAD_STORE_PATH, {
19992
20256
  ttlDays: THREAD_STORE_TTL_DAYS,
19993
20257
  onError: (msg) => process.stderr.write(`${redact(msg)}
@@ -20003,6 +20267,18 @@ if (THREAD_STORE_PATH) {
20003
20267
  `slack-channel: hydrated ${trackedThreads.size} tracked thread(s)${pruned > 0 ? ` (pruned ${pruned} stale)` : ""} from ${threadStoreLabel}
20004
20268
  `
20005
20269
  );
20270
+ if (SLACK_RECENT_DMS_FILE) {
20271
+ const loadedDms = loadRecentDms(SLACK_RECENT_DMS_FILE, {
20272
+ onError: (msg) => process.stderr.write(`${redact(msg)}
20273
+ `)
20274
+ });
20275
+ for (const [key2, entry] of loadedDms) recentDms.set(key2, entry);
20276
+ recentDmPersister = createRecentDmPersister({
20277
+ filePath: SLACK_RECENT_DMS_FILE,
20278
+ onError: (msg) => process.stderr.write(`${redact(msg)}
20279
+ `)
20280
+ });
20281
+ }
20006
20282
  } else {
20007
20283
  process.stderr.write(
20008
20284
  "slack-channel: AGT_AGENT_CODE_NAME not set \u2014 running without thread-follow persistence\n"