cli-jaw 2.2.19 → 2.2.20

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.
Files changed (95) hide show
  1. package/README.md +94 -3
  2. package/dist/server.js +11 -0
  3. package/dist/server.js.map +1 -1
  4. package/dist/src/agent/spawn/process-kill.js +96 -2
  5. package/dist/src/agent/spawn/process-kill.js.map +1 -1
  6. package/dist/src/agent/spawn.js +34 -65
  7. package/dist/src/agent/spawn.js.map +1 -1
  8. package/dist/src/bgtask/runner.js +26 -21
  9. package/dist/src/bgtask/runner.js.map +1 -1
  10. package/dist/src/cli/acp-client.js +11 -5
  11. package/dist/src/cli/acp-client.js.map +1 -1
  12. package/dist/src/cli/employee-handler.js +3 -2
  13. package/dist/src/cli/employee-handler.js.map +1 -1
  14. package/dist/src/code-mode/acp-host.js +22 -10
  15. package/dist/src/code-mode/acp-host.js.map +1 -1
  16. package/dist/src/core/config.js +14 -0
  17. package/dist/src/core/config.js.map +1 -1
  18. package/dist/src/core/db.js +16 -0
  19. package/dist/src/core/db.js.map +1 -1
  20. package/dist/src/core/employees.js +22 -4
  21. package/dist/src/core/employees.js.map +1 -1
  22. package/dist/src/core/windows-shell.js +63 -0
  23. package/dist/src/core/windows-shell.js.map +1 -0
  24. package/dist/src/discord/bot.js +199 -147
  25. package/dist/src/discord/bot.js.map +1 -1
  26. package/dist/src/discord/gateway-supervisor.js +303 -0
  27. package/dist/src/discord/gateway-supervisor.js.map +1 -0
  28. package/dist/src/discord/rest-scheduler.js +448 -0
  29. package/dist/src/discord/rest-scheduler.js.map +1 -0
  30. package/dist/src/discord/send-only-client.js +59 -43
  31. package/dist/src/discord/send-only-client.js.map +1 -1
  32. package/dist/src/manager/lifecycle.js +7 -2
  33. package/dist/src/manager/lifecycle.js.map +1 -1
  34. package/dist/src/manager/notes/capabilities.js +9 -7
  35. package/dist/src/manager/notes/capabilities.js.map +1 -1
  36. package/dist/src/memory/bootstrap.js +5 -1
  37. package/dist/src/memory/bootstrap.js.map +1 -1
  38. package/dist/src/memory/host-toolchain.js +156 -0
  39. package/dist/src/memory/host-toolchain.js.map +1 -0
  40. package/dist/src/messaging/channel-capabilities.js +30 -0
  41. package/dist/src/messaging/channel-capabilities.js.map +1 -0
  42. package/dist/src/messaging/delivery-outcome.js +73 -0
  43. package/dist/src/messaging/delivery-outcome.js.map +1 -0
  44. package/dist/src/messaging/draft-stream.js +154 -0
  45. package/dist/src/messaging/draft-stream.js.map +1 -0
  46. package/dist/src/messaging/runtime.js +3 -9
  47. package/dist/src/messaging/runtime.js.map +1 -1
  48. package/dist/src/messaging/send.js +40 -8
  49. package/dist/src/messaging/send.js.map +1 -1
  50. package/dist/src/messaging/types.js +22 -1
  51. package/dist/src/messaging/types.js.map +1 -1
  52. package/dist/src/notes/search.js +6 -4
  53. package/dist/src/notes/search.js.map +1 -1
  54. package/dist/src/prompt/builder.js +110 -6
  55. package/dist/src/prompt/builder.js.map +1 -1
  56. package/dist/src/prompt/conversation-context.js +23 -0
  57. package/dist/src/prompt/conversation-context.js.map +1 -0
  58. package/dist/src/prompt/templates/a1-system.md +28 -12
  59. package/dist/src/prompt/templates/control-system.md +19 -6
  60. package/dist/src/prompt/templates/employee.md +16 -3
  61. package/dist/src/security/path-guards.js +124 -27
  62. package/dist/src/security/path-guards.js.map +1 -1
  63. package/dist/src/slack/api.js +38 -5
  64. package/dist/src/slack/api.js.map +1 -1
  65. package/dist/src/slack/bot.js +214 -35
  66. package/dist/src/slack/bot.js.map +1 -1
  67. package/dist/src/slack/context.js +192 -0
  68. package/dist/src/slack/context.js.map +1 -0
  69. package/dist/src/slack/conversation.js +275 -0
  70. package/dist/src/slack/conversation.js.map +1 -0
  71. package/dist/src/slack/enrichment-cache.js +334 -0
  72. package/dist/src/slack/enrichment-cache.js.map +1 -0
  73. package/dist/src/slack/history.js +47 -8
  74. package/dist/src/slack/history.js.map +1 -1
  75. package/dist/src/slack/identity.js +99 -190
  76. package/dist/src/slack/identity.js.map +1 -1
  77. package/dist/src/slack/ingress.js +78 -2
  78. package/dist/src/slack/ingress.js.map +1 -1
  79. package/dist/src/slack/progress.js +39 -51
  80. package/dist/src/slack/progress.js.map +1 -1
  81. package/dist/src/slack/thread-tracker.js +78 -0
  82. package/dist/src/slack/thread-tracker.js.map +1 -1
  83. package/dist/src/telegram/bot.js +105 -56
  84. package/dist/src/telegram/bot.js.map +1 -1
  85. package/dist/src/telegram/update-offset.js +152 -0
  86. package/dist/src/telegram/update-offset.js.map +1 -0
  87. package/package.json +1 -1
  88. package/public/dist/assets/Agent-C5EvX-_v.js +1 -0
  89. package/public/dist/assets/{manager-Bjuh1jpq.js → manager-CP-Au860.js} +1 -1
  90. package/public/dist/manager/index.html +1 -1
  91. package/public/manager/src/settings/cli-status-polling.ts +84 -0
  92. package/public/manager/src/settings/pages/Agent.tsx +84 -5
  93. package/scripts/install-officecli.ps1 +38 -2
  94. package/scripts/install.ps1 +1 -1
  95. package/public/dist/assets/Agent-nMPZsMfN.js +0 -1
