metheus-governance-mcp-cli 0.2.184 → 0.2.185
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/cli.mjs +81 -0
- package/lib/runner-data.mjs +49 -0
- package/lib/runner-delivery.mjs +6 -2
- package/lib/runner-helpers.mjs +85 -2
- package/lib/runner-orchestration.mjs +31 -21
- package/lib/runner-runtime.mjs +75 -7
- package/lib/selftest-runner-scenarios.mjs +81 -0
- package/lib/selftest-telegram-e2e.mjs +270 -11
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -130,6 +130,8 @@ import {
|
|
|
130
130
|
import {
|
|
131
131
|
applyPendingAgeSelection,
|
|
132
132
|
buildRunnerRouteStateFromComment,
|
|
133
|
+
buildProcessableArchiveLogicalKey,
|
|
134
|
+
findEarlierProcessableArchiveDuplicate,
|
|
133
135
|
isInboundArchiveKind,
|
|
134
136
|
normalizeArchiveCommentRecord,
|
|
135
137
|
selectPendingArchiveComments,
|
|
@@ -148,6 +150,7 @@ import {
|
|
|
148
150
|
listProjectContextItems as listProjectContextItemsImpl,
|
|
149
151
|
listProjectCtxpackFiles as listProjectCtxpackFilesImpl,
|
|
150
152
|
listThreadComments as listThreadCommentsImpl,
|
|
153
|
+
listThreadCommentsTail as listThreadCommentsTailImpl,
|
|
151
154
|
listUserBotsForRunner as listUserBotsForRunnerImpl,
|
|
152
155
|
selectProjectChatDestination as selectProjectChatDestinationImpl,
|
|
153
156
|
selectRunnerBot as selectRunnerBotImpl,
|
|
@@ -3977,6 +3980,10 @@ async function listThreadComments(params) {
|
|
|
3977
3980
|
return listThreadCommentsImpl(params, buildRunnerDataDeps());
|
|
3978
3981
|
}
|
|
3979
3982
|
|
|
3983
|
+
async function listThreadCommentsTail(params) {
|
|
3984
|
+
return listThreadCommentsTailImpl(params, buildRunnerDataDeps());
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3980
3987
|
async function createThreadComment(params) {
|
|
3981
3988
|
return createThreadCommentImpl(params, buildRunnerDataDeps());
|
|
3982
3989
|
}
|
|
@@ -4005,6 +4012,7 @@ function buildRunnerDeliveryDeps() {
|
|
|
4005
4012
|
providerEnvConfig,
|
|
4006
4013
|
discoverArchiveThreadForDestination,
|
|
4007
4014
|
listThreadComments,
|
|
4015
|
+
listThreadCommentsTail,
|
|
4008
4016
|
createThreadComment,
|
|
4009
4017
|
parseArchivedChatComment,
|
|
4010
4018
|
};
|
|
@@ -4377,6 +4385,7 @@ function buildRunnerRuntimeDeps() {
|
|
|
4377
4385
|
getTelegramUpdates,
|
|
4378
4386
|
normalizeLocalTelegramUpdate,
|
|
4379
4387
|
listThreadComments,
|
|
4388
|
+
listThreadCommentsTail,
|
|
4380
4389
|
applyPendingAgeSelection,
|
|
4381
4390
|
normalizeArchiveCommentRecord,
|
|
4382
4391
|
isInboundArchiveKind,
|
|
@@ -4819,9 +4828,67 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
|
|
|
4819
4828
|
pending_count: pending.pending.length,
|
|
4820
4829
|
};
|
|
4821
4830
|
}
|
|
4831
|
+
const maybeBuildDuplicateArchivedSkip = (selectedRecord, routeStateForDuplicate) => {
|
|
4832
|
+
const selectedParsed = safeObject(selectedRecord?.parsedArchive);
|
|
4833
|
+
const selectedLogicalKey = buildProcessableArchiveLogicalKey(selectedRecord);
|
|
4834
|
+
if (!selectedLogicalKey) {
|
|
4835
|
+
return null;
|
|
4836
|
+
}
|
|
4837
|
+
const earlierDuplicate = findEarlierProcessableArchiveDuplicate(pending.ordered, selectedRecord);
|
|
4838
|
+
if (earlierDuplicate) {
|
|
4839
|
+
return {
|
|
4840
|
+
id: selectedRecord.id,
|
|
4841
|
+
reason: "duplicate_archived_source_message",
|
|
4842
|
+
messageID: intFromRawAllowZero(selectedParsed.messageID, 0),
|
|
4843
|
+
diagnosticType: "skip",
|
|
4844
|
+
contextExcluded: true,
|
|
4845
|
+
action: "skip_duplicate_archived_message",
|
|
4846
|
+
closedReason: "",
|
|
4847
|
+
};
|
|
4848
|
+
}
|
|
4849
|
+
const duplicateState = safeObject(routeStateForDuplicate);
|
|
4850
|
+
const lastSourceMessageID = intFromRawAllowZero(duplicateState.last_source_message_id, 0);
|
|
4851
|
+
const lastSourceKind = String(duplicateState.last_source_kind || "").trim().toLowerCase();
|
|
4852
|
+
const selectedMessageID = intFromRawAllowZero(selectedParsed.messageID, 0);
|
|
4853
|
+
const selectedKind = String(selectedParsed.kind || "").trim().toLowerCase();
|
|
4854
|
+
const lastCommentID = String(duplicateState.last_processed_comment_id || "").trim();
|
|
4855
|
+
if (
|
|
4856
|
+
selectedMessageID > 0
|
|
4857
|
+
&& lastSourceMessageID > 0
|
|
4858
|
+
&& selectedMessageID === lastSourceMessageID
|
|
4859
|
+
&& selectedKind
|
|
4860
|
+
&& lastSourceKind === selectedKind
|
|
4861
|
+
&& lastCommentID
|
|
4862
|
+
&& lastCommentID !== String(selectedRecord?.id || "").trim()
|
|
4863
|
+
) {
|
|
4864
|
+
return {
|
|
4865
|
+
id: selectedRecord.id,
|
|
4866
|
+
reason: "duplicate_archived_source_message",
|
|
4867
|
+
messageID: selectedMessageID,
|
|
4868
|
+
diagnosticType: "skip",
|
|
4869
|
+
contextExcluded: true,
|
|
4870
|
+
action: "skip_duplicate_archived_message",
|
|
4871
|
+
closedReason: "",
|
|
4872
|
+
};
|
|
4873
|
+
}
|
|
4874
|
+
return null;
|
|
4875
|
+
};
|
|
4822
4876
|
if (deferExecution) {
|
|
4823
4877
|
const skippedRecords = [];
|
|
4824
4878
|
for (const selectedRecord of pending.pending) {
|
|
4879
|
+
const duplicateArchivedSkip = maybeBuildDuplicateArchivedSkip(selectedRecord, refreshedState);
|
|
4880
|
+
if (duplicateArchivedSkip) {
|
|
4881
|
+
saveRunnerRouteState(
|
|
4882
|
+
routeKey,
|
|
4883
|
+
buildRunnerRouteStateFromComment(selectedRecord, {
|
|
4884
|
+
last_action: "duplicate_skipped",
|
|
4885
|
+
last_reason: String(duplicateArchivedSkip.reason || "duplicate_archived_source_message").trim(),
|
|
4886
|
+
last_trigger: "archive_dedupe",
|
|
4887
|
+
}),
|
|
4888
|
+
);
|
|
4889
|
+
skippedRecords.push(duplicateArchivedSkip);
|
|
4890
|
+
continue;
|
|
4891
|
+
}
|
|
4825
4892
|
const startupLoopSkipped = await maybeHandleRunnerStartupLoopCandidate({
|
|
4826
4893
|
routeKey,
|
|
4827
4894
|
normalizedRoute,
|
|
@@ -4929,6 +4996,19 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
|
|
|
4929
4996
|
}
|
|
4930
4997
|
const skippedRecords = [];
|
|
4931
4998
|
for (const selectedRecord of pending.pending) {
|
|
4999
|
+
const duplicateArchivedSkip = maybeBuildDuplicateArchivedSkip(selectedRecord, refreshedState);
|
|
5000
|
+
if (duplicateArchivedSkip) {
|
|
5001
|
+
saveRunnerRouteState(
|
|
5002
|
+
routeKey,
|
|
5003
|
+
buildRunnerRouteStateFromComment(selectedRecord, {
|
|
5004
|
+
last_action: "duplicate_skipped",
|
|
5005
|
+
last_reason: String(duplicateArchivedSkip.reason || "duplicate_archived_source_message").trim(),
|
|
5006
|
+
last_trigger: "archive_dedupe",
|
|
5007
|
+
}),
|
|
5008
|
+
);
|
|
5009
|
+
skippedRecords.push(duplicateArchivedSkip);
|
|
5010
|
+
continue;
|
|
5011
|
+
}
|
|
4932
5012
|
const startupLoopSkipped = await maybeHandleRunnerStartupLoopCandidate({
|
|
4933
5013
|
routeKey,
|
|
4934
5014
|
normalizedRoute,
|
|
@@ -11341,6 +11421,7 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
|
|
|
11341
11421
|
normalizeRunnerTriggerPolicy,
|
|
11342
11422
|
evaluateTelegramRunnerTrigger,
|
|
11343
11423
|
selectPendingArchiveComments,
|
|
11424
|
+
selectRunnerPendingWork,
|
|
11344
11425
|
processRunnerSelectedRecord,
|
|
11345
11426
|
resolveRunnerStartupLoopAdjudication,
|
|
11346
11427
|
runRunnerAIExecution,
|
package/lib/runner-data.mjs
CHANGED
|
@@ -348,6 +348,55 @@ export async function listThreadComments(
|
|
|
348
348
|
return items;
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
export async function listThreadCommentsTail(
|
|
352
|
+
{
|
|
353
|
+
siteBaseURL,
|
|
354
|
+
threadID,
|
|
355
|
+
token,
|
|
356
|
+
timeoutSeconds,
|
|
357
|
+
tailLimit = 200,
|
|
358
|
+
scanLimit = 5000,
|
|
359
|
+
actorUserID = "",
|
|
360
|
+
},
|
|
361
|
+
deps,
|
|
362
|
+
) {
|
|
363
|
+
const getJSONWithAuthHeaders = requireDependency(deps, "getJSONWithAuthHeaders");
|
|
364
|
+
const extraHeaders = actorUserID ? { "X-Actor-User-Id": actorUserID } : {};
|
|
365
|
+
const maxTail = Math.max(1, parsePositiveInt(tailLimit, 200));
|
|
366
|
+
const maxScan = Math.max(maxTail, parsePositiveInt(scanLimit, 5000));
|
|
367
|
+
const items = [];
|
|
368
|
+
let nextOffset = 0;
|
|
369
|
+
let scanned = 0;
|
|
370
|
+
let previousPageSignature = "";
|
|
371
|
+
|
|
372
|
+
while (scanned < maxScan) {
|
|
373
|
+
const pageSize = Math.min(100, maxScan - scanned);
|
|
374
|
+
if (pageSize <= 0) break;
|
|
375
|
+
const url = `${siteBaseURL}/api/v1/threads/${encodeURIComponent(threadID)}/comments?limit=${pageSize}&offset=${nextOffset}`;
|
|
376
|
+
const page = ensureArray(await getJSONWithAuthHeaders(url, timeoutSeconds, token, extraHeaders))
|
|
377
|
+
.map((row) => safeObject(row));
|
|
378
|
+
if (!page.length) {
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
const pageSignature = `${page.length}:${String(page[0]?.id || "").trim()}:${String(page[page.length - 1]?.id || "").trim()}`;
|
|
382
|
+
if (nextOffset > 0 && pageSignature === previousPageSignature) {
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
previousPageSignature = pageSignature;
|
|
386
|
+
items.push(...page);
|
|
387
|
+
if (items.length > maxTail) {
|
|
388
|
+
items.splice(0, items.length - maxTail);
|
|
389
|
+
}
|
|
390
|
+
scanned += page.length;
|
|
391
|
+
if (page.length < pageSize) {
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
nextOffset += page.length;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return items;
|
|
398
|
+
}
|
|
399
|
+
|
|
351
400
|
export async function createThreadComment(
|
|
352
401
|
{
|
|
353
402
|
siteBaseURL,
|
package/lib/runner-delivery.mjs
CHANGED
|
@@ -251,6 +251,9 @@ export async function performLocalBotDelivery({
|
|
|
251
251
|
const providerEnvConfig = requireDependency(deps, "providerEnvConfig");
|
|
252
252
|
const discoverArchiveThreadForDestination = requireDependency(deps, "discoverArchiveThreadForDestination");
|
|
253
253
|
const listThreadComments = requireDependency(deps, "listThreadComments");
|
|
254
|
+
const listThreadCommentsTail = typeof deps?.listThreadCommentsTail === "function"
|
|
255
|
+
? deps.listThreadCommentsTail
|
|
256
|
+
: listThreadComments;
|
|
254
257
|
const createThreadComment = requireDependency(deps, "createThreadComment");
|
|
255
258
|
const parseArchivedChatComment = requireDependency(deps, "parseArchivedChatComment");
|
|
256
259
|
|
|
@@ -352,12 +355,13 @@ export async function performLocalBotDelivery({
|
|
|
352
355
|
);
|
|
353
356
|
const archiveReplyToMessageID = intFromRawAllowZero(delivery.effectiveReplyToMessageID, replyToMessageID);
|
|
354
357
|
if (archiveDedupeOutbound && deliveredMessageID > 0) {
|
|
355
|
-
const existingComments = await
|
|
358
|
+
const existingComments = await listThreadCommentsTail({
|
|
356
359
|
siteBaseURL,
|
|
357
360
|
threadID: thread.threadID,
|
|
358
361
|
token,
|
|
359
362
|
timeoutSeconds,
|
|
360
|
-
|
|
363
|
+
tailLimit: 500,
|
|
364
|
+
scanLimit: 5000,
|
|
361
365
|
actorUserID,
|
|
362
366
|
});
|
|
363
367
|
const existingRecord = findArchivedBotReplyRecord(existingComments, {
|
package/lib/runner-helpers.mjs
CHANGED
|
@@ -192,15 +192,70 @@ function isRunnerProcessableArchiveKind(kind) {
|
|
|
192
192
|
return isInboundArchiveKind(kind) || String(kind || "").trim().toLowerCase() === "bot_reply";
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
export function buildProcessableArchiveLogicalKey(recordRaw) {
|
|
196
|
+
const record = safeObject(recordRaw);
|
|
197
|
+
const parsed = safeObject(record.parsedArchive);
|
|
198
|
+
const kind = String(parsed.kind || "").trim().toLowerCase();
|
|
199
|
+
if (!isRunnerProcessableArchiveKind(kind)) {
|
|
200
|
+
return "";
|
|
201
|
+
}
|
|
202
|
+
const chatID = firstNonEmptyString([parsed.chatID, parsed.chatId]);
|
|
203
|
+
const messageID = intFromRawAllowZero(parsed.messageID, 0);
|
|
204
|
+
if (!chatID || !(messageID > 0)) {
|
|
205
|
+
return "";
|
|
206
|
+
}
|
|
207
|
+
return `${kind}:${chatID}:${messageID}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function findEarlierProcessableArchiveDuplicate(records, selectedRecord) {
|
|
211
|
+
const selected = safeObject(selectedRecord);
|
|
212
|
+
const selectedID = String(selected.id || "").trim();
|
|
213
|
+
if (!selectedID) return null;
|
|
214
|
+
const selectedLogicalKey = buildProcessableArchiveLogicalKey(selected);
|
|
215
|
+
if (!selectedLogicalKey) return null;
|
|
216
|
+
const ordered = ensureArray(records).filter(Boolean).sort(compareArchiveCommentRecords);
|
|
217
|
+
for (const record of ordered) {
|
|
218
|
+
const recordID = String(safeObject(record).id || "").trim();
|
|
219
|
+
if (!recordID || recordID === selectedID) {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
if (buildProcessableArchiveLogicalKey(record) === selectedLogicalKey) {
|
|
223
|
+
return record;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function dedupeProcessableArchiveComments(records) {
|
|
230
|
+
const deduped = [];
|
|
231
|
+
const seenLogicalKeys = new Set();
|
|
232
|
+
for (const record of ensureArray(records)) {
|
|
233
|
+
const logicalKey = buildProcessableArchiveLogicalKey(record);
|
|
234
|
+
if (logicalKey) {
|
|
235
|
+
if (seenLogicalKeys.has(logicalKey)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
seenLogicalKeys.add(logicalKey);
|
|
239
|
+
}
|
|
240
|
+
deduped.push(record);
|
|
241
|
+
}
|
|
242
|
+
return deduped;
|
|
243
|
+
}
|
|
244
|
+
|
|
195
245
|
function isArchiveRecordAfterState(record, routeState) {
|
|
196
246
|
const state = safeObject(routeState);
|
|
197
247
|
const lastCommentID = String(state.last_processed_comment_id || "").trim();
|
|
198
248
|
const lastCreatedAt = String(state.last_processed_created_at || "").trim();
|
|
249
|
+
const createdAt = firstNonEmptyString([record.createdAt, record.updatedAt]);
|
|
250
|
+
if (lastCreatedAt && createdAt) {
|
|
251
|
+
if (createdAt > lastCreatedAt) return true;
|
|
252
|
+
if (createdAt < lastCreatedAt) return false;
|
|
253
|
+
return lastCommentID ? String(record.id || "").trim() !== lastCommentID : false;
|
|
254
|
+
}
|
|
199
255
|
if (lastCommentID) {
|
|
200
256
|
return String(record.id || "").trim() !== lastCommentID;
|
|
201
257
|
}
|
|
202
258
|
if (!lastCreatedAt) return true;
|
|
203
|
-
const createdAt = firstNonEmptyString([record.createdAt, record.updatedAt]);
|
|
204
259
|
if (!createdAt) return true;
|
|
205
260
|
return createdAt > lastCreatedAt;
|
|
206
261
|
}
|
|
@@ -244,7 +299,9 @@ export function selectPendingArchiveComments(records, routeState, mode, normaliz
|
|
|
244
299
|
.map((record) => normalizeArchiveCommentRecord(record))
|
|
245
300
|
.filter((record) => record.id && record.parsedArchive)
|
|
246
301
|
.sort(compareArchiveCommentRecords);
|
|
247
|
-
const processable =
|
|
302
|
+
const processable = dedupeProcessableArchiveComments(
|
|
303
|
+
ordered.filter((record) => isRunnerProcessableArchiveKind(record.parsedArchive.kind)),
|
|
304
|
+
);
|
|
248
305
|
const latest = processable.length ? processable[processable.length - 1] : null;
|
|
249
306
|
const state = safeObject(routeState);
|
|
250
307
|
const activeCommentID = String(state.active_comment_id || "").trim();
|
|
@@ -282,6 +339,19 @@ export function selectPendingArchiveComments(records, routeState, mode, normaliz
|
|
|
282
339
|
pending: processable.slice(activeIndex + 1),
|
|
283
340
|
}, rawOptions);
|
|
284
341
|
}
|
|
342
|
+
const activeRaw = ordered.find((record) => record.id === activeCommentID);
|
|
343
|
+
const activeLogicalKey = buildProcessableArchiveLogicalKey(activeRaw);
|
|
344
|
+
if (activeLogicalKey) {
|
|
345
|
+
const activeLogicalIndex = processable.findIndex((record) => buildProcessableArchiveLogicalKey(record) === activeLogicalKey);
|
|
346
|
+
if (activeLogicalIndex >= 0) {
|
|
347
|
+
return applyPendingAgeSelection({
|
|
348
|
+
ordered,
|
|
349
|
+
latest,
|
|
350
|
+
shouldPrime: false,
|
|
351
|
+
pending: processable.slice(activeLogicalIndex + 1),
|
|
352
|
+
}, rawOptions);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
285
355
|
}
|
|
286
356
|
|
|
287
357
|
const lastCommentID = String(state.last_processed_comment_id || "").trim();
|
|
@@ -295,6 +365,19 @@ export function selectPendingArchiveComments(records, routeState, mode, normaliz
|
|
|
295
365
|
pending: processable.slice(seenIndex + 1),
|
|
296
366
|
}, rawOptions);
|
|
297
367
|
}
|
|
368
|
+
const lastRaw = ordered.find((record) => record.id === lastCommentID);
|
|
369
|
+
const lastLogicalKey = buildProcessableArchiveLogicalKey(lastRaw);
|
|
370
|
+
if (lastLogicalKey) {
|
|
371
|
+
const seenLogicalIndex = processable.findIndex((record) => buildProcessableArchiveLogicalKey(record) === lastLogicalKey);
|
|
372
|
+
if (seenLogicalIndex >= 0) {
|
|
373
|
+
return applyPendingAgeSelection({
|
|
374
|
+
ordered,
|
|
375
|
+
latest,
|
|
376
|
+
shouldPrime: false,
|
|
377
|
+
pending: processable.slice(seenLogicalIndex + 1),
|
|
378
|
+
}, rawOptions);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
298
381
|
}
|
|
299
382
|
|
|
300
383
|
return applyPendingAgeSelection({
|
|
@@ -4,9 +4,10 @@ import {
|
|
|
4
4
|
buildRunnerContextWindow,
|
|
5
5
|
buildRunnerRouteStateFromComment,
|
|
6
6
|
compareArchiveCommentRecords,
|
|
7
|
+
dedupeProcessableArchiveComments,
|
|
7
8
|
isInboundArchiveKind,
|
|
8
|
-
selectPendingArchiveComments,
|
|
9
|
-
} from "./runner-helpers.mjs";
|
|
9
|
+
selectPendingArchiveComments,
|
|
10
|
+
} from "./runner-helpers.mjs";
|
|
10
11
|
import {
|
|
11
12
|
buildRunnerInputPayload,
|
|
12
13
|
evaluateTelegramRunnerTrigger,
|
|
@@ -3538,31 +3539,40 @@ export function selectRunnerPendingWork({
|
|
|
3538
3539
|
const normalizeArchiveCommentRecord = requireDependency(deps, "normalizeArchiveCommentRecord");
|
|
3539
3540
|
const applyPendingAgeSelection = requireDependency(deps, "applyPendingAgeSelection");
|
|
3540
3541
|
const orderedComments = ensureArray(comments)
|
|
3541
|
-
.map((record) => normalizeArchiveCommentRecord(record, parseArchivedChatComment))
|
|
3542
|
-
.filter((record) => record.id && record.parsedArchive)
|
|
3543
|
-
.sort(compareArchiveCommentRecords);
|
|
3544
|
-
const inboundComments = orderedComments.filter((record) => isInboundArchiveKind(record.parsedArchive.kind));
|
|
3545
|
-
const importedRecords =
|
|
3546
|
-
|
|
3547
|
-
.
|
|
3548
|
-
|
|
3542
|
+
.map((record) => normalizeArchiveCommentRecord(record, parseArchivedChatComment))
|
|
3543
|
+
.filter((record) => record.id && record.parsedArchive)
|
|
3544
|
+
.sort(compareArchiveCommentRecords);
|
|
3545
|
+
const inboundComments = orderedComments.filter((record) => isInboundArchiveKind(record.parsedArchive.kind));
|
|
3546
|
+
const importedRecords = dedupeProcessableArchiveComments(
|
|
3547
|
+
ensureArray(importOutcome?.importedCommentIDs)
|
|
3548
|
+
.map((commentID) => orderedComments.find((record) => record.id === commentID))
|
|
3549
|
+
.filter(Boolean),
|
|
3550
|
+
);
|
|
3551
|
+
const hasCursor = Boolean(
|
|
3552
|
+
String(safeObject(refreshedState).active_comment_id || "").trim()
|
|
3553
|
+
|| String(safeObject(refreshedState).active_comment_created_at || "").trim()
|
|
3554
|
+
|| String(safeObject(refreshedState).last_processed_comment_id || "").trim()
|
|
3555
|
+
|| String(safeObject(refreshedState).last_processed_created_at || "").trim(),
|
|
3556
|
+
);
|
|
3557
|
+
const fullPending = selectPendingArchiveComments(
|
|
3558
|
+
comments,
|
|
3559
|
+
refreshedState,
|
|
3560
|
+
mode,
|
|
3561
|
+
(record) => normalizeArchiveCommentRecord(record, parseArchivedChatComment),
|
|
3562
|
+
pendingSelectionOptions,
|
|
3563
|
+
);
|
|
3564
|
+
const pending = importedRecords.length > 0 && !hasCursor
|
|
3549
3565
|
? applyPendingAgeSelection({
|
|
3550
3566
|
ordered: orderedComments,
|
|
3551
3567
|
latest: inboundComments.length ? inboundComments[inboundComments.length - 1] : null,
|
|
3552
3568
|
shouldPrime: false,
|
|
3553
3569
|
pending: importedRecords,
|
|
3554
3570
|
}, pendingSelectionOptions)
|
|
3555
|
-
:
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
pendingSelectionOptions,
|
|
3561
|
-
);
|
|
3562
|
-
return {
|
|
3563
|
-
orderedComments,
|
|
3564
|
-
inboundComments,
|
|
3565
|
-
importedRecords,
|
|
3571
|
+
: fullPending;
|
|
3572
|
+
return {
|
|
3573
|
+
orderedComments,
|
|
3574
|
+
inboundComments,
|
|
3575
|
+
importedRecords,
|
|
3566
3576
|
pending,
|
|
3567
3577
|
};
|
|
3568
3578
|
}
|
package/lib/runner-runtime.mjs
CHANGED
|
@@ -198,6 +198,57 @@ function mergeRunnerSharedInboxUpdates(existingUpdates, incomingUpdates) {
|
|
|
198
198
|
.slice(-500);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
const RUNNER_INBOUND_ARCHIVE_RESERVATION_TTL_MS = 10 * 60 * 1000;
|
|
202
|
+
const runnerInboundArchiveReservations = new Map();
|
|
203
|
+
|
|
204
|
+
function buildRunnerInboundArchiveReservationKey(threadID, chatID, messageID) {
|
|
205
|
+
const normalizedThreadID = String(threadID || "").trim();
|
|
206
|
+
const normalizedChatID = String(chatID || "").trim();
|
|
207
|
+
const normalizedMessageID = intFromRawAllowZero(messageID, 0);
|
|
208
|
+
if (!normalizedThreadID || !normalizedChatID || !(normalizedMessageID > 0)) {
|
|
209
|
+
return "";
|
|
210
|
+
}
|
|
211
|
+
return `${normalizedThreadID}::${normalizedChatID}:${normalizedMessageID}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function cleanupRunnerInboundArchiveReservations(nowMs = Date.now()) {
|
|
215
|
+
for (const [key, expiresAtMs] of runnerInboundArchiveReservations.entries()) {
|
|
216
|
+
if (!(Number(expiresAtMs) > nowMs)) {
|
|
217
|
+
runnerInboundArchiveReservations.delete(key);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function reserveRunnerInboundArchiveMessage(threadID, chatID, messageID) {
|
|
223
|
+
const reservationKey = buildRunnerInboundArchiveReservationKey(threadID, chatID, messageID);
|
|
224
|
+
if (!reservationKey) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
reservationKey: "",
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
cleanupRunnerInboundArchiveReservations();
|
|
231
|
+
if (runnerInboundArchiveReservations.has(reservationKey)) {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
reservationKey,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
runnerInboundArchiveReservations.set(reservationKey, Date.now() + RUNNER_INBOUND_ARCHIVE_RESERVATION_TTL_MS);
|
|
238
|
+
return {
|
|
239
|
+
ok: true,
|
|
240
|
+
reservationKey,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function releaseRunnerInboundArchiveMessageReservation(reservationKey) {
|
|
245
|
+
const normalizedKey = String(reservationKey || "").trim();
|
|
246
|
+
if (!normalizedKey) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
runnerInboundArchiveReservations.delete(normalizedKey);
|
|
250
|
+
}
|
|
251
|
+
|
|
201
252
|
export async function maybeSendRunnerChatAction({
|
|
202
253
|
provider,
|
|
203
254
|
bot,
|
|
@@ -346,6 +397,9 @@ export async function archiveLocalTelegramMessagesForRoute({
|
|
|
346
397
|
const getTelegramUpdates = requireDependency(deps, "getTelegramUpdates");
|
|
347
398
|
const normalizeLocalTelegramUpdate = requireDependency(deps, "normalizeLocalTelegramUpdate");
|
|
348
399
|
const listThreadComments = requireDependency(deps, "listThreadComments");
|
|
400
|
+
const listThreadCommentsTail = typeof deps?.listThreadCommentsTail === "function"
|
|
401
|
+
? deps.listThreadCommentsTail
|
|
402
|
+
: listThreadComments;
|
|
349
403
|
const normalizeArchiveCommentRecord = requireDependency(deps, "normalizeArchiveCommentRecord");
|
|
350
404
|
const isInboundArchiveKind = requireDependency(deps, "isInboundArchiveKind");
|
|
351
405
|
const buildArchivedInboundMessageKey = requireDependency(deps, "buildArchivedInboundMessageKey");
|
|
@@ -417,7 +471,7 @@ export async function archiveLocalTelegramMessagesForRoute({
|
|
|
417
471
|
let handledUpdateID = lastUpdateID;
|
|
418
472
|
const mergedSharedUpdates = mergeRunnerSharedInboxUpdates(sharedInbox.updates, updates);
|
|
419
473
|
|
|
420
|
-
const persistPollingProgress = () => {
|
|
474
|
+
const persistPollingProgress = (remainingSharedUpdates = []) => {
|
|
421
475
|
if (sharedInboxKey && saveBotRunnerState) {
|
|
422
476
|
saveBotRunnerState({
|
|
423
477
|
sharedInboxes: {
|
|
@@ -425,7 +479,7 @@ export async function archiveLocalTelegramMessagesForRoute({
|
|
|
425
479
|
[sharedInboxKey]: serializeRunnerSharedInbox({
|
|
426
480
|
lastProviderUpdateID: handledUpdateID,
|
|
427
481
|
updatedAt: new Date().toISOString(),
|
|
428
|
-
updates:
|
|
482
|
+
updates: remainingSharedUpdates,
|
|
429
483
|
}),
|
|
430
484
|
},
|
|
431
485
|
});
|
|
@@ -447,12 +501,13 @@ export async function archiveLocalTelegramMessagesForRoute({
|
|
|
447
501
|
};
|
|
448
502
|
}
|
|
449
503
|
|
|
450
|
-
const existingComments = await
|
|
504
|
+
const existingComments = await listThreadCommentsTail({
|
|
451
505
|
siteBaseURL: runtime.baseURL,
|
|
452
506
|
threadID: archiveThread.threadID,
|
|
453
507
|
token: runtime.token,
|
|
454
508
|
timeoutSeconds: runtime.timeoutSeconds,
|
|
455
|
-
|
|
509
|
+
tailLimit: 500,
|
|
510
|
+
scanLimit: 5000,
|
|
456
511
|
actorUserID: runtime.actor.user_id,
|
|
457
512
|
});
|
|
458
513
|
const existingKeys = new Set(
|
|
@@ -466,10 +521,13 @@ export async function archiveLocalTelegramMessagesForRoute({
|
|
|
466
521
|
);
|
|
467
522
|
|
|
468
523
|
const importedCommentIDs = [];
|
|
469
|
-
|
|
524
|
+
const retainedSharedUpdates = [];
|
|
525
|
+
for (let index = 0; index < mergedSharedUpdates.length; index += 1) {
|
|
526
|
+
const update = mergedSharedUpdates[index];
|
|
470
527
|
const updateID = intFromRawAllowZero(update.updateID, 0);
|
|
471
528
|
if (String(update.chatID || "").trim() !== String(destination.chatID || "").trim()) {
|
|
472
529
|
handledUpdateID = Math.max(handledUpdateID, updateID);
|
|
530
|
+
retainedSharedUpdates.push(update);
|
|
473
531
|
continue;
|
|
474
532
|
}
|
|
475
533
|
const allowManagedBotImport = shouldImportManagedBotMessageForRoute({
|
|
@@ -491,6 +549,13 @@ export async function archiveLocalTelegramMessagesForRoute({
|
|
|
491
549
|
handledUpdateID = Math.max(handledUpdateID, updateID);
|
|
492
550
|
continue;
|
|
493
551
|
}
|
|
552
|
+
const reservation = boolFromRaw(archivePolicy.dedupeInbound, true)
|
|
553
|
+
? reserveRunnerInboundArchiveMessage(archiveThread?.threadID, update.chatID, update.messageID)
|
|
554
|
+
: { ok: true, reservationKey: "" };
|
|
555
|
+
if (!reservation.ok) {
|
|
556
|
+
handledUpdateID = Math.max(handledUpdateID, updateID);
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
494
559
|
let createdComment;
|
|
495
560
|
try {
|
|
496
561
|
createdComment = await createThreadComment({
|
|
@@ -502,7 +567,10 @@ export async function archiveLocalTelegramMessagesForRoute({
|
|
|
502
567
|
body: formatTelegramInboundArchiveComment(update),
|
|
503
568
|
});
|
|
504
569
|
} catch (err) {
|
|
505
|
-
|
|
570
|
+
releaseRunnerInboundArchiveMessageReservation(reservation.reservationKey);
|
|
571
|
+
persistPollingProgress(
|
|
572
|
+
mergeRunnerSharedInboxUpdates(retainedSharedUpdates, mergedSharedUpdates.slice(index)),
|
|
573
|
+
);
|
|
506
574
|
throw err;
|
|
507
575
|
}
|
|
508
576
|
if (String(createdComment.id || "").trim()) {
|
|
@@ -513,7 +581,7 @@ export async function archiveLocalTelegramMessagesForRoute({
|
|
|
513
581
|
}
|
|
514
582
|
handledUpdateID = Math.max(handledUpdateID, updateID);
|
|
515
583
|
}
|
|
516
|
-
persistPollingProgress();
|
|
584
|
+
persistPollingProgress(retainedSharedUpdates);
|
|
517
585
|
|
|
518
586
|
return {
|
|
519
587
|
importedCommentIDs,
|
|
@@ -50,6 +50,7 @@ export async function runSelftestRunnerScenarios(push, deps) {
|
|
|
50
50
|
const normalizeRunnerTriggerPolicy = requireDependency(deps, "normalizeRunnerTriggerPolicy");
|
|
51
51
|
const evaluateTelegramRunnerTrigger = requireDependency(deps, "evaluateTelegramRunnerTrigger");
|
|
52
52
|
const selectPendingArchiveComments = requireDependency(deps, "selectPendingArchiveComments");
|
|
53
|
+
const selectRunnerPendingWork = requireDependency(deps, "selectRunnerPendingWork");
|
|
53
54
|
const processRunnerSelectedRecord = requireDependency(deps, "processRunnerSelectedRecord");
|
|
54
55
|
const resolveRunnerStartupLoopAdjudication = requireDependency(deps, "resolveRunnerStartupLoopAdjudication");
|
|
55
56
|
const runRunnerAIExecution = requireDependency(deps, "runRunnerAIExecution");
|
|
@@ -348,6 +349,86 @@ export async function runSelftestRunnerScenarios(push, deps) {
|
|
|
348
349
|
);
|
|
349
350
|
}
|
|
350
351
|
|
|
352
|
+
try {
|
|
353
|
+
const pendingSelection = selectPendingArchiveComments(
|
|
354
|
+
[
|
|
355
|
+
{
|
|
356
|
+
id: "dup-comment-1",
|
|
357
|
+
createdAt: "2026-03-18T00:00:01.000Z",
|
|
358
|
+
updatedAt: "2026-03-18T00:00:01.000Z",
|
|
359
|
+
parsedArchive: { kind: "telegram_message", chatID: "-1001", messageID: 353, body: "@RyoAI_bot first copy" },
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
id: "dup-comment-2",
|
|
363
|
+
createdAt: "2026-03-18T00:00:02.000Z",
|
|
364
|
+
updatedAt: "2026-03-18T00:00:02.000Z",
|
|
365
|
+
parsedArchive: { kind: "telegram_message", chatID: "-1001", messageID: 353, body: "@RyoAI_bot duplicate copy" },
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
{
|
|
369
|
+
last_processed_comment_id: "dup-comment-1",
|
|
370
|
+
},
|
|
371
|
+
"start",
|
|
372
|
+
(record) => record,
|
|
373
|
+
);
|
|
374
|
+
push(
|
|
375
|
+
"runner_pending_selection_ignores_duplicate_archived_inbound_message_ids",
|
|
376
|
+
pendingSelection.pending.length === 0,
|
|
377
|
+
`pending=${pendingSelection.pending.map((item) => item.id).join(",") || "(none)"}`,
|
|
378
|
+
);
|
|
379
|
+
} catch (err) {
|
|
380
|
+
push(
|
|
381
|
+
"runner_pending_selection_ignores_duplicate_archived_inbound_message_ids",
|
|
382
|
+
false,
|
|
383
|
+
String(err?.message || err),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const duplicateComments = [
|
|
389
|
+
{
|
|
390
|
+
id: "dup-comment-earliest",
|
|
391
|
+
createdAt: "2026-03-18T00:00:01.000Z",
|
|
392
|
+
updatedAt: "2026-03-18T00:00:01.000Z",
|
|
393
|
+
parsedArchive: { kind: "telegram_message", chatID: "-1001", messageID: 353, body: "@RyoAI_bot first copy" },
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
id: "dup-comment-imported",
|
|
397
|
+
createdAt: "2026-03-18T00:00:05.000Z",
|
|
398
|
+
updatedAt: "2026-03-18T00:00:05.000Z",
|
|
399
|
+
parsedArchive: { kind: "telegram_message", chatID: "-1001", messageID: 353, body: "@RyoAI_bot imported duplicate" },
|
|
400
|
+
},
|
|
401
|
+
];
|
|
402
|
+
const pendingWork = selectRunnerPendingWork({
|
|
403
|
+
comments: duplicateComments,
|
|
404
|
+
importOutcome: {
|
|
405
|
+
importedCommentIDs: ["dup-comment-imported"],
|
|
406
|
+
},
|
|
407
|
+
refreshedState: {
|
|
408
|
+
last_processed_comment_id: "dup-comment-earliest",
|
|
409
|
+
last_processed_created_at: "2026-03-18T00:00:01.000Z",
|
|
410
|
+
},
|
|
411
|
+
mode: "start",
|
|
412
|
+
parseArchivedChatComment: () => null,
|
|
413
|
+
deps: {
|
|
414
|
+
normalizeArchiveCommentRecord: (record) => record,
|
|
415
|
+
applyPendingAgeSelection: (selection) => selection,
|
|
416
|
+
},
|
|
417
|
+
pendingSelectionOptions: {},
|
|
418
|
+
});
|
|
419
|
+
push(
|
|
420
|
+
"runner_pending_selection_does_not_requeue_imported_duplicate_after_cursor",
|
|
421
|
+
pendingWork.pending.pending.length === 0,
|
|
422
|
+
`pending=${pendingWork.pending.pending.map((item) => item.id).join(",") || "(none)"}`,
|
|
423
|
+
);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
push(
|
|
426
|
+
"runner_pending_selection_does_not_requeue_imported_duplicate_after_cursor",
|
|
427
|
+
false,
|
|
428
|
+
String(err?.message || err),
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
351
432
|
try {
|
|
352
433
|
const selected = selectProjectChatDestination(
|
|
353
434
|
[
|
|
@@ -106,17 +106,22 @@ async function startLocalTelegramRunnerSelftestServer({
|
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
const commentsMatch = pathname.match(/^\/api\/v1\/threads\/([^/]+)\/comments$/);
|
|
110
|
-
if (commentsMatch) {
|
|
111
|
-
const requestedThreadID = decodeURIComponent(String(commentsMatch[1] || "").trim());
|
|
112
|
-
if (requestedThreadID !== threadID) {
|
|
113
|
-
writeJSON(res, 404, { error: "thread not found" });
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
if (req.method === "GET") {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
109
|
+
const commentsMatch = pathname.match(/^\/api\/v1\/threads\/([^/]+)\/comments$/);
|
|
110
|
+
if (commentsMatch) {
|
|
111
|
+
const requestedThreadID = decodeURIComponent(String(commentsMatch[1] || "").trim());
|
|
112
|
+
if (requestedThreadID !== threadID) {
|
|
113
|
+
writeJSON(res, 404, { error: "thread not found" });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (req.method === "GET") {
|
|
117
|
+
const limit = Math.max(0, intFromRawAllowZero(requestURL.searchParams.get("limit"), state.comments.length || 0));
|
|
118
|
+
const offset = Math.max(0, intFromRawAllowZero(requestURL.searchParams.get("offset"), 0));
|
|
119
|
+
const paged = limit > 0
|
|
120
|
+
? state.comments.slice(offset, offset + limit)
|
|
121
|
+
: state.comments.slice(offset);
|
|
122
|
+
writeJSON(res, 200, paged);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
120
125
|
if (req.method === "POST") {
|
|
121
126
|
const payload = await readJSONBody(req);
|
|
122
127
|
const created = {
|
|
@@ -609,6 +614,260 @@ export async function runSelftestTelegramE2E(push, deps) {
|
|
|
609
614
|
`comments=${sharedInboxBodies.length} bodies=${sharedInboxBodies.join(" || ")}`,
|
|
610
615
|
);
|
|
611
616
|
|
|
617
|
+
telegramE2EServer.state.comments = [];
|
|
618
|
+
telegramE2EServer.state.updates = [
|
|
619
|
+
{
|
|
620
|
+
update_id: 330,
|
|
621
|
+
message: {
|
|
622
|
+
message_id: 79,
|
|
623
|
+
date: Math.floor(Date.now() / 1000),
|
|
624
|
+
chat: {
|
|
625
|
+
id: Number(e2eDestination.chat_id),
|
|
626
|
+
type: "supergroup",
|
|
627
|
+
title: e2eDestination.label,
|
|
628
|
+
},
|
|
629
|
+
from: {
|
|
630
|
+
id: 5001,
|
|
631
|
+
is_bot: false,
|
|
632
|
+
first_name: "Operator",
|
|
633
|
+
username: "operator_user",
|
|
634
|
+
},
|
|
635
|
+
text: `shared inbox prune @${String(e2eBot.name || "monitor_bot").replace(/^@+/, "")}`,
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
];
|
|
639
|
+
const pruneRoute = {
|
|
640
|
+
...e2eRoute,
|
|
641
|
+
name: "selftest-runner-e2e-shared-inbox-prune",
|
|
642
|
+
};
|
|
643
|
+
const pruneRouteKey = runnerRouteKey(normalizeRunnerRoute(pruneRoute));
|
|
644
|
+
await archiveLocalTelegramMessagesForRoute({
|
|
645
|
+
routeKey: pruneRouteKey,
|
|
646
|
+
route: pruneRoute,
|
|
647
|
+
routeState: {},
|
|
648
|
+
runtime: {
|
|
649
|
+
baseURL: telegramE2EServer.baseURL,
|
|
650
|
+
timeoutSeconds: 10,
|
|
651
|
+
token: e2eToken,
|
|
652
|
+
actor: {
|
|
653
|
+
user_id: e2eActorUserID,
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
bot: e2eBot,
|
|
657
|
+
destination: {
|
|
658
|
+
chatID: e2eDestination.chat_id,
|
|
659
|
+
},
|
|
660
|
+
archiveThread: {
|
|
661
|
+
threadID: e2eThreadID,
|
|
662
|
+
},
|
|
663
|
+
deps: buildRunnerRuntimeDeps(),
|
|
664
|
+
});
|
|
665
|
+
telegramE2EServer.state.updates = [];
|
|
666
|
+
const pruneRouteState = safeObject(loadBotRunnerState().routes[pruneRouteKey]);
|
|
667
|
+
await archiveLocalTelegramMessagesForRoute({
|
|
668
|
+
routeKey: pruneRouteKey,
|
|
669
|
+
route: pruneRoute,
|
|
670
|
+
routeState: pruneRouteState,
|
|
671
|
+
runtime: {
|
|
672
|
+
baseURL: telegramE2EServer.baseURL,
|
|
673
|
+
timeoutSeconds: 10,
|
|
674
|
+
token: e2eToken,
|
|
675
|
+
actor: {
|
|
676
|
+
user_id: e2eActorUserID,
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
bot: e2eBot,
|
|
680
|
+
destination: {
|
|
681
|
+
chatID: e2eDestination.chat_id,
|
|
682
|
+
},
|
|
683
|
+
archiveThread: {
|
|
684
|
+
threadID: e2eThreadID,
|
|
685
|
+
},
|
|
686
|
+
deps: buildRunnerRuntimeDeps(),
|
|
687
|
+
});
|
|
688
|
+
const prunedSharedBodies = telegramE2EServer.state.comments.map((item) => String(item.body || ""));
|
|
689
|
+
push(
|
|
690
|
+
"telegram_shared_inbox_prunes_handled_updates_after_import",
|
|
691
|
+
prunedSharedBodies.filter((item) => item.includes("message_id: 79")).length === 1,
|
|
692
|
+
`count=${prunedSharedBodies.filter((item) => item.includes("message_id: 79")).length} comments=${prunedSharedBodies.length}`,
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
telegramE2EServer.state.comments = Array.from({ length: 240 }, (_, index) => ({
|
|
696
|
+
id: `historical-comment-${index + 1}`,
|
|
697
|
+
body: [
|
|
698
|
+
"[Telegram message]",
|
|
699
|
+
`chat_id: ${e2eDestination.chat_id}`,
|
|
700
|
+
"chat_type: supergroup",
|
|
701
|
+
`message_id: ${1000 + index}`,
|
|
702
|
+
`occurred_at: ${new Date(Date.now() - ((241 - index) * 1000)).toISOString()}`,
|
|
703
|
+
`sender_id: ${5000 + index}`,
|
|
704
|
+
`sender: Historical User ${index + 1}`,
|
|
705
|
+
"sender_is_bot: false",
|
|
706
|
+
"",
|
|
707
|
+
`historical message ${index + 1}`,
|
|
708
|
+
].join("\n"),
|
|
709
|
+
created_at: new Date(Date.now() - ((241 - index) * 1000)).toISOString(),
|
|
710
|
+
updated_at: new Date(Date.now() - ((241 - index) * 1000)).toISOString(),
|
|
711
|
+
author_user_id: e2eActorUserID,
|
|
712
|
+
}));
|
|
713
|
+
telegramE2EServer.state.comments.push({
|
|
714
|
+
id: "historical-tail-duplicate",
|
|
715
|
+
body: [
|
|
716
|
+
"[Telegram message]",
|
|
717
|
+
`chat_id: ${e2eDestination.chat_id}`,
|
|
718
|
+
"chat_type: supergroup",
|
|
719
|
+
"message_id: 353",
|
|
720
|
+
`occurred_at: ${new Date().toISOString()}`,
|
|
721
|
+
"sender_id: 5001",
|
|
722
|
+
"sender: Operator",
|
|
723
|
+
"sender_is_bot: false",
|
|
724
|
+
"telegram_username: @operator_user",
|
|
725
|
+
"",
|
|
726
|
+
"tail duplicate should dedupe",
|
|
727
|
+
].join("\n"),
|
|
728
|
+
created_at: new Date().toISOString(),
|
|
729
|
+
updated_at: new Date().toISOString(),
|
|
730
|
+
author_user_id: e2eActorUserID,
|
|
731
|
+
});
|
|
732
|
+
telegramE2EServer.state.updates = [
|
|
733
|
+
{
|
|
734
|
+
update_id: 351,
|
|
735
|
+
message: {
|
|
736
|
+
message_id: 353,
|
|
737
|
+
date: Math.floor(Date.now() / 1000),
|
|
738
|
+
chat: {
|
|
739
|
+
id: Number(e2eDestination.chat_id),
|
|
740
|
+
type: "supergroup",
|
|
741
|
+
title: e2eDestination.label,
|
|
742
|
+
},
|
|
743
|
+
from: {
|
|
744
|
+
id: 5001,
|
|
745
|
+
is_bot: false,
|
|
746
|
+
first_name: "Operator",
|
|
747
|
+
username: "operator_user",
|
|
748
|
+
},
|
|
749
|
+
text: "tail duplicate should dedupe",
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
];
|
|
753
|
+
await archiveLocalTelegramMessagesForRoute({
|
|
754
|
+
routeKey: pruneRouteKey,
|
|
755
|
+
route: pruneRoute,
|
|
756
|
+
routeState: safeObject(loadBotRunnerState().routes[pruneRouteKey]),
|
|
757
|
+
runtime: {
|
|
758
|
+
baseURL: telegramE2EServer.baseURL,
|
|
759
|
+
timeoutSeconds: 10,
|
|
760
|
+
token: e2eToken,
|
|
761
|
+
actor: {
|
|
762
|
+
user_id: e2eActorUserID,
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
bot: e2eBot,
|
|
766
|
+
destination: {
|
|
767
|
+
chatID: e2eDestination.chat_id,
|
|
768
|
+
},
|
|
769
|
+
archiveThread: {
|
|
770
|
+
threadID: e2eThreadID,
|
|
771
|
+
},
|
|
772
|
+
deps: buildRunnerRuntimeDeps(),
|
|
773
|
+
});
|
|
774
|
+
push(
|
|
775
|
+
"telegram_inbound_archive_dedupe_checks_recent_thread_tail",
|
|
776
|
+
telegramE2EServer.state.comments.filter((item) => String(item.body || "").includes("message_id: 353")).length === 1,
|
|
777
|
+
`count=${telegramE2EServer.state.comments.filter((item) => String(item.body || "").includes("message_id: 353")).length}`,
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
telegramE2EServer.state.comments = [];
|
|
781
|
+
telegramE2EServer.state.updates = [
|
|
782
|
+
{
|
|
783
|
+
update_id: 350,
|
|
784
|
+
message: {
|
|
785
|
+
message_id: 91,
|
|
786
|
+
date: Math.floor(Date.now() / 1000),
|
|
787
|
+
chat: {
|
|
788
|
+
id: Number(e2eDestination.chat_id),
|
|
789
|
+
type: "supergroup",
|
|
790
|
+
title: e2eDestination.label,
|
|
791
|
+
},
|
|
792
|
+
from: {
|
|
793
|
+
id: 5001,
|
|
794
|
+
is_bot: false,
|
|
795
|
+
first_name: "Operator",
|
|
796
|
+
username: "operator_user",
|
|
797
|
+
},
|
|
798
|
+
text: "same inbound message for concurrent routes",
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
];
|
|
802
|
+
const concurrentRouteA = {
|
|
803
|
+
...e2eRoute,
|
|
804
|
+
name: "selftest-runner-e2e-concurrent-archive-a",
|
|
805
|
+
server_bot_name: "ConcurrentBotA",
|
|
806
|
+
};
|
|
807
|
+
const concurrentRouteB = {
|
|
808
|
+
...e2eRoute,
|
|
809
|
+
name: "selftest-runner-e2e-concurrent-archive-b",
|
|
810
|
+
server_bot_name: "ConcurrentBotB",
|
|
811
|
+
};
|
|
812
|
+
await Promise.all([
|
|
813
|
+
archiveLocalTelegramMessagesForRoute({
|
|
814
|
+
routeKey: runnerRouteKey(normalizeRunnerRoute(concurrentRouteA)),
|
|
815
|
+
route: concurrentRouteA,
|
|
816
|
+
routeState: {},
|
|
817
|
+
runtime: {
|
|
818
|
+
baseURL: telegramE2EServer.baseURL,
|
|
819
|
+
timeoutSeconds: 10,
|
|
820
|
+
token: e2eToken,
|
|
821
|
+
actor: {
|
|
822
|
+
user_id: e2eActorUserID,
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
bot: {
|
|
826
|
+
id: "concurrent-bot-a",
|
|
827
|
+
name: "ConcurrentBotA",
|
|
828
|
+
role: "monitor",
|
|
829
|
+
},
|
|
830
|
+
destination: {
|
|
831
|
+
chatID: e2eDestination.chat_id,
|
|
832
|
+
},
|
|
833
|
+
archiveThread: {
|
|
834
|
+
threadID: e2eThreadID,
|
|
835
|
+
},
|
|
836
|
+
deps: buildRunnerRuntimeDeps(),
|
|
837
|
+
}),
|
|
838
|
+
archiveLocalTelegramMessagesForRoute({
|
|
839
|
+
routeKey: runnerRouteKey(normalizeRunnerRoute(concurrentRouteB)),
|
|
840
|
+
route: concurrentRouteB,
|
|
841
|
+
routeState: {},
|
|
842
|
+
runtime: {
|
|
843
|
+
baseURL: telegramE2EServer.baseURL,
|
|
844
|
+
timeoutSeconds: 10,
|
|
845
|
+
token: e2eToken,
|
|
846
|
+
actor: {
|
|
847
|
+
user_id: e2eActorUserID,
|
|
848
|
+
},
|
|
849
|
+
},
|
|
850
|
+
bot: {
|
|
851
|
+
id: "concurrent-bot-b",
|
|
852
|
+
name: "ConcurrentBotB",
|
|
853
|
+
role: "monitor",
|
|
854
|
+
},
|
|
855
|
+
destination: {
|
|
856
|
+
chatID: e2eDestination.chat_id,
|
|
857
|
+
},
|
|
858
|
+
archiveThread: {
|
|
859
|
+
threadID: e2eThreadID,
|
|
860
|
+
},
|
|
861
|
+
deps: buildRunnerRuntimeDeps(),
|
|
862
|
+
}),
|
|
863
|
+
]);
|
|
864
|
+
const concurrentBodies = telegramE2EServer.state.comments.map((item) => String(item.body || ""));
|
|
865
|
+
push(
|
|
866
|
+
"telegram_concurrent_routes_archive_same_inbound_message_only_once",
|
|
867
|
+
concurrentBodies.filter((item) => item.includes("message_id: 91")).length === 1,
|
|
868
|
+
`count=${concurrentBodies.filter((item) => item.includes("message_id: 91")).length} comments=${concurrentBodies.length}`,
|
|
869
|
+
);
|
|
870
|
+
|
|
612
871
|
telegramE2EServer.state.comments = [];
|
|
613
872
|
telegramE2EServer.state.updates = [
|
|
614
873
|
{
|