@integrity-labs/agt-cli 0.28.79 → 0.28.81

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.
@@ -16232,6 +16232,12 @@ function markerArrivalMs(fullPath) {
16232
16232
  }
16233
16233
  }
16234
16234
  function clearAllTelegramPendingMarkersForChat(pendingDir, chatId, clearMarkerFile, cutoffMs = Number.POSITIVE_INFINITY) {
16235
+ return applyToChatMarkers(pendingDir, chatId, clearMarkerFile, cutoffMs);
16236
+ }
16237
+ function markSeenAllTelegramPendingMarkersForChat(pendingDir, chatId, markSeen, cutoffMs = Number.POSITIVE_INFINITY) {
16238
+ return applyToChatMarkers(pendingDir, chatId, markSeen, cutoffMs);
16239
+ }
16240
+ function applyToChatMarkers(pendingDir, chatId, op, cutoffMs) {
16235
16241
  const prefix = `${chatId.replace(/[^A-Za-z0-9_-]/g, "_")}__`;
16236
16242
  const bounded = Number.isFinite(cutoffMs);
16237
16243
  let filenames;
@@ -16240,16 +16246,69 @@ function clearAllTelegramPendingMarkersForChat(pendingDir, chatId, clearMarkerFi
16240
16246
  } catch {
16241
16247
  return 0;
16242
16248
  }
16243
- let cleared = 0;
16249
+ let applied = 0;
16244
16250
  for (const filename of filenames) {
16245
16251
  if (!filename.startsWith(prefix)) continue;
16246
16252
  if (!filename.endsWith(".json")) continue;
16247
16253
  const fullPath = join4(pendingDir, filename);
16248
16254
  if (bounded && markerArrivalMs(fullPath) > cutoffMs) continue;
16249
- clearMarkerFile(fullPath);
16250
- cleared++;
16255
+ op(fullPath);
16256
+ applied++;
16257
+ }
16258
+ return applied;
16259
+ }
16260
+
16261
+ // src/channel-progress.ts
16262
+ function channelLiveProgressEnabled() {
16263
+ return resolveHostBooleanFlag({
16264
+ key: "channel-live-progress",
16265
+ envVar: "AGT_CHANNEL_PROGRESS_ENABLED",
16266
+ defaultValue: false
16267
+ });
16268
+ }
16269
+ function decideProgressAction(input) {
16270
+ const { now, heartbeat, target, tracked, freshnessMs, minPendingMs } = input;
16271
+ const fresh = heartbeat != null && now - heartbeat.updatedAtMs <= freshnessMs;
16272
+ const active = target != null && fresh;
16273
+ if (!active) {
16274
+ if (tracked) {
16275
+ return { type: "delete", channel: tracked.channel, threadTs: tracked.threadTs, ts: tracked.ts };
16276
+ }
16277
+ return { type: "none" };
16278
+ }
16279
+ const t = target;
16280
+ const hb = heartbeat;
16281
+ if (tracked && (tracked.threadTs !== t.threadTs || tracked.channel !== t.channel)) {
16282
+ return { type: "delete", channel: tracked.channel, threadTs: tracked.threadTs, ts: tracked.ts };
16283
+ }
16284
+ if (!tracked) {
16285
+ if (now - t.receivedAtMs < minPendingMs) return { type: "none" };
16286
+ return { type: "post", channel: t.channel, threadTs: t.threadTs, step: hb.step };
16287
+ }
16288
+ if (hb.step !== tracked.lastStep) {
16289
+ return { type: "update", channel: t.channel, threadTs: t.threadTs, ts: tracked.ts, step: hb.step };
16290
+ }
16291
+ return { type: "none" };
16292
+ }
16293
+ function progressHeartbeatFreshMs() {
16294
+ const raw = parseInt(process.env.AGT_CHANNEL_PROGRESS_FRESH_MS ?? "", 10);
16295
+ return Number.isFinite(raw) && raw > 0 ? raw : 45e3;
16296
+ }
16297
+ function progressMinPendingMs() {
16298
+ const raw = parseInt(process.env.AGT_CHANNEL_PROGRESS_MIN_PENDING_MS ?? "", 10);
16299
+ return Number.isFinite(raw) && raw > 0 ? raw : 12e3;
16300
+ }
16301
+ function parseProgressHeartbeat(raw) {
16302
+ if (!raw) return null;
16303
+ try {
16304
+ const obj = JSON.parse(raw);
16305
+ const step = typeof obj.step === "string" ? obj.step.trim() : "";
16306
+ const updatedAtMs = typeof obj.updated_at_ms === "number" ? obj.updated_at_ms : NaN;
16307
+ if (!step || !Number.isFinite(updatedAtMs)) return null;
16308
+ return { step, updatedAtMs };
16309
+ } catch {
16310
+ return null;
16251
16311
  }
16252
- return cleared;
16253
16312
  }