@@ -10,6 +10,7 @@
10
10
  // bursty tool events would otherwise burn the budget and get throttled.
11
11
  // - Best-effort. A failed status post/edit must never break the answer path.
12
12
  import { slackApi, describeSlackError } from './api.js';
13
+ import { startDraftStream } from '../messaging/draft-stream.js';
13
14
  import { toMrkdwn } from './format.js';
14
15
  /** Minimum gap between chat.update calls for one run (Slack tier limit). */
15
16
  const EDIT_INTERVAL_MS = 1200;
@@ -33,66 +34,53 @@ export function statusFromToolEvent(data, fallback) {
33
34
  }
34
35
  export async function startSlackProgress(token, target, initialText, options = {}) {
35
36
  const fetchOpts = options.fetchImpl ? { fetchImpl: options.fetchImpl } : {};
36
- let messageTs = null;
37
- let done = false;
38
- let lastSentAt = 0;
39
- let pendingText = null;
40
- let flushTimer = null;
41
- const post = await slackApi(token, 'chat.postMessage', {
42
- channel: target.targetId,
43
- text: toMrkdwn(truncateStatus(initialText)),
44
- ...(target.threadId ? { thread_ts: target.threadId } : {}),
45
- }, fetchOpts);
46
- if (post.ok && post.data?.ts)
47
- messageTs = post.data.ts;
48
- const flush = async () => {
49
- if (done || !messageTs || pendingText === null)
50
- return;
51
- const text = pendingText;
52
- pendingText = null;
53
- lastSentAt = Date.now();
54
- await slackApi(token, 'chat.update', { channel: target.targetId, ts: messageTs, text: toMrkdwn(text) }, fetchOpts).catch(() => ({ ok: false, error: 'update_failed' }));
55
- };
56
- const schedule = () => {
57
- if (done || flushTimer || pendingText === null)
58
- return;
59
- const wait = Math.max(0, EDIT_INTERVAL_MS - (Date.now() - lastSentAt));
60
- flushTimer = setTimeout(() => {
61
- flushTimer = null;
62
- void flush().finally(() => { if (pendingText !== null)
63
- schedule(); });
64
- }, wait);
65
- // A pending edit must never hold the process open.
66
- flushTimer.unref?.();
37
+ const transport = {
38
+ async post(text) {
39
+ const result = await slackApi(token, 'chat.postMessage', {
40
+ channel: target.targetId,
41
+ text,
42
+ ...(target.threadId ? { thread_ts: target.threadId } : {}),
43
+ }, fetchOpts);
44
+ return result.ok && result.data?.ts ? result.data.ts : null;
45
+ },
46
+ async edit(ts, text) {
47
+ const result = await slackApi(token, 'chat.update', { channel: target.targetId, ts, text }, fetchOpts);
48
+ if (!result.ok) {
49
+ throw Object.assign(new Error(result.error || 'chat_update_failed'), {
50
+ slackData: result.data,
51
+ });
52
+ }
53
+ },
54
+ async remove(ts) {
55
+ const result = await slackApi(token, 'chat.delete', { channel: target.targetId, ts }, fetchOpts);
56
+ if (!result.ok) {
57
+ throw Object.assign(new Error(result.error || 'chat_delete_failed'), {
58
+ slackData: result.data,
59
+ });
60
+ }
61
+ },
67
62
  };
63
+ const stream = await startDraftStream(transport, toMrkdwn(truncateStatus(initialText)), {
64
+ minEditIntervalMs: EDIT_INTERVAL_MS,
65
+ maxChars: MAX_STATUS_LEN,
66
+ ...options.draftClock,
67
+ onError(operation, error) {
68
+ const typed = error;
69
+ describeSlackError(typed.message || `${operation}_failed`, typed.slackData);
70
+ },
71
+ });
68
72
  return {
69
73
  update(text) {
70
- if (done || !messageTs)
71
- return;
72
74
  const next = truncateStatus(text);
73
- if (!next)
74
- return;
75
- pendingText = next;
76
- schedule();
75
+ if (next)
76
+ stream.update(toMrkdwn(next));
77
77
  },
78
78
  async finish() {
79
- done = true;
80
- pendingText = null;
81
- if (flushTimer) {
82
- clearTimeout(flushTimer);
83
- flushTimer = null;
84
- }
85
- if (!messageTs)
86
- return;
87
- const ts = messageTs;
88
- messageTs = null;
89
79
  // The answer arrives as its own message, so the status placeholder
90
80
  // is deleted rather than left as a stale "working…" line.
91
- const result = await slackApi(token, 'chat.delete', { channel: target.targetId, ts }, fetchOpts);
92
- if (!result.ok)
93
- describeSlackError(result.error || 'chat_delete_failed', result.data);
81
+ await stream.discard();
94
82
  },
95
- ts() { return messageTs; },
83
+ ts() { return stream.handle(); },
96
84
  };
97
85
  }
98
86
  //# sourceMappingURL=progress.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"progress.js","sourceRoot":"","sources":["../../../src/slack/progress.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,uEAAuE;AACvE,8EAA8E;AAC9E,6EAA6E;AAC7E,yDAAyD;AACzD,EAAE;AACF,gBAAgB;AAChB,4EAA4E;AAC5E,8EAA8E;AAC9E,4EAA4E;AAC5E,+EAA+E;AAC/E,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAmB,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,4EAA4E;AAC5E,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAC9B,4EAA4E;AAC5E,MAAM,cAAc,GAAG,GAAG,CAAC;AAW3B,MAAM,UAAU,cAAc,CAAC,IAAY;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9C,OAAO,IAAI,CAAC,MAAM,IAAI,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,cAAc,GAAG,CAAC,CAAC,GAAG,CAAC;AAC1F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAA6B,EAAE,QAAgB;IAC/E,MAAM,KAAK,GAAG,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5E,MAAM,MAAM,GAAG,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/E,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,IAAI,GAAG,KAAK,IAAI,QAAQ,CAAC;IAC/B,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,MAAM,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACpC,KAAa,EACb,MAAoB,EACpB,WAAmB,EACnB,UAAsC,EAAE;IAExC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5E,IAAI,SAAS,GAAkB,IAAI,CAAC;IACpC,IAAI,IAAI,GAAG,KAAK,CAAC;IACjB,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,WAAW,GAAkB,IAAI,CAAC;IACtC,IAAI,UAAU,GAAyC,IAAI,CAAC;IAE5D,MAAM,IAAI,GAAG,MAAM,QAAQ,CACvB,KAAK,EACL,kBAAkB,EAClB;QACI,OAAO,EAAE,MAAM,CAAC,QAAQ;QACxB,IAAI,EAAE,QAAQ,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;QAC3C,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC7D,EACD,SAAS,CACZ,CAAC;IACF,IAAI,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE;QAAE,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAEvD,MAAM,KAAK,GAAG,KAAK,IAAmB,EAAE;QACpC,IAAI,IAAI,IAAI,CAAC,SAAS,IAAI,WAAW,KAAK,IAAI;YAAE,OAAO;QACvD,MAAM,IAAI,GAAG,WAAW,CAAC;QACzB,WAAW,GAAG,IAAI,CAAC;QACnB,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACxB,MAAM,QAAQ,CACV,KAAK,EACL,aAAa,EACb,EAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,EACjE,SAAS,CACZ,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC;IAC3D,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,GAAS,EAAE;QACxB,IAAI,IAAI,IAAI,UAAU,IAAI,WAAW,KAAK,IAAI;YAAE,OAAO;QACvD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,gBAAgB,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC;QACvE,UAAU,GAAG,UAAU,CAAC,GAAG,EAAE;YACzB,UAAU,GAAG,IAAI,CAAC;YAClB,KAAK,KAAK,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,IAAI,WAAW,KAAK,IAAI;gBAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1E,CAAC,EAAE,IAAI,CAAC,CAAC;QACT,mDAAmD;QAClD,UAAqC,CAAC,KAAK,EAAE,EAAE,CAAC;IACrD,CAAC,CAAC;IAEF,OAAO;QACH,MAAM,CAAC,IAAY;YACf,IAAI,IAAI,IAAI,CAAC,SAAS;gBAAE,OAAO;YAC/B,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,CAAC,IAAI;gBAAE,OAAO;YAClB,WAAW,GAAG,IAAI,CAAC;YACnB,QAAQ,EAAE,CAAC;QACf,CAAC;QACD,KAAK,CAAC,MAAM;YACR,IAAI,GAAG,IAAI,CAAC;YACZ,WAAW,GAAG,IAAI,CAAC;YACnB,IAAI,UAAU,EAAE,CAAC;gBAAC,YAAY,CAAC,UAAU,CAAC,CAAC;gBAAC,UAAU,GAAG,IAAI,CAAC;YAAC,CAAC;YAChE,IAAI,CAAC,SAAS;gBAAE,OAAO;YACvB,MAAM,EAAE,GAAG,SAAS,CAAC;YACrB,SAAS,GAAG,IAAI,CAAC;YACjB,mEAAmE;YACnE,0DAA0D;YAC1D,MAAM,MAAM,GAAG,MAAM,QAAQ,CACzB,KAAK,EAAE,aAAa,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,EAAE,EAAE,EAAE,EAAE,SAAS,CACpE,CAAC;YACF,IAAI,CAAC,MAAM,CAAC,EAAE;gBAAE,kBAAkB,CAAC,MAAM,CAAC,KAAK,IAAI,oBAAoB,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1F,CAAC;QACD,EAAE,KAAK,OAAO,SAAS,CAAC,CAAC,CAAC;KAC7B,CAAC;AACN,CAAC"}
1
+ {"version":3,"file":"progress.js","sourceRoot":"","sources":["../../../src/slack/progress.ts"],"names":[],"mappings":"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"}
@@ -78,6 +78,84 @@ export function isThreadParticipated(channel, threadTs) {
78
78
  return false;
79
79
  return load().has(threadKey(channel, threadTs));
80
80
  }
81
+ /** key -> current owner and whether history was actually injected. */
82
+ const prefetchClaimed = new Map();
83
+ const PREFETCH_CLAIM_CAP = 500;
84
+ let prefetchToken = 0;
85
+ /**
86
+ * Claim the one-time prefetch for a thread.
87
+ *
88
+ * Returns a token on success and 0 when the thread is already claimed.
89
+ * Synchronous test-and-set: the caller runs it before any `await`, so two
90
+ * envelopes arriving in the same tick cannot both win.
91
+ *
92
+ * The token exists because releasing is asynchronous. Without it a late
93
+ * release from an abandoned attempt would delete whichever claim happened to
94
+ * hold the key by then — the classic ABA: A claims, A times out and releases,
95
+ * B claims, A's straggler releases B's claim, and the thread gets prefetched
96
+ * twice.
97
+ */
98
+ export function claimThreadPrefetch(channel, threadTs) {
99
+ if (!channel || !threadTs)
100
+ return 0;
101
+ const key = threadKey(channel, threadTs);
102
+ if (prefetchClaimed.has(key))
103
+ return 0;
104
+ if (prefetchClaimed.size >= PREFETCH_CLAIM_CAP) {
105
+ // Active owners are singleflight locks, not cache entries. Evicting one
106
+ // lets another envelope claim the same live thread and inject history
107
+ // twice. Only completed claims may give ground under pressure.
108
+ let removed = 0;
109
+ const target = Math.floor(PREFETCH_CLAIM_CAP / 2);
110
+ for (const [stale, claim] of prefetchClaimed) {
111
+ if (!claim.committed)
112
+ continue;
113
+ prefetchClaimed.delete(stale);
114
+ removed += 1;
115
+ if (removed >= target)
116
+ break;
117
+ }
118
+ // All bounded slots can legitimately be in flight. Decline rather than
119
+ // queue or violate singleflight; a later message can retry after one
120
+ // owner commits or releases.
121
+ if (prefetchClaimed.size >= PREFETCH_CLAIM_CAP)
122
+ return 0;
123
+ }
124
+ const token = ++prefetchToken;
125
+ prefetchClaimed.set(key, { token, committed: false });
126
+ return token;
127
+ }
128
+ /** Mark that this owner actually injected history; completed claims are evictable. */
129
+ export function commitThreadPrefetch(channel, threadTs, token) {
130
+ if (!channel || !threadTs || !token)
131
+ return false;
132
+ const claim = prefetchClaimed.get(threadKey(channel, threadTs));
133
+ if (!claim || claim.token !== token)
134
+ return false;
135
+ claim.committed = true;
136
+ return true;
137
+ }
138
+ /**
139
+ * Give a claim back when no history was actually injected.
140
+ *
141
+ * Without this a failed or skipped first attempt would silently consume the
142
+ * thread's only chance: every later message would see the thread as already
143
+ * prefetched and the agent would never receive the earlier conversation.
144
+ *
145
+ * Only the CURRENT owner may release. A stale token is a no-op.
146
+ */
147
+ export function releaseThreadPrefetch(channel, threadTs, token) {
148
+ if (!channel || !threadTs || !token)
149
+ return;
150
+ const key = threadKey(channel, threadTs);
151
+ if (prefetchClaimed.get(key)?.token !== token)
152
+ return;
153
+ prefetchClaimed.delete(key);
154
+ }
155
+ export function resetThreadPrefetchClaims() {
156
+ prefetchClaimed.clear();
157
+ prefetchToken = 0;
158
+ }
81
159
  /** Test hook: point the store at a temp file and drop the cache. */
82
160
  export function resetThreadTrackerForTest(filePath) {
83
161
  threads = null;
@@ -1 +1 @@
1
- {"version":3,"file":"thread-tracker.js","sourceRoot":"","sources":["../../../src/slack/thread-tracker.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"thread-tracker.js","sourceRoot":"","sources":["../../../src/slack/thread-tracker.ts"],"names":[],"mappings":"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"}
@@ -26,6 +26,8 @@ import { getLastActiveTarget, registerTransport, setLastActiveTarget, setLatestS
26
26
  import { registerSendTransport, sendChannelOutput } from '../messaging/send.js';
27
27
  import { escapeHtmlTg, createForwarderLifecycle, createTelegramForwarder, relayTelegramImages, } from './forwarder.js';
28
28
  import { sendTelegramMarkdown } from './rich-message.js';
29
+ import { db } from '../core/db.js';
30
+ import { TelegramDurablePoller, TelegramUpdateOffsetStore } from './update-offset.js';
29
31
  export { escapeHtmlTg, markdownToTelegramHtml, chunkTelegramMessage, createForwarderLifecycle, createTelegramForwarder, } from './forwarder.js';
30
32
  // Re-exported from collect.ts (extracted in Phase B)
31
33
  import { orchestrateAndCollect, orchestrateAndCollectData } from '../orchestrator/collect.js';
@@ -33,7 +35,6 @@ import { log } from '../core/logger.js';
33
35
  export { orchestrateAndCollect };
34
36
  import { startPendingElicitation, handleElicitationCallback, discardPendingElicitation, } from './elicitation-buttons.js';
35
37
  import { redactOutboundPayload, redactOutboundText, logErrorText, userErrorText } from '../messaging/redact.js';
36
- import { createSeenSet, DELIVERY_DEDUPE_TTL_MS } from '../messaging/dedupe.js';
37
38
  import { sendWithRetryPolicy } from '../messaging/retry.js';
38
39
  // ─── State ───────────────────────────────────────────
39
40
  export let telegramBot = null;
@@ -43,6 +44,8 @@ let tgInitLock = false;
43
44
  let tg409RetryCount = 0;
44
45
  const TG_MAX_RETRIES = 3;
45
46
  let botUsername = null;
47
+ let telegramPoller = null;
48
+ const telegramFinalDeliveryFailures = new Set();
46
49
  /**
47
50
  * The bot's own user id, learned from getMe at startup.
48
51
  *
@@ -66,8 +69,7 @@ export function isSelfEcho(input) {
66
69
  return true;
67
70
  return false;
68
71
  }
69
- /** Redelivered update_ids, so a reconnect replay does not run the agent twice. */
70
- const telegramSeenUpdates = createSeenSet(DELIVERY_DEDUPE_TTL_MS);
72
+ const telegramUpdateOffsets = new TelegramUpdateOffsetStore(db);
71
73
  let targetReplyForwarderInstalled = false;
72
74
  const telegramForwarderLifecycle = createForwarderLifecycle({
73
75
  addListener: addBroadcastListener,
@@ -155,6 +157,16 @@ export async function shutdownTelegram() {
155
157
  tgRetryTimer = null;
156
158
  }
157
159
  detachTelegramForwarder();
160
+ if (telegramPoller) {
161
+ const oldPoller = telegramPoller;
162
+ telegramPoller = null;
163
+ try {
164
+ await oldPoller.stop();
165
+ }
166
+ catch (e) {
167
+ log.warn('[telegram:poller-stop]', logErrorText(e));
168
+ }
169
+ }
158
170
  if (!telegramBot)
159
171
  return;
160
172
  const old = telegramBot;
@@ -364,6 +376,11 @@ async function _initTelegramInner() {
364
376
  await new Promise(r => setTimeout(r, 2000));
365
377
  }
366
378
  }
379
+ const stoppingPoller = telegramPoller?.stop().catch((e) => {
380
+ log.warn('[telegram:poller-stop]', logErrorText(e));
381
+ });
382
+ telegramPoller = null;
383
+ await stoppingPoller;
367
384
  const envToken = process.env["TELEGRAM_TOKEN"];
368
385
  if (envToken)
369
386
  settings["telegram"].token = envToken;
@@ -467,38 +484,21 @@ async function _initTelegramInner() {
467
484
  }
468
485
  await next();
469
486
  });
470
- // Drop a redelivered update. grammY replays from the last committed offset
471
- // after a reconnect, so the same update_id can arrive twice — and each one
472
- // would start another agent run.
473
- //
474
- // Placed AFTER the allowlist and mention gates deliberately: dedupe
475
- // upstream of them lets traffic we never process fill the seen-set — memory
476
- // an outsider controls, and a budget an unmentioned group message can
477
- // exhaust before a real one arrives. A duplicate that WOULD be processed
478
- // passes both gates too, so nothing is lost by checking later.
479
- bot.use(async (ctx, next) => {
480
- const updateId = ctx.update?.update_id;
481
- if (updateId !== undefined && telegramSeenUpdates.seen(String(updateId))) {
482
- log.info(`[tg:duplicate] update_id=${updateId}`);
483
- return;
484
- }
485
- await next();
486
- });
487
487
  bot.command('start', (ctx) => ctx.reply(t('tg.connected', {}, currentLocale())));
488
488
  bot.command('id', (ctx) => ctx.reply(`Chat ID: <code>${ctx.chat?.id ?? ''}</code>`, { parse_mode: 'HTML' }));
489
489
  // Inline-keyboard elicitation answers (single_select fences → buttons).
490
490
  bot.callbackQuery(/^elic:/, async (ctx) => {
491
491
  const cbChatId = ctx.chat?.id;
492
492
  if (!cbChatId) {
493
- await ctx.answerCallbackQuery().catch(() => { });
493
+ await ctx.answerCallbackQuery();
494
494
  return;
495
495
  }
496
496
  const result = handleElicitationCallback(String(cbChatId), ctx.callbackQuery.data ?? '');
497
497
  if (result.kind === 'stale') {
498
- await ctx.answerCallbackQuery({ text: t('tg.elicitationExpired', {}, currentLocale()) }).catch(() => { });
498
+ await ctx.answerCallbackQuery({ text: t('tg.elicitationExpired', {}, currentLocale()) });
499
499
  return;
500
500
  }
501
- await ctx.answerCallbackQuery({ text: redactOutboundText(result.ack) }).catch(() => { });
501
+ await ctx.answerCallbackQuery({ text: redactOutboundText(result.ack) });
502
502
  // Best-effort: freeze the tapped question's keyboard so the choice reads as taken.
503
503
  await ctx.editMessageReplyMarkup({ reply_markup: { inline_keyboard: [] } }).catch(() => { });
504
504
  if (result.kind === 'complete') {
@@ -533,22 +533,54 @@ async function _initTelegramInner() {
533
533
  };
534
534
  if (result.action === 'queued') {
535
535
  log.info(`[tg:queue] agent busy, queued (${result.pending} pending)`);
536
- await ctx.reply(t('tg.queued', { count: result.pending }, currentLocale()));
537
536
  // 큐 처리 후 응답을 이 채팅으로 전달 — requestId로 request-level 격리
538
537
  const requestId = result.requestId;
539
- const queueHandler = (type, data) => {
540
- if (type === 'orchestrate_done' && data["text"] && data["origin"] === 'telegram' && data["requestId"] === requestId) {
538
+ const finalDeliveryControl = {};
539
+ const finalDelivery = new Promise((resolve, reject) => {
540
+ let timer;
541
+ let settled = false;
542
+ const cleanup = () => {
543
+ clearTimeout(timer);
541
544
  removeBroadcastListener(queueHandler);
542
- sendTelegramMarkdown(ctx.api, chat.id, String(data["text"]), replyOptsOf(ctx))
543
- .then(async () => {
544
- await relayTelegramImages(bot, chat.id, String(data["text"]), responseTarget);
545
- await sendElicitationKeyboards(chat.id, data["elicitationSpecs"]);
545
+ };
546
+ finalDeliveryControl.cancel = (reason) => {
547
+ if (settled)
548
+ return;
549
+ settled = true;
550
+ cleanup();
551
+ reject(reason);
552
+ };
553
+ const queueHandler = (type, data) => {
554
+ if (type !== 'orchestrate_done' || !data["text"] || data["origin"] !== 'telegram' || data["requestId"] !== requestId)
555
+ return;
556
+ if (settled)
557
+ return;
558
+ settled = true;
559
+ cleanup();
560
+ void sendTelegramMarkdown(ctx.api, chat.id, String(data["text"]), replyOptsOf(ctx))
561
+ .then(() => {
562
+ resolve();
563
+ void relayTelegramImages(bot, chat.id, String(data["text"]), responseTarget).catch(() => { });
564
+ void sendElicitationKeyboards(chat.id, data["elicitationSpecs"]).catch(() => { });
546
565
  })
547
- .catch(() => { });
548
- }
549
- };
550
- addBroadcastListener(queueHandler);
551
- setTimeout(() => removeBroadcastListener(queueHandler), 300000);
566
+ .catch(reject);
567
+ };
568
+ timer = setTimeout(() => {
569
+ finalDeliveryControl.cancel?.(new Error('telegram_queue_delivery_timeout'));
570
+ }, 300000);
571
+ addBroadcastListener(queueHandler);
572
+ });
573
+ void finalDelivery.catch(() => { });
574
+ try {
575
+ await ctx.reply(t('tg.queued', { count: result.pending }, currentLocale()));
576
+ await finalDelivery;
577
+ }
578
+ catch (error) {
579
+ finalDeliveryControl.cancel?.(error);
580
+ await finalDelivery.catch(() => { });
581
+ telegramFinalDeliveryFailures.add(ctx.update.update_id);
582
+ throw error;
583
+ }
552
584
  return;
553
585
  }
554
586
  if (result.action === 'rejected') {
@@ -649,6 +681,7 @@ async function _initTelegramInner() {
649
681
  } : null;
650
682
  if (toolHandler)
651
683
  addBroadcastListener(toolHandler);
684
+ let finalDeliveryStarted = false;
652
685
  try {
653
686
  const { text: collectedText, data: doneData } = await orchestrateAndCollectData(prompt, stripUndefined({
654
687
  origin: 'telegram', chatId: chat.id, requestId: submitRequestId, _skipInsert: true,
@@ -667,10 +700,11 @@ async function _initTelegramInner() {
667
700
  if (statusMsgId) {
668
701
  ctx.api.deleteMessage(chat.id, statusMsgId).catch(() => { });
669
702
  }
703
+ finalDeliveryStarted = true;
670
704
  await sendTelegramMarkdown(ctx.api, chat.id, collectedText, replyOptsOf(ctx));
671
- await relayTelegramImages(bot, chat.id, collectedText, responseTarget);
672
- await sendElicitationKeyboards(chat.id, doneData["elicitationSpecs"]);
673
705
  log.info(`[tg:out] ${chat.id}: ${redactOutboundText(collectedText).slice(0, 80)}`);
706
+ void relayTelegramImages(bot, chat.id, collectedText, responseTarget).catch(() => { });
707
+ void sendElicitationKeyboards(chat.id, doneData["elicitationSpecs"]).catch(() => { });
674
708
  }
675
709
  catch (err) {
676
710
  clearInterval(typingInterval);
@@ -685,6 +719,8 @@ async function _initTelegramInner() {
685
719
  }
686
720
  log.error('[tg:error]', logErrorText(err));
687
721
  await ctx.reply(`❌ Error: ${userErrorText(err)}`);
722
+ if (finalDeliveryStarted)
723
+ telegramFinalDeliveryFailures.add(ctx.update.update_id);
688
724
  }
689
725
  }
690
726
  bot.on('message:text', async (ctx) => {
@@ -742,7 +778,7 @@ async function _initTelegramInner() {
742
778
  }
743
779
  return;
744
780
  }
745
- tgOrchestrate(ctx, text, text);
781
+ await tgOrchestrate(ctx, text, text);
746
782
  });
747
783
  bot.on('message:photo', async (ctx) => {
748
784
  const photos = ctx.message.photo;
@@ -757,7 +793,7 @@ async function _initTelegramInner() {
757
793
  }));
758
794
  const filePath = saveUpload(dlResult["buffer"], `photo${dlResult["ext"]}`);
759
795
  const prompt = buildMediaPrompt(filePath, caption);
760
- tgOrchestrate(ctx, prompt, `${t('tg.imageCaption', { caption }, currentLocale())}`);
796
+ await tgOrchestrate(ctx, prompt, `${t('tg.imageCaption', { caption }, currentLocale())}`);
761
797
  }
762
798
  catch (err) {
763
799
  log.error('[tg:photo:error]', logErrorText(err));
@@ -776,14 +812,14 @@ async function _initTelegramInner() {
776
812
  }));
777
813
  const filePath = saveUpload(dlResult["buffer"], doc.file_name || 'document');
778
814
  const prompt = buildMediaPrompt(filePath, caption);
779
- tgOrchestrate(ctx, prompt, `[📎 ${doc.file_name || 'file'}] ${caption}`);
815
+ await tgOrchestrate(ctx, prompt, `[📎 ${doc.file_name || 'file'}] ${caption}`);
780
816
  }
781
817
  catch (err) {
782
818
  log.error('[tg:doc:error]', logErrorText(err));
783
819
  await ctx.reply(t('tg.fileFail', { msg: userErrorText(err) }, currentLocale()));
784
820
  }
785
821
  });
786
- bot.on('message:voice', (ctx) => handleVoice(ctx, currentLocale, tgOrchestrate));
822
+ bot.on('message:voice', async (ctx) => { await handleVoice(ctx, currentLocale, tgOrchestrate); });
787
823
  // Identity first: the self-echo guard needs it, and the refusal below has
788
824
  // to happen BEFORE anything is attached. Returning after attaching left the
789
825
  // forwarder wired to a bot that never starts, still trying to send.
@@ -794,17 +830,17 @@ async function _initTelegramInner() {
794
830
  botUserId = null;
795
831
  try {
796
832
  const me = await bot.api.getMe();
833
+ bot.botInfo = me;
797
834
  botUsername = me.username || null;
798
835
  botUserId = me.id ?? null;
799
836
  }
800
837
  catch (err) {
801
838
  log.warn('[tg] getMe failed; bot identity unknown', logErrorText(err));
802
839
  }
803
- // Without an identity the guard has only the is_bot flag to work with — and
804
- // allowBots turns that off. Starting anyway would leave the loop this guard
805
- // exists to prevent wide open, so refuse instead.
806
- if (botUserId === null && settings["telegram"]?.allowBots) {
807
- log.error('[tg] refusing to start: allowBots is on but the bot identity could not be read');
840
+ // The durable offset is scoped by bot identity, so polling must not start
841
+ // when getMe cannot provide that identity.
842
+ if (botUserId === null) {
843
+ log.error(logErrorText('[tg] refusing to start durable polling: bot identity could not be read'));
808
844
  return;
809
845
  }
810
846
  // ─── Global Forwarding: non-Telegram responses → Telegram ───
@@ -814,18 +850,31 @@ async function _initTelegramInner() {
814
850
  void syncTelegramCommands(bot).catch((e) => {
815
851
  log.warn('[tg:commands] setMyCommands failed:', logErrorText(e));
816
852
  });
817
- try {
818
- await bot.api.raw.deleteWebhook({ drop_pending_updates: true });
819
- }
820
- catch { /* best effort */ }
821
- bot.start({
822
- drop_pending_updates: true,
853
+ const poller = new TelegramDurablePoller({
854
+ api: bot.api,
855
+ key: String(botUserId),
856
+ store: telegramUpdateOffsets,
857
+ handleUpdateThroughFinalDelivery: async (update) => {
858
+ try {
859
+ await bot.handleUpdate(update);
860
+ if (telegramFinalDeliveryFailures.has(update.update_id)) {
861
+ throw new Error('telegram_final_delivery_failed');
862
+ }
863
+ }
864
+ finally {
865
+ telegramFinalDeliveryFailures.delete(update.update_id);
866
+ }
867
+ },
823
868
  onStart: (info) => {
824
869
  tg409RetryCount = 0;
825
- log.info(`[tg] @${info.username} polling active`);
870
+ const skipped = info.skippedThroughUpdateId === null ? '' : `; skipped through update ${info.skippedThroughUpdateId}`;
871
+ log.info(`[tg] ✅ @${botUsername ?? botUserId} durable polling active at offset ${info.nextOffset}${skipped}`);
826
872
  },
827
- }).catch((err) => {
828
- const is409 = err?.error_code === 409 || err?.message?.includes('409');
873
+ });
874
+ telegramPoller = poller;
875
+ poller.start().catch((err) => {
876
+ const telegramError = err;
877
+ const is409 = telegramError.error_code === 409 || telegramError.message?.includes('409');
829
878
  if (is409) {
830
879
  tg409RetryCount++;
831
880
  if (tg409RetryCount > TG_MAX_RETRIES) {
@@ -839,7 +888,7 @@ async function _initTelegramInner() {
839
888
  }
840
889
  }
841
890
  else {
842
- log.error('[tg:fatal]', logErrorText(err));
891
+ log.error('[tg:fatal] Telegram durability bootstrap/polling failed; no uncommitted backlog consumed', logErrorText(err));
843
892
  }
844
893
  });
845
894
  telegramBot = bot;