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.
- package/README.md +94 -3
- package/dist/server.js +11 -0
- package/dist/server.js.map +1 -1
- package/dist/src/agent/spawn/process-kill.js +96 -2
- package/dist/src/agent/spawn/process-kill.js.map +1 -1
- package/dist/src/agent/spawn.js +34 -65
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/bgtask/runner.js +26 -21
- package/dist/src/bgtask/runner.js.map +1 -1
- package/dist/src/cli/acp-client.js +11 -5
- package/dist/src/cli/acp-client.js.map +1 -1
- package/dist/src/cli/employee-handler.js +3 -2
- package/dist/src/cli/employee-handler.js.map +1 -1
- package/dist/src/code-mode/acp-host.js +22 -10
- package/dist/src/code-mode/acp-host.js.map +1 -1
- package/dist/src/core/config.js +14 -0
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/db.js +16 -0
- package/dist/src/core/db.js.map +1 -1
- package/dist/src/core/employees.js +22 -4
- package/dist/src/core/employees.js.map +1 -1
- package/dist/src/core/windows-shell.js +63 -0
- package/dist/src/core/windows-shell.js.map +1 -0
- package/dist/src/discord/bot.js +199 -147
- package/dist/src/discord/bot.js.map +1 -1
- package/dist/src/discord/gateway-supervisor.js +303 -0
- package/dist/src/discord/gateway-supervisor.js.map +1 -0
- package/dist/src/discord/rest-scheduler.js +448 -0
- package/dist/src/discord/rest-scheduler.js.map +1 -0
- package/dist/src/discord/send-only-client.js +59 -43
- package/dist/src/discord/send-only-client.js.map +1 -1
- package/dist/src/manager/lifecycle.js +7 -2
- package/dist/src/manager/lifecycle.js.map +1 -1
- package/dist/src/manager/notes/capabilities.js +9 -7
- package/dist/src/manager/notes/capabilities.js.map +1 -1
- package/dist/src/memory/bootstrap.js +5 -1
- package/dist/src/memory/bootstrap.js.map +1 -1
- package/dist/src/memory/host-toolchain.js +156 -0
- package/dist/src/memory/host-toolchain.js.map +1 -0
- package/dist/src/messaging/channel-capabilities.js +30 -0
- package/dist/src/messaging/channel-capabilities.js.map +1 -0
- package/dist/src/messaging/delivery-outcome.js +73 -0
- package/dist/src/messaging/delivery-outcome.js.map +1 -0
- package/dist/src/messaging/draft-stream.js +154 -0
- package/dist/src/messaging/draft-stream.js.map +1 -0
- package/dist/src/messaging/runtime.js +3 -9
- package/dist/src/messaging/runtime.js.map +1 -1
- package/dist/src/messaging/send.js +40 -8
- package/dist/src/messaging/send.js.map +1 -1
- package/dist/src/messaging/types.js +22 -1
- package/dist/src/messaging/types.js.map +1 -1
- package/dist/src/notes/search.js +6 -4
- package/dist/src/notes/search.js.map +1 -1
- package/dist/src/prompt/builder.js +110 -6
- package/dist/src/prompt/builder.js.map +1 -1
- package/dist/src/prompt/conversation-context.js +23 -0
- package/dist/src/prompt/conversation-context.js.map +1 -0
- package/dist/src/prompt/templates/a1-system.md +28 -12
- package/dist/src/prompt/templates/control-system.md +19 -6
- package/dist/src/prompt/templates/employee.md +16 -3
- package/dist/src/security/path-guards.js +124 -27
- package/dist/src/security/path-guards.js.map +1 -1
- package/dist/src/slack/api.js +38 -5
- package/dist/src/slack/api.js.map +1 -1
- package/dist/src/slack/bot.js +214 -35
- package/dist/src/slack/bot.js.map +1 -1
- package/dist/src/slack/context.js +192 -0
- package/dist/src/slack/context.js.map +1 -0
- package/dist/src/slack/conversation.js +275 -0
- package/dist/src/slack/conversation.js.map +1 -0
- package/dist/src/slack/enrichment-cache.js +334 -0
- package/dist/src/slack/enrichment-cache.js.map +1 -0
- package/dist/src/slack/history.js +47 -8
- package/dist/src/slack/history.js.map +1 -1
- package/dist/src/slack/identity.js +99 -190
- package/dist/src/slack/identity.js.map +1 -1
- package/dist/src/slack/ingress.js +78 -2
- package/dist/src/slack/ingress.js.map +1 -1
- package/dist/src/slack/progress.js +39 -51
- package/dist/src/slack/progress.js.map +1 -1
- package/dist/src/slack/thread-tracker.js +78 -0
- package/dist/src/slack/thread-tracker.js.map +1 -1
- package/dist/src/telegram/bot.js +105 -56
- package/dist/src/telegram/bot.js.map +1 -1
- package/dist/src/telegram/update-offset.js +152 -0
- package/dist/src/telegram/update-offset.js.map +1 -0
- package/package.json +1 -1
- package/public/dist/assets/Agent-C5EvX-_v.js +1 -0
- package/public/dist/assets/{manager-Bjuh1jpq.js → manager-CP-Au860.js} +1 -1
- package/public/dist/manager/index.html +1 -1
- package/public/manager/src/settings/cli-status-polling.ts +84 -0
- package/public/manager/src/settings/pages/Agent.tsx +84 -5
- package/scripts/install-officecli.ps1 +38 -2
- package/scripts/install.ps1 +1 -1
- 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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 (
|
|
74
|
-
|
|
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
|
-
|
|
92
|
-
if (!result.ok)
|
|
93
|
-
describeSlackError(result.error || 'chat_delete_failed', result.data);
|
|
81
|
+
await stream.discard();
|
|
94
82
|
},
|
|
95
|
-
ts() { return
|
|
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;
|
|
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":"AAAA,wDAAwD;AACxD,8EAA8E;AAC9E,2EAA2E;AAC3E,2EAA2E;AAC3E,iEAAiE;AACjE,0DAA0D;AAC1D,8CAA8C;AAC9C,EAAE;AACF,4EAA4E;AAC5E,6EAA6E;AAC7E,yEAAyE;AACzE,6DAA6D;AAE7D,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE7C,+DAA+D;AAC/D,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAErC,IAAI,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,oBAAoB,CAAC,CAAC;AAC1D,8EAA8E;AAC9E,mEAAmE;AACnE,IAAI,OAAO,GAA+B,IAAI,CAAC;AAE/C,MAAM,UAAU,SAAS,CAAC,OAAe,EAAE,QAAgB;IACvD,OAAO,GAAG,OAAO,IAAI,QAAQ,EAAE,CAAC;AACpC,CAAC;AAED,SAAS,IAAI;IACT,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAC5B,OAAO,GAAG,IAAI,GAAG,EAAE,CAAC;IACpB,IAAI,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;QAC3D,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YACxD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC7C,IAAI,OAAO,KAAK,KAAK,QAAQ;oBAAE,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAC3D,CAAC;QACL,CAAC;IACL,CAAC;IAAC,MAAM,CAAC,CAAC,qEAAqE,CAAC,CAAC;IACjF,OAAO,OAAO,CAAC;AACnB,CAAC;AAED,SAAS,IAAI,CAAC,GAAwB;IAClC,MAAM,GAAG,GAAG,SAAS,GAAG,MAAM,CAAC;IAC/B,IAAI,CAAC;QACD,EAAE,CAAC,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC/D,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAClC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,qEAAqE;QACrE,mEAAmE;QACnE,OAAO,CAAC,IAAI,CAAC,iCAAiC,EAAG,KAAe,CAAC,OAAO,CAAC,CAAC;IAC9E,CAAC;AACL,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,OAAe,EAAE,QAAgB;IACpE,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ;QAAE,OAAO;IAClC,MAAM,GAAG,GAAG,IAAI,EAAE,CAAC;IACnB,MAAM,GAAG,GAAG,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC5B,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IACzB,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,GAAG,CAAC,IAAI,GAAG,iBAAiB,EAAE,CAAC;QAC/B,qEAAqE;QACrE,+DAA+D;QAC/D,oEAAoE;QACpE,MAAM,OAAO,GAAG,CAAC,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/D,KAAK,MAAM,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAClE,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACzB,CAAC;QACD,OAAO,GAAG,IAAI,CAAC;IACnB,CAAC;IACD,yEAAyE;IACzE,8CAA8C;IAC9C,IAAI,KAAK,IAAI,OAAO;QAAE,IAAI,CAAC,GAAG,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,OAAe,EAAE,QAAgB;IAClE,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IACxC,OAAO,IAAI,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;AACpD,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,yBAAyB,CAAC,QAAiB;IACvD,OAAO,GAAG,IAAI,CAAC;IACf,SAAS,GAAG,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,oBAAoB,CAAC,CAAC;AACtE,CAAC"}
|
|
1
|
+
{"version":3,"file":"thread-tracker.js","sourceRoot":"","sources":["../../../src/slack/thread-tracker.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,8EAA8E;AAC9E,2EAA2E;AAC3E,2EAA2E;AAC3E,iEAAiE;AACjE,0DAA0D;AAC1D,8CAA8C;AAC9C,EAAE;AACF,4EAA4E;AAC5E,6EAA6E;AAC7E,yEAAyE;AACzE,6DAA6D;AAE7D,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE7C,+DAA+D;AAC/D,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAErC,IAAI,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,oBAAoB,CAAC,CAAC;AAC1D,8EAA8E;AAC9E,mEAAmE;AACnE,IAAI,OAAO,GAA+B,IAAI,CAAC;AAE/C,MAAM,UAAU,SAAS,CAAC,OAAe,EAAE,QAAgB;IACvD,OAAO,GAAG,OAAO,IAAI,QAAQ,EAAE,CAAC;AACpC,CAAC;AAED,SAAS,IAAI;IACT,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAC5B,OAAO,GAAG,IAAI,GAAG,EAAE,CAAC;IACpB,IAAI,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;QAC3D,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YACxD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC7C,IAAI,OAAO,KAAK,KAAK,QAAQ;oBAAE,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAC3D,CAAC;QACL,CAAC;IACL,CAAC;IAAC,MAAM,CAAC,CAAC,qEAAqE,CAAC,CAAC;IACjF,OAAO,OAAO,CAAC;AACnB,CAAC;AAED,SAAS,IAAI,CAAC,GAAwB;IAClC,MAAM,GAAG,GAAG,SAAS,GAAG,MAAM,CAAC;IAC/B,IAAI,CAAC;QACD,EAAE,CAAC,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC/D,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAClC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,qEAAqE;QACrE,mEAAmE;QACnE,OAAO,CAAC,IAAI,CAAC,iCAAiC,EAAG,KAAe,CAAC,OAAO,CAAC,CAAC;IAC9E,CAAC;AACL,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,OAAe,EAAE,QAAgB;IACpE,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ;QAAE,OAAO;IAClC,MAAM,GAAG,GAAG,IAAI,EAAE,CAAC;IACnB,MAAM,GAAG,GAAG,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC5B,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IACzB,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,GAAG,CAAC,IAAI,GAAG,iBAAiB,EAAE,CAAC;QAC/B,qEAAqE;QACrE,+DAA+D;QAC/D,oEAAoE;QACpE,MAAM,OAAO,GAAG,CAAC,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/D,KAAK,MAAM,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAClE,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACzB,CAAC;QACD,OAAO,GAAG,IAAI,CAAC;IACnB,CAAC;IACD,yEAAyE;IACzE,8CAA8C;IAC9C,IAAI,KAAK,IAAI,OAAO;QAAE,IAAI,CAAC,GAAG,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,OAAe,EAAE,QAAgB;IAClE,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IACxC,OAAO,IAAI,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;AACpD,CAAC;AAeD,sEAAsE;AACtE,MAAM,eAAe,GAAG,IAAI,GAAG,EAAyB,CAAC;AACzD,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAC/B,IAAI,aAAa,GAAG,CAAC,CAAC;AAEtB;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAe,EAAE,QAAgB;IACjE,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ;QAAE,OAAO,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACzC,IAAI,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IACvC,IAAI,eAAe,CAAC,IAAI,IAAI,kBAAkB,EAAE,CAAC;QAC7C,wEAAwE;QACxE,sEAAsE;QACtE,+DAA+D;QAC/D,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,kBAAkB,GAAG,CAAC,CAAC,CAAC;QAClD,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,eAAe,EAAE,CAAC;YAC3C,IAAI,CAAC,KAAK,CAAC,SAAS;gBAAE,SAAS;YAC/B,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC9B,OAAO,IAAI,CAAC,CAAC;YACb,IAAI,OAAO,IAAI,MAAM;gBAAE,MAAM;QACjC,CAAC;QACD,uEAAuE;QACvE,qEAAqE;QACrE,6BAA6B;QAC7B,IAAI,eAAe,CAAC,IAAI,IAAI,kBAAkB;YAAE,OAAO,CAAC,CAAC;IAC7D,CAAC;IACD,MAAM,KAAK,GAAG,EAAE,aAAa,CAAC;IAC9B,eAAe,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;IACtD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,oBAAoB,CAAC,OAAe,EAAE,QAAgB,EAAE,KAAa;IACjF,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IAClD,MAAM,KAAK,GAAG,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;IAChE,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IAClD,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC;IACvB,OAAO,IAAI,CAAC;AAChB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAe,EAAE,QAAgB,EAAE,KAAa;IAClF,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ,IAAI,CAAC,KAAK;QAAE,OAAO;IAC5C,MAAM,GAAG,GAAG,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACzC,IAAI,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,KAAK;QAAE,OAAO;IACtD,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,yBAAyB;IACrC,eAAe,CAAC,KAAK,EAAE,CAAC;IACxB,aAAa,GAAG,CAAC,CAAC;AACtB,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,yBAAyB,CAAC,QAAiB;IACvD,OAAO,GAAG,IAAI,CAAC;IACf,SAAS,GAAG,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,oBAAoB,CAAC,CAAC;AACtE,CAAC"}
|
package/dist/src/telegram/bot.js
CHANGED
|
@@ -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
|
-
|
|
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()
|
|
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()) })
|
|
498
|
+
await ctx.answerCallbackQuery({ text: t('tg.elicitationExpired', {}, currentLocale()) });
|
|
499
499
|
return;
|
|
500
500
|
}
|
|
501
|
-
await ctx.answerCallbackQuery({ text: redactOutboundText(result.ack) })
|
|
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
|
|
540
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
551
|
-
|
|
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
|
-
//
|
|
804
|
-
//
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
828
|
-
|
|
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;
|