16254
16313
 
16255
16314
  // src/mcp-spawn-lock.ts
@@ -17666,6 +17725,33 @@ function clearTelegramMarkerFileWithHeal(fullPath) {
17666
17725
  } catch {
17667
17726
  }
17668
17727
  }
17728
+ function markTelegramMarkerSeenInPlace(fullPath) {
17729
+ let marker;
17730
+ try {
17731
+ marker = JSON.parse(readFileSync8(fullPath, "utf-8"));
17732
+ } catch {
17733
+ return;
17734
+ }
17735
+ if (marker.seen_at) return;
17736
+ marker.seen_at = (/* @__PURE__ */ new Date()).toISOString();
17737
+ if (marker.undeliverable) delete marker.undeliverable;
17738
+ rewriteTelegramMarkerInPlace(fullPath, marker);
17739
+ }
17740
+ function markTelegramMarkerSeenWithHeal(fullPath) {
17741
+ let marker = null;
17742
+ try {
17743
+ marker = JSON.parse(readFileSync8(fullPath, "utf-8"));
17744
+ } catch {
17745
+ return;
17746
+ }
17747
+ if (decideRecoveryHeal({
17748
+ wasUndeliverable: marker.undeliverable === true,
17749
+ hasTarget: Boolean(marker.chat_id)
17750
+ }) === "heal") {
17751
+ notifyBackOnline(marker.chat_id);
17752
+ }
17753
+ markTelegramMarkerSeenInPlace(fullPath);
17754
+ }
17669
17755
  function readPendingInboundMarker(chatId, messageId) {
17670
17756
  const path = pendingInboundPath(chatId, messageId);
17671
17757
  if (!path || !existsSync5(path)) return null;
@@ -17911,6 +17997,143 @@ var orphanSweepTimer = setInterval(() => {
17911
17997
  checkWatchdogGiveUpNotice();
17912
17998
  }, orphanSweepIntervalMs());
17913
17999
  orphanSweepTimer.unref?.();
18000
+ var TELEGRAM_PROGRESS_HEARTBEAT_PATH = AGENT_DIR ? join7(AGENT_DIR, "channel-progress-heartbeat.json") : null;
18001
+ var telegramTrackedProgress = null;
18002
+ var telegramProgressTickRunning = false;
18003
+ function readTelegramProgressHeartbeat() {
18004
+ if (!TELEGRAM_PROGRESS_HEARTBEAT_PATH || !existsSync5(TELEGRAM_PROGRESS_HEARTBEAT_PATH)) return null;
18005
+ try {
18006
+ return parseProgressHeartbeat(readFileSync8(TELEGRAM_PROGRESS_HEARTBEAT_PATH, "utf-8"));
18007
+ } catch {
18008
+ return null;
18009
+ }
18010
+ }
18011
+ function findTelegramProgressTarget() {
18012
+ if (!PENDING_INBOUND_DIR || !existsSync5(PENDING_INBOUND_DIR)) return null;
18013
+ let best = null;
18014
+ let bestMs = Infinity;
18015
+ try {
18016
+ for (const name of readdirSync3(PENDING_INBOUND_DIR)) {
18017
+ if (!name.endsWith(".json")) continue;
18018
+ let m;
18019
+ try {
18020
+ m = JSON.parse(readFileSync8(join7(PENDING_INBOUND_DIR, name), "utf-8"));
18021
+ } catch {
18022
+ continue;
18023
+ }
18024
+ if (!m.chat_id) continue;
18025
+ if (m.undeliverable === true) continue;
18026
+ const ms = Date.parse(m.received_at ?? "");
18027
+ if (!Number.isFinite(ms)) continue;
18028
+ if (ms < bestMs) {
18029
+ bestMs = ms;
18030
+ best = { channel: m.chat_id, threadTs: m.chat_id, receivedAtMs: ms };
18031
+ }
18032
+ }
18033
+ } catch {
18034
+ return null;
18035
+ }
18036
+ return best;
18037
+ }
18038
+ var TELEGRAM_PROGRESS_PREFIX = "\u23F3";
18039
+ function telegramProgressText(step) {
18040
+ return `${TELEGRAM_PROGRESS_PREFIX} Working\u2026 \xB7 ${step}`;
18041
+ }
18042
+ async function postTelegramProgress(chatId, step) {
18043
+ if (!BOT_TOKEN) return null;
18044
+ try {
18045
+ const data = await telegramApiCall(
18046
+ "sendMessage",
18047
+ { chat_id: chatId, text: telegramProgressText(step), disable_notification: true },
18048
+ 8e3
18049
+ );
18050
+ return data.ok && data.result?.message_id != null ? String(data.result.message_id) : null;
18051
+ } catch {
18052
+ return null;
18053
+ }
18054
+ }
18055
+ async function updateTelegramProgress(chatId, messageId, step) {
18056
+ if (!BOT_TOKEN) return false;
18057
+ try {
18058
+ const data = await telegramApiCall(
18059
+ "editMessageText",
18060
+ { chat_id: chatId, message_id: Number(messageId), text: telegramProgressText(step) },
18061
+ 8e3
18062
+ );
18063
+ return data.ok === true || /not modified/i.test(data.description ?? "");
18064
+ } catch {
18065
+ return false;
18066
+ }
18067
+ }
18068
+ async function deleteTelegramProgress(chatId, messageId) {
18069
+ if (!BOT_TOKEN) return false;
18070
+ try {
18071
+ const data = await telegramApiCall(
18072
+ "deleteMessage",
18073
+ { chat_id: chatId, message_id: Number(messageId) },
18074
+ 8e3
18075
+ );
18076
+ return data.ok === true || /message to delete not found|message can't be deleted/i.test(data.description ?? "");
18077
+ } catch {
18078
+ return false;
18079
+ }
18080
+ }
18081
+ async function telegramProgressTick() {
18082
+ if (telegramProgressTickRunning) return;
18083
+ telegramProgressTickRunning = true;
18084
+ try {
18085
+ if (!channelLiveProgressEnabled()) {
18086
+ if (telegramTrackedProgress) {
18087
+ if (await deleteTelegramProgress(telegramTrackedProgress.channel, telegramTrackedProgress.ts)) {
18088
+ telegramTrackedProgress = null;
18089
+ }
18090
+ }
18091
+ return;
18092
+ }
18093
+ const action = decideProgressAction({
18094
+ now: Date.now(),
18095
+ heartbeat: readTelegramProgressHeartbeat(),
18096
+ target: findTelegramProgressTarget(),
18097
+ tracked: telegramTrackedProgress,
18098
+ freshnessMs: progressHeartbeatFreshMs(),
18099
+ minPendingMs: progressMinPendingMs()
18100
+ });
18101
+ switch (action.type) {
18102
+ case "post": {
18103
+ const id = await postTelegramProgress(action.channel, action.step);
18104
+ if (id) telegramTrackedProgress = { channel: action.channel, threadTs: action.threadTs, ts: id, lastStep: action.step };
18105
+ break;
18106
+ }
18107
+ case "update": {
18108
+ if (await updateTelegramProgress(action.channel, action.ts, action.step)) {
18109
+ if (telegramTrackedProgress) telegramTrackedProgress.lastStep = action.step;
18110
+ }
18111
+ break;
18112
+ }
18113
+ case "delete": {
18114
+ if (await deleteTelegramProgress(action.channel, action.ts)) {
18115
+ telegramTrackedProgress = null;
18116
+ }
18117
+ break;
18118
+ }
18119
+ case "none":
18120
+ break;
18121
+ }
18122
+ } catch {
18123
+ } finally {
18124
+ telegramProgressTickRunning = false;
18125
+ }
18126
+ }
18127
+ function telegramProgressPollMs() {
18128
+ const raw = parseInt(process.env.AGT_CHANNEL_PROGRESS_POLL_MS ?? "", 10);
18129
+ return Number.isFinite(raw) && raw >= 2e3 ? raw : 8e3;
18130
+ }
18131
+ if (BOT_TOKEN && PENDING_INBOUND_DIR) {
18132
+ const telegramProgressTimer = setInterval(() => {
18133
+ void telegramProgressTick();
18134
+ }, telegramProgressPollMs());
18135
+ telegramProgressTimer.unref?.();
18136
+ }
17914
18137
  var lastGiveUpHandledAtMs = null;
17915
18138
  function listPendingInboundChatIds() {
17916
18139
  if (!PENDING_INBOUND_DIR || !existsSync5(PENDING_INBOUND_DIR)) return [];
@@ -17922,6 +18145,7 @@ function listPendingInboundChatIds() {
17922
18145
  const marker = JSON.parse(
17923
18146
  readFileSync8(join7(PENDING_INBOUND_DIR, name), "utf8")
17924
18147
  );
18148
+ if (typeof marker.seen_at === "string" && marker.seen_at) continue;
17925
18149
  if (typeof marker.chat_id === "string" && marker.chat_id) chats.add(marker.chat_id);
17926
18150
  } catch {
17927
18151
  }
@@ -18044,9 +18268,18 @@ function clearPendingMessage(chatId, cutoffMs = Number.POSITIVE_INFINITY) {
18044
18268
  cutoffMs
18045
18269
  );
18046
18270
  }
18271
+ function markSeenPendingMessage(chatId, cutoffMs = Number.POSITIVE_INFINITY) {
18272
+ if (!PENDING_INBOUND_DIR || !existsSync5(PENDING_INBOUND_DIR)) return;
18273
+ markSeenAllTelegramPendingMarkersForChat(
18274
+ PENDING_INBOUND_DIR,
18275
+ chatId,
18276
+ markTelegramMarkerSeenWithHeal,
18277
+ cutoffMs
18278
+ );
18279
+ }
18047
18280
  function noteThreadActivity(chatId, cutoffMs = Number.POSITIVE_INFINITY) {
18048
18281
  if (!chatId) return;
18049
- clearPendingMessage(chatId, cutoffMs);
18282
+ markSeenPendingMessage(chatId, cutoffMs);
18050
18283
  }
18051
18284
  var SKIP_REACTION_ON = channelSkipReactionEnabled() && TELEGRAM_SKIP_REACTION.length > 0;
18052
18285
  var mcp = new Server(
@@ -18062,7 +18295,7 @@ var mcp = new Server(
18062
18295
  instructions: [
18063
18296
  // Highest-priority lines first — Claude Code truncates this string at
18064
18297
  // 2048 chars, so anything appended late silently disappears.
18065
- "CRITICAL: every response to a Telegram <channel> tag MUST go through telegram.reply with the chat_id from the tag. Text in your session WITHOUT a telegram.reply call never reaches the user.",
18298
+ "CRITICAL: every response to a Telegram <channel> tag MUST go through telegram.reply with the chat_id from the tag \u2014 typed text never reaches the user. Slow work: interim ack (telegram.reply interim:true), then your FINAL answer as a separate telegram.reply (interim omitted). An ack is not the answer.",
18066
18299
  'Messages from Telegram arrive as <channel source="telegram" chat_id="..." user="..." user_name="..." message_id="...">. Pass reply_to_message_id from the tag so the response lands as a quote-reply in busy chats.',
18067
18300
  "Inbound attachments: <channel> `files` is a JSON-serialised array \u2014 JSON.parse it. If an entry has `path`, the image is already downloaded \u2014 Read it directly, do NOT call telegram.download_attachment. Use that tool only for entries with `file_id` but NO `path` (PDF, docx, voice, audio, video, animations): pass file_id + chat_id verbatim, then Read the returned path. Single-image messages also get a top-level `image_path`. Caption arrives as channel content. Don't surface internal file-handling errors that don't affect the answer.",
18068
18301
  'For work >30s follow CLAUDE.md kanban flow: kanban_add \u2192 reply "On it \u2014 tracking here: <kanban URL>" \u2192 move to in_progress \u2192 do the work \u2192 reply with the result. Simple lookups skip kanban but still reply.',
@@ -18125,6 +18358,10 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
18125
18358
  reply_to_message_id: {
18126
18359
  type: "string",
18127
18360
  description: "Optional Telegram message_id to quote-reply to"
18361
+ },
18362
+ interim: {
18363
+ type: "boolean",
18364
+ description: 'Set true ONLY when this is an interim acknowledgement (e.g. "On it \u2014 checking now\u2026") and the real answer is still coming. The system keeps tracking this chat as awaiting your final reply. Your eventual substantive answer MUST be a separate telegram.reply with interim omitted/false \u2014 that is what marks the request complete. Omit (default false) for any reply that fully answers the user.'
18128
18365
  }
18129
18366
  },
18130
18367
  required: ["chat_id", "text"]
@@ -18196,7 +18433,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
18196
18433
  return handleChannelRequestInput(args ?? {});
18197
18434
  }
18198
18435
  if (name === "telegram.reply" || name === "telegram.send_message") {
18199
- const { chat_id, text, reply_to_message_id } = args;
18436
+ const { chat_id, text, reply_to_message_id, interim } = args;
18200
18437
  if (ALLOWED_CHATS.size > 0 && !ALLOWED_CHATS.has(chat_id)) {
18201
18438
  return {
18202
18439
  content: [{ type: "text", text: `Chat ${chat_id} is not in TELEGRAM_ALLOWED_CHATS` }],
@@ -18322,7 +18559,8 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
18322
18559
  }
18323
18560
  recordReply(chat_id, "", tgThrottleNow, tgThrottleCfg);
18324
18561
  if (name === "telegram.reply") {
18325
- clearPendingMessage(chat_id, drainCutoffMs);
18562
+ if (interim) markSeenPendingMessage(chat_id, drainCutoffMs);
18563
+ else clearPendingMessage(chat_id, drainCutoffMs);
18326
18564
  }
18327
18565
  return { content: [{ type: "text", text: "sent" }] };
18328
18566
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.28.79",
3
+ "version": "0.28.81",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {