@integrity-labs/agt-cli 0.20.3 → 0.20.5

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.
@@ -14323,27 +14323,6 @@ async function isThreadKilled(opts) {
14323
14323
  }
14324
14324
  }
14325
14325
 
14326
- // src/pending-timeout-rearm.ts
14327
- var DEFAULT_STALE_MARKER_MS = 24 * 60 * 60 * 1e3;
14328
- var DEFAULT_RESPONSE_TIMEOUT_MS = 5 * 60 * 1e3;
14329
- function decideRearmAction(opts) {
14330
- const { receivedAtMs, nowMs } = opts;
14331
- const timeoutMs = opts.timeoutMs ?? DEFAULT_RESPONSE_TIMEOUT_MS;
14332
- const staleMs = opts.staleMs ?? DEFAULT_STALE_MARKER_MS;
14333
- if (!Number.isFinite(receivedAtMs)) {
14334
- return { kind: "stale" };
14335
- }
14336
- const elapsedMs = Math.max(0, nowMs - receivedAtMs);
14337
- if (elapsedMs > staleMs) {
14338
- return { kind: "stale" };
14339
- }
14340
- const remainingMs = timeoutMs - elapsedMs;
14341
- if (remainingMs <= 0) {
14342
- return { kind: "fire" };
14343
- }
14344
- return { kind: "arm", remainingMs };
14345
- }
14346
-
14347
14326
  // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.27.1_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
14348
14327
  import process2 from "process";
14349
14328
 
@@ -14439,16 +14418,21 @@ var StdioServerTransport = class {
14439
14418
  // src/slack-channel.ts
14440
14419
  import {
14441
14420
  chmodSync,
14421
+ closeSync,
14442
14422
  createWriteStream,
14443
14423
  existsSync,
14424
+ fsyncSync,
14425
+ ftruncateSync,
14444
14426
  mkdirSync as mkdirSync2,
14427
+ openSync,
14445
14428
  readFileSync as readFileSync2,
14446
14429
  readdirSync,
14447
14430
  renameSync,
14448
14431
  statSync,
14449
14432
  unlinkSync,
14450
14433
  watch,
14451
- writeFileSync as writeFileSync2
14434
+ writeFileSync as writeFileSync2,
14435
+ writeSync
14452
14436
  } from "fs";
14453
14437
  import { basename, join as join2, resolve as resolve2 } from "path";
14454
14438
  import { homedir as homedir2 } from "os";
@@ -15047,6 +15031,30 @@ function createSlackBotUserIdClient(args) {
15047
15031
  };
15048
15032
  }
15049
15033
 
15034
+ // src/pending-reminder-schedule.ts
15035
+ var REMINDER_SCHEDULE_MS = [5, 10, 20].map((m) => m * 6e4);
15036
+ var MAX_REMINDERS = REMINDER_SCHEDULE_MS.length;
15037
+ var RESPONSE_TIMEOUT_MS = REMINDER_SCHEDULE_MS[0];
15038
+ function nextReminderFireAtMs(receivedAtMs, currentReminderCount) {
15039
+ const safeCount = Number.isFinite(currentReminderCount) ? Math.max(0, Math.trunc(currentReminderCount)) : 0;
15040
+ let elapsed = 0;
15041
+ for (let i = 0; i <= safeCount && i < MAX_REMINDERS; i++) {
15042
+ elapsed += REMINDER_SCHEDULE_MS[i];
15043
+ }
15044
+ return receivedAtMs + elapsed;
15045
+ }
15046
+ function reminderText(reminderIndex, cancelCommandAvailable) {
15047
+ switch (reminderIndex) {
15048
+ case 0:
15049
+ return "Heads-up \u2014 I'm still on it. This one's taking more than the usual minute.";
15050
+ case 1:
15051
+ return "Still working. Going deeper than expected; back when I have something useful.";
15052
+ case 2:
15053
+ default:
15054
+ return cancelCommandAvailable ? "I'm 30+ min in on this. Type `/cancel` to abort, or ping me if it's urgent and I'll re-scope." : "I'm 30+ min in on this. If the answer's urgent, ping me and I'll pause to triage. Otherwise I'll keep going.";
15055
+ }
15056
+ }
15057
+
15050
15058
  // src/slack-channel.ts
15051
15059
  var BOT_TOKEN = process.env.SLACK_BOT_TOKEN;
15052
15060
  var APP_TOKEN = process.env.SLACK_APP_TOKEN;
@@ -15091,8 +15099,8 @@ var SLACK_PEER_CLASSIFIER_CONFIG = {
15091
15099
  peers: parsePeersEnv(process.env.SLACK_PEERS, process.env.SLACK_PEERS_GATE),
15092
15100
  peer_disabled_mode: SLACK_PEER_DISABLED_MODE
15093
15101
  };
15094
- var RESPONSE_TIMEOUT_MS = 3e5;
15095
15102
  var pendingMessages = /* @__PURE__ */ new Map();
15103
+ var CANCEL_COMMAND_AVAILABLE = false;
15096
15104
  var SLACK_AGENT_DIR = AGENT_CODE_NAME ? join2(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
15097
15105
  var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ? join2(SLACK_AGENT_DIR, "slack-pending-inbound") : null;
15098
15106
  var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ? join2(SLACK_AGENT_DIR, "slack-recovery-outbox") : null;
@@ -15128,6 +15136,49 @@ function writeSlackPendingInboundMarker(channel, threadTs, messageTs) {
15128
15136
  );
15129
15137
  }
15130
15138
  }
15139
+ function updateSlackPendingInboundMarker(marker) {
15140
+ const path = slackPendingInboundPath(marker.channel, marker.thread_ts, marker.message_ts);
15141
+ if (!path) return false;
15142
+ let fd;
15143
+ try {
15144
+ fd = openSync(path, "r+");
15145
+ } catch (err) {
15146
+ const code = err.code;
15147
+ if (code === "ENOENT") return false;
15148
+ process.stderr.write(
15149
+ `slack-channel(${AGENT_CODE_NAME}): pending-inbound marker open-for-update failed: ${err.message}
15150
+ `
15151
+ );
15152
+ return false;
15153
+ }
15154
+ try {
15155
+ const payload = Buffer.from(JSON.stringify(marker));
15156
+ ftruncateSync(fd, 0);
15157
+ writeSync(fd, payload, 0, payload.length, 0);
15158
+ fsyncSync(fd);
15159
+ return true;
15160
+ } catch (err) {
15161
+ process.stderr.write(
15162
+ `slack-channel(${AGENT_CODE_NAME}): pending-inbound marker write failed: ${err.message}
15163
+ `
15164
+ );
15165
+ return false;
15166
+ } finally {
15167
+ try {
15168
+ closeSync(fd);
15169
+ } catch {
15170
+ }
15171
+ }
15172
+ }
15173
+ function readSlackPendingInboundMarker(channel, threadTs, messageTs) {
15174
+ const path = slackPendingInboundPath(channel, threadTs, messageTs);
15175
+ if (!path || !existsSync(path)) return null;
15176
+ try {
15177
+ return JSON.parse(readFileSync2(path, "utf-8"));
15178
+ } catch {
15179
+ return null;
15180
+ }
15181
+ }
15131
15182
  function clearSlackPendingInboundMarker(channel, threadTs, messageTs) {
15132
15183
  const path = slackPendingInboundPath(channel, threadTs, messageTs);
15133
15184
  if (!path) return;
@@ -15327,32 +15378,72 @@ function startSlackRecoveryOutboxWatcher() {
15327
15378
  retryTimer.unref?.();
15328
15379
  }
15329
15380
  startSlackRecoveryOutboxWatcher();
15330
- async function fireSlackResponseTimeout(channel, threadTs, messageTs) {
15381
+ var SLACK_REMINDER_FETCH_TIMEOUT_MS = 1e4;
15382
+ async function slackFetchWithTimeout(url, body) {
15383
+ const controller = new AbortController();
15384
+ const timeoutId = setTimeout(() => controller.abort(), SLACK_REMINDER_FETCH_TIMEOUT_MS);
15331
15385
  try {
15332
- await fetch("https://slack.com/api/chat.postMessage", {
15386
+ await fetch(url, {
15333
15387
  method: "POST",
15334
15388
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${BOT_TOKEN}` },
15335
- body: JSON.stringify({
15336
- channel,
15337
- thread_ts: threadTs,
15338
- text: "Sorry \u2014 I didn't get a response back to you within 5 minutes, so this one fell on the floor. Please re-send if it's still relevant."
15339
- })
15389
+ body: JSON.stringify(body),
15390
+ signal: controller.signal
15340
15391
  });
15341
- } catch {
15392
+ } finally {
15393
+ clearTimeout(timeoutId);
15394
+ }
15395
+ }
15396
+ async function fireSlackResponseTimeout(channel, threadTs, messageTs) {
15397
+ const marker = readSlackPendingInboundMarker(channel, threadTs, messageTs);
15398
+ if (!marker) {
15399
+ return;
15400
+ }
15401
+ const reminderIndex = marker.reminder_count ?? 0;
15402
+ if (reminderIndex >= MAX_REMINDERS) {
15403
+ return;
15342
15404
  }
15343
15405
  try {
15344
- await fetch("https://slack.com/api/reactions.remove", {
15345
- method: "POST",
15346
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${BOT_TOKEN}` },
15347
- body: JSON.stringify({ channel, timestamp: messageTs, name: "eyes" })
15406
+ await slackFetchWithTimeout("https://slack.com/api/chat.postMessage", {
15407
+ channel,
15408
+ thread_ts: threadTs,
15409
+ text: reminderText(reminderIndex, CANCEL_COMMAND_AVAILABLE)
15348
15410
  });
15349
15411
  } catch {
15350
15412
  }
15413
+ if (reminderIndex === 1) {
15414
+ try {
15415
+ await slackFetchWithTimeout("https://slack.com/api/reactions.remove", {
15416
+ channel,
15417
+ timestamp: messageTs,
15418
+ name: "eyes"
15419
+ });
15420
+ } catch {
15421
+ }
15422
+ try {
15423
+ await slackFetchWithTimeout("https://slack.com/api/reactions.add", {
15424
+ channel,
15425
+ timestamp: messageTs,
15426
+ name: "hourglass_flowing_sand"
15427
+ });
15428
+ } catch {
15429
+ }
15430
+ }
15431
+ const nextCount = reminderIndex + 1;
15432
+ const persisted = updateSlackPendingInboundMarker({ ...marker, reminder_count: nextCount });
15433
+ if (!persisted) {
15434
+ process.stderr.write(
15435
+ `slack-channel(${AGENT_CODE_NAME}): reminder ${nextCount}/${MAX_REMINDERS} posted but marker cleared mid-fire (agent replied) \u2014 not arming next timer
15436
+ `
15437
+ );
15438
+ return;
15439
+ }
15351
15440
  process.stderr.write(
15352
- `slack-channel: Response timeout for message ${redactSlackId(messageTs)} in ${redactSlackId(channel)}
15441
+ `slack-channel(${AGENT_CODE_NAME}): reminder ${nextCount}/${MAX_REMINDERS} posted for message ${redactSlackId(messageTs)} in ${redactSlackId(channel)} (marker left pending)
15353
15442
  `
15354
15443
  );
15355
- clearSlackPendingInboundMarker(channel, threadTs, messageTs);
15444
+ if (nextCount < MAX_REMINDERS) {
15445
+ armSlackPendingTimer(channel, threadTs, messageTs, REMINDER_SCHEDULE_MS[nextCount]);
15446
+ }
15356
15447
  }
15357
15448
  function armSlackPendingTimer(channel, threadTs, messageTs, durationMs) {
15358
15449
  const key2 = `${channel}:${threadTs}:${messageTs}`;
@@ -15387,8 +15478,10 @@ function rearmSlackPendingTimersFromDisk() {
15387
15478
  let armed = 0;
15388
15479
  let firedNow = 0;
15389
15480
  let cleared = 0;
15481
+ let exhausted = 0;
15390
15482
  for (const filename of filenames) {
15391
15483
  if (!filename.endsWith(".json")) continue;
15484
+ if (filename.endsWith(".tmp")) continue;
15392
15485
  const fullPath = join2(SLACK_PENDING_INBOUND_DIR, filename);
15393
15486
  let marker;
15394
15487
  try {
@@ -15412,15 +15505,18 @@ function rearmSlackPendingTimersFromDisk() {
15412
15505
  }
15413
15506
  continue;
15414
15507
  }
15415
- const action = decideRearmAction({
15416
- receivedAtMs: Date.parse(received_at),
15417
- nowMs: now,
15418
- timeoutMs: RESPONSE_TIMEOUT_MS,
15419
- staleMs: STALE_MARKER_MS
15420
- });
15421
- if (action.kind === "stale") {
15508
+ const receivedAtMs = Date.parse(received_at);
15509
+ if (!Number.isFinite(receivedAtMs)) {
15510
+ try {
15511
+ unlinkSync(fullPath);
15512
+ } catch {
15513
+ }
15514
+ cleared++;
15515
+ continue;
15516
+ }
15517
+ if (now - receivedAtMs > STALE_MARKER_MS) {
15422
15518
  process.stderr.write(
15423
- `slack-channel(${AGENT_CODE_NAME}): rearm skipping stale/invalid marker channel=${redactSlackId(channel)} message=${redactSlackId(message_ts)}
15519
+ `slack-channel(${AGENT_CODE_NAME}): rearm skipping stale marker channel=${redactSlackId(channel)} message=${redactSlackId(message_ts)}
15424
15520
  `
15425
15521
  );
15426
15522
  try {
@@ -15430,18 +15526,25 @@ function rearmSlackPendingTimersFromDisk() {
15430
15526
  cleared++;
15431
15527
  continue;
15432
15528
  }
15433
- if (action.kind === "fire") {
15529
+ const reminderCount = marker.reminder_count ?? 0;
15530
+ if (reminderCount >= MAX_REMINDERS) {
15531
+ exhausted++;
15532
+ continue;
15533
+ }
15534
+ const nextFireAt = nextReminderFireAtMs(receivedAtMs, reminderCount);
15535
+ const remainingMs = nextFireAt - now;
15536
+ if (remainingMs <= 0) {
15434
15537
  pendingMessages.delete(`${channel}:${thread_ts}:${message_ts}`);
15435
15538
  void fireSlackResponseTimeout(channel, thread_ts, message_ts);
15436
15539
  firedNow++;
15437
15540
  } else {
15438
- armSlackPendingTimer(channel, thread_ts, message_ts, action.remainingMs);
15541
+ armSlackPendingTimer(channel, thread_ts, message_ts, remainingMs);
15439
15542
  armed++;
15440
15543
  }
15441
15544
  }
15442
- if (armed > 0 || firedNow > 0 || cleared > 0) {
15545
+ if (armed > 0 || firedNow > 0 || cleared > 0 || exhausted > 0) {
15443
15546
  process.stderr.write(
15444
- `slack-channel(${AGENT_CODE_NAME}): rearm summary armed=${armed} fired_now=${firedNow} stale_cleared=${cleared}
15547
+ `slack-channel(${AGENT_CODE_NAME}): rearm summary armed=${armed} fired_now=${firedNow} stale_cleared=${cleared} reminders_exhausted=${exhausted}
15445
15548
  `
15446
15549
  );
15447
15550
  }
@@ -15456,6 +15559,26 @@ function clearPendingMessage(channel, threadTs) {
15456
15559
  }
15457
15560
  clearAllSlackPendingMarkersForThread(channel, threadTs);
15458
15561
  }
15562
+ function noteThreadActivity(channel, threadTs) {
15563
+ if (!channel || !threadTs) return;
15564
+ clearPendingMessage(channel, threadTs);
15565
+ }
15566
+ function noteThreadActivityByMessageTs(channel, messageTs) {
15567
+ if (!channel || !messageTs) return;
15568
+ clearPendingMessage(channel, messageTs);
15569
+ const suffix = `:${messageTs}`;
15570
+ for (const [key2, timer] of pendingMessages) {
15571
+ if (!key2.startsWith(`${channel}:`)) continue;
15572
+ if (!key2.endsWith(suffix)) continue;
15573
+ clearTimeout(timer);
15574
+ pendingMessages.delete(key2);
15575
+ const parts = key2.split(":");
15576
+ if (parts.length >= 3) {
15577
+ const threadTs = parts[1];
15578
+ clearSlackPendingInboundMarker(channel, threadTs, messageTs);
15579
+ }
15580
+ }
15581
+ }
15459
15582
  var RESTART_FLAGS_DIR = join2(homedir2(), ".augmented", "restart-flags");
15460
15583
  function hashChannelId(id) {
15461
15584
  return createHash("sha256").update(id).digest("hex").slice(0, 8);
@@ -16221,6 +16344,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
16221
16344
  }
16222
16345
  if (name === "slack.react") {
16223
16346
  const { channel, timestamp, emoji: emoji2 } = args;
16347
+ noteThreadActivityByMessageTs(channel, timestamp);
16224
16348
  try {
16225
16349
  const res = await fetch("https://slack.com/api/reactions.add", {
16226
16350
  method: "POST",
@@ -16248,6 +16372,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
16248
16372
  if (name === "slack.read_thread") {
16249
16373
  const { channel, thread_ts, limit: rawLimit } = args;
16250
16374
  const limit = Math.min(Math.max(rawLimit ?? 50, 1), 200);
16375
+ noteThreadActivity(channel, thread_ts);
16251
16376
  try {
16252
16377
  const allMessages = [];
16253
16378
  let cursor;
@@ -16637,6 +16762,7 @@ async function handleSendStructured(args) {
16637
16762
  if (typeof text !== "string" || !text) {
16638
16763
  return errResult("text is required (used as fallback for push notifications and unfurls)");
16639
16764
  }
16765
+ noteThreadActivity(channel, thread_ts);
16640
16766
  const validation = validateSlackBlocks2(blocks);
16641
16767
  if (!validation.ok) {
16642
16768
  return errResult(`Invalid blocks: ${validation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
@@ -13865,16 +13865,21 @@ var StdioServerTransport = class {
13865
13865
  import https from "https";
13866
13866
  import { createHash, randomUUID } from "crypto";
13867
13867
  import {
13868
+ closeSync,
13868
13869
  createWriteStream,
13869
13870
  existsSync,
13871
+ fsyncSync,
13872
+ ftruncateSync,
13870
13873
  mkdirSync as mkdirSync2,
13874
+ openSync,
13871
13875
  readFileSync,
13872
13876
  readdirSync,
13873
13877
  renameSync as renameSync2,
13874
13878
  statSync,
13875
13879
  unlinkSync as unlinkSync2,
13876
13880
  watch,
13877
- writeFileSync as writeFileSync2
13881
+ writeFileSync as writeFileSync2,
13882
+ writeSync
13878
13883
  } from "fs";
13879
13884
  import { homedir as homedir2 } from "os";
13880
13885
  import { join as join2 } from "path";
@@ -14000,27 +14005,6 @@ function redactAugmentedPaths(msg) {
14000
14005
  );
14001
14006
  }
14002
14007
 
14003
- // src/pending-timeout-rearm.ts
14004
- var DEFAULT_STALE_MARKER_MS = 24 * 60 * 60 * 1e3;
14005
- var DEFAULT_RESPONSE_TIMEOUT_MS = 5 * 60 * 1e3;
14006
- function decideRearmAction(opts) {
14007
- const { receivedAtMs, nowMs } = opts;
14008
- const timeoutMs = opts.timeoutMs ?? DEFAULT_RESPONSE_TIMEOUT_MS;
14009
- const staleMs = opts.staleMs ?? DEFAULT_STALE_MARKER_MS;
14010
- if (!Number.isFinite(receivedAtMs)) {
14011
- return { kind: "stale" };
14012
- }
14013
- const elapsedMs = Math.max(0, nowMs - receivedAtMs);
14014
- if (elapsedMs > staleMs) {
14015
- return { kind: "stale" };
14016
- }
14017
- const remainingMs = timeoutMs - elapsedMs;
14018
- if (remainingMs <= 0) {
14019
- return { kind: "fire" };
14020
- }
14021
- return { kind: "arm", remainingMs };
14022
- }
14023
-
14024
14008
  // src/telegram-attachments.ts
14025
14009
  import { mkdirSync, writeFileSync, chmodSync, renameSync, unlinkSync } from "fs";
14026
14010
  function resolveTelegramInboundDir(codeName) {
@@ -14836,6 +14820,30 @@ function createObservedChatClient(args) {
14836
14820
  };
14837
14821
  }
14838
14822
 
14823
+ // src/pending-reminder-schedule.ts
14824
+ var REMINDER_SCHEDULE_MS = [5, 10, 20].map((m) => m * 6e4);
14825
+ var MAX_REMINDERS = REMINDER_SCHEDULE_MS.length;
14826
+ var RESPONSE_TIMEOUT_MS = REMINDER_SCHEDULE_MS[0];
14827
+ function nextReminderFireAtMs(receivedAtMs, currentReminderCount) {
14828
+ const safeCount = Number.isFinite(currentReminderCount) ? Math.max(0, Math.trunc(currentReminderCount)) : 0;
14829
+ let elapsed = 0;
14830
+ for (let i = 0; i <= safeCount && i < MAX_REMINDERS; i++) {
14831
+ elapsed += REMINDER_SCHEDULE_MS[i];
14832
+ }
14833
+ return receivedAtMs + elapsed;
14834
+ }
14835
+ function reminderText(reminderIndex, cancelCommandAvailable) {
14836
+ switch (reminderIndex) {
14837
+ case 0:
14838
+ return "Heads-up \u2014 I'm still on it. This one's taking more than the usual minute.";
14839
+ case 1:
14840
+ return "Still working. Going deeper than expected; back when I have something useful.";
14841
+ case 2:
14842
+ default:
14843
+ return cancelCommandAvailable ? "I'm 30+ min in on this. Type `/cancel` to abort, or ping me if it's urgent and I'll re-scope." : "I'm 30+ min in on this. If the answer's urgent, ping me and I'll pause to triage. Otherwise I'll keep going.";
14844
+ }
14845
+ }
14846
+
14839
14847
  // src/telegram-channel.ts
14840
14848
  function redactId(id) {
14841
14849
  return createHash("sha256").update(String(id)).digest("hex").slice(0, 8);
@@ -14969,7 +14977,8 @@ function telegramApiCall(method, body, timeoutMs) {
14969
14977
  });
14970
14978
  }
14971
14979
  var ACK_EMOJI = "\u{1F440}";
14972
- var RESPONSE_TIMEOUT_MS = 3e5;
14980
+ var CANCEL_COMMAND_AVAILABLE = false;
14981
+ var REMINDER_REACTION_EMOJI = "\u23F3";
14973
14982
  async function setMessageReaction(chatId, messageId, emoji2) {
14974
14983
  try {
14975
14984
  const resp = await telegramApiCall(
@@ -15189,6 +15198,49 @@ function clearPendingInboundMarker(chatId, messageId) {
15189
15198
  } catch {
15190
15199
  }
15191
15200
  }
15201
+ function updatePendingInboundMarker(marker) {
15202
+ const path = pendingInboundPath(marker.chat_id, marker.message_id);
15203
+ if (!path) return false;
15204
+ let fd;
15205
+ try {
15206
+ fd = openSync(path, "r+");
15207
+ } catch (err) {
15208
+ const code = err.code;
15209
+ if (code === "ENOENT") return false;
15210
+ process.stderr.write(
15211
+ `telegram-channel(${AGENT_CODE_NAME}): pending-inbound marker open-for-update failed: ${err.message}
15212
+ `
15213
+ );
15214
+ return false;
15215
+ }
15216
+ try {
15217
+ const payload = Buffer.from(JSON.stringify(marker));
15218
+ ftruncateSync(fd, 0);
15219
+ writeSync(fd, payload, 0, payload.length, 0);
15220
+ fsyncSync(fd);
15221
+ return true;
15222
+ } catch (err) {
15223
+ process.stderr.write(
15224
+ `telegram-channel(${AGENT_CODE_NAME}): pending-inbound marker write failed: ${err.message}
15225
+ `
15226
+ );
15227
+ return false;
15228
+ } finally {
15229
+ try {
15230
+ closeSync(fd);
15231
+ } catch {
15232
+ }
15233
+ }
15234
+ }
15235
+ function readPendingInboundMarker(chatId, messageId) {
15236
+ const path = pendingInboundPath(chatId, messageId);
15237
+ if (!path || !existsSync(path)) return null;
15238
+ try {
15239
+ return JSON.parse(readFileSync(path, "utf-8"));
15240
+ } catch {
15241
+ return null;
15242
+ }
15243
+ }
15192
15244
  var MAX_RECOVERY_ATTEMPTS = 3;
15193
15245
  function nextRetryName(filename) {
15194
15246
  const match = filename.match(/^(.*?)(?:\.retry-(\d+))?\.json$/);
@@ -15356,13 +15408,16 @@ function startRecoveryOutboxWatcher() {
15356
15408
  }
15357
15409
  startRecoveryOutboxWatcher();
15358
15410
  function fireTelegramResponseTimeout(chatId, messageId) {
15359
- clearPendingInboundMarker(chatId, messageId);
15360
- void setMessageReaction(chatId, messageId, null);
15411
+ const marker = readPendingInboundMarker(chatId, messageId);
15412
+ if (!marker) return;
15413
+ const reminderIndex = marker.reminder_count ?? 0;
15414
+ if (reminderIndex >= MAX_REMINDERS) return;
15415
+ const chatType = marker.chat_type ?? "private";
15361
15416
  void telegramApiCall(
15362
15417
  "sendMessage",
15363
15418
  {
15364
15419
  chat_id: chatId,
15365
- text: "Sorry \u2014 I didn't get a response back to you within 5 minutes, so this one fell on the floor. Please re-send if it's still relevant.",
15420
+ text: reminderText(reminderIndex, CANCEL_COMMAND_AVAILABLE),
15366
15421
  reply_to_message_id: Number(messageId),
15367
15422
  allow_sending_without_reply: true
15368
15423
  },
@@ -15370,20 +15425,35 @@ function fireTelegramResponseTimeout(chatId, messageId) {
15370
15425
  ).then((resp) => {
15371
15426
  if (!resp.ok) {
15372
15427
  process.stderr.write(
15373
- `telegram-channel(${AGENT_CODE_NAME}): timeout sendMessage failed for chat ${redactId(chatId)} message ${redactId(messageId)}: ${resp.description ?? "unknown"}
15428
+ `telegram-channel(${AGENT_CODE_NAME}): reminder sendMessage failed for chat ${redactId(chatId)} message ${redactId(messageId)}: ${resp.description ?? "unknown"}
15374
15429
  `
15375
15430
  );
15376
15431
  }
15377
15432
  }).catch((err) => {
15378
15433
  process.stderr.write(
15379
- `telegram-channel(${AGENT_CODE_NAME}): timeout sendMessage error for chat ${redactId(chatId)} message ${redactId(messageId)}: ${err.message}
15434
+ `telegram-channel(${AGENT_CODE_NAME}): reminder sendMessage error for chat ${redactId(chatId)} message ${redactId(messageId)}: ${err.message}
15380
15435
  `
15381
15436
  );
15382
15437
  });
15438
+ if (reminderIndex === 1) {
15439
+ void setMessageReaction(chatId, messageId, REMINDER_REACTION_EMOJI);
15440
+ }
15441
+ const nextCount = reminderIndex + 1;
15442
+ const persisted = updatePendingInboundMarker({ ...marker, reminder_count: nextCount, chat_type: chatType });
15443
+ if (!persisted) {
15444
+ process.stderr.write(
15445
+ `telegram-channel(${AGENT_CODE_NAME}): reminder ${nextCount}/${MAX_REMINDERS} posted but marker cleared mid-fire (agent replied) \u2014 not arming next timer
15446
+ `
15447
+ );
15448
+ return;
15449
+ }
15383
15450
  process.stderr.write(
15384
- `telegram-channel(${AGENT_CODE_NAME}): response timeout for message ${redactId(messageId)} in chat ${redactId(chatId)}
15451
+ `telegram-channel(${AGENT_CODE_NAME}): reminder ${nextCount}/${MAX_REMINDERS} posted for message ${redactId(messageId)} in chat ${redactId(chatId)} (marker left pending)
15385
15452
  `
15386
15453
  );
15454
+ if (nextCount < MAX_REMINDERS) {
15455
+ armTelegramPendingTimer(chatId, messageId, chatType, REMINDER_SCHEDULE_MS[nextCount]);
15456
+ }
15387
15457
  }
15388
15458
  function armTelegramPendingTimer(chatId, messageId, chatType, durationMs) {
15389
15459
  const key2 = `${chatId}:${messageId}`;
@@ -15418,8 +15488,10 @@ function rearmPendingTimersFromDisk() {
15418
15488
  let armed = 0;
15419
15489
  let firedNow = 0;
15420
15490
  let cleared = 0;
15491
+ let exhausted = 0;
15421
15492
  for (const filename of filenames) {
15422
15493
  if (!filename.endsWith(".json")) continue;
15494
+ if (filename.endsWith(".tmp")) continue;
15423
15495
  const fullPath = join2(PENDING_INBOUND_DIR, filename);
15424
15496
  let marker;
15425
15497
  try {
@@ -15443,15 +15515,18 @@ function rearmPendingTimersFromDisk() {
15443
15515
  }
15444
15516
  continue;
15445
15517
  }
15446
- const action = decideRearmAction({
15447
- receivedAtMs: Date.parse(received_at),
15448
- nowMs: now,
15449
- timeoutMs: RESPONSE_TIMEOUT_MS,
15450
- staleMs: STALE_MARKER_MS
15451
- });
15452
- if (action.kind === "stale") {
15518
+ const receivedAtMs = Date.parse(received_at);
15519
+ if (!Number.isFinite(receivedAtMs)) {
15520
+ try {
15521
+ unlinkSync2(fullPath);
15522
+ } catch {
15523
+ }
15524
+ cleared++;
15525
+ continue;
15526
+ }
15527
+ if (now - receivedAtMs > STALE_MARKER_MS) {
15453
15528
  process.stderr.write(
15454
- `telegram-channel(${AGENT_CODE_NAME}): rearm skipping stale/invalid marker chat=${redactId(chat_id)} message=${redactId(message_id)}
15529
+ `telegram-channel(${AGENT_CODE_NAME}): rearm skipping stale marker chat=${redactId(chat_id)} message=${redactId(message_id)}
15455
15530
  `
15456
15531
  );
15457
15532
  try {
@@ -15461,18 +15536,25 @@ function rearmPendingTimersFromDisk() {
15461
15536
  cleared++;
15462
15537
  continue;
15463
15538
  }
15464
- if (action.kind === "fire") {
15539
+ const reminderCount = marker.reminder_count ?? 0;
15540
+ if (reminderCount >= MAX_REMINDERS) {
15541
+ exhausted++;
15542
+ continue;
15543
+ }
15544
+ const nextFireAt = nextReminderFireAtMs(receivedAtMs, reminderCount);
15545
+ const remainingMs = nextFireAt - now;
15546
+ if (remainingMs <= 0) {
15465
15547
  pendingMessages.delete(`${chat_id}:${message_id}`);
15466
15548
  fireTelegramResponseTimeout(chat_id, message_id);
15467
15549
  firedNow++;
15468
15550
  } else {
15469
- armTelegramPendingTimer(chat_id, message_id, chat_type ?? "private", action.remainingMs);
15551
+ armTelegramPendingTimer(chat_id, message_id, chat_type ?? "private", remainingMs);
15470
15552
  armed++;
15471
15553
  }
15472
15554
  }
15473
- if (armed > 0 || firedNow > 0 || cleared > 0) {
15555
+ if (armed > 0 || firedNow > 0 || cleared > 0 || exhausted > 0) {
15474
15556
  process.stderr.write(
15475
- `telegram-channel(${AGENT_CODE_NAME}): rearm summary armed=${armed} fired_now=${firedNow} stale_cleared=${cleared}
15557
+ `telegram-channel(${AGENT_CODE_NAME}): rearm summary armed=${armed} fired_now=${firedNow} stale_cleared=${cleared} reminders_exhausted=${exhausted}
15476
15558
  `
15477
15559
  );
15478
15560
  }
@@ -15511,6 +15593,10 @@ function clearPendingMessage(chatId, messageId) {
15511
15593
  return;
15512
15594
  }
15513
15595
  }
15596
+ function noteThreadActivity(chatId, messageId) {
15597
+ if (!chatId) return;
15598
+ clearPendingMessage(chatId, messageId);
15599
+ }
15514
15600
  var mcp = new Server(
15515
15601
  { name: "telegram", version: "0.1.0" },
15516
15602
  {
@@ -15729,6 +15815,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
15729
15815
  isError: true
15730
15816
  };
15731
15817
  }
15818
+ noteThreadActivity(chat_id, message_id);
15732
15819
  try {
15733
15820
  const data = await telegramApiCall(
15734
15821
  "setMessageReaction",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.20.3",
3
+ "version": "0.20.5",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {