metheus-governance-mcp-cli 0.2.273 → 0.2.274

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 CHANGED
@@ -3333,37 +3333,46 @@ function normalizeBotRunnerConsumedComments(rawConsumed, nowMs = Date.now()) {
3333
3333
  return normalized;
3334
3334
  }
3335
3335
 
3336
- function buildRunnerConsumedCommentLedgerKey(commentIDRaw, routeKeyRaw = "", commentKindRaw = "") {
3337
- const commentID = String(commentIDRaw || "").trim();
3338
- if (!commentID) return "";
3339
- const routeKey = String(routeKeyRaw || "").trim();
3340
- const commentKind = String(commentKindRaw || "").trim().toLowerCase();
3341
- if (commentKind === "bot_reply" && routeKey) {
3342
- return `${commentID}::${routeKey}`;
3343
- }
3344
- return commentID;
3345
- }
3346
-
3347
- function findRunnerConsumedCommentEntry(rawConsumed, commentIDRaw, { routeKey = "", commentKind = "" } = {}) {
3348
- const commentID = String(commentIDRaw || "").trim();
3349
- if (!commentID) return {};
3350
- const consumedComments = normalizeBotRunnerConsumedComments(rawConsumed);
3351
- const ledgerKey = buildRunnerConsumedCommentLedgerKey(commentID, routeKey, commentKind);
3352
- const directEntry = safeObject(consumedComments[ledgerKey]);
3353
- if (Object.keys(directEntry).length) {
3354
- return directEntry;
3355
- }
3356
- const normalizedKind = String(commentKind || "").trim().toLowerCase();
3357
- if (normalizedKind === "bot_reply" && String(routeKey || "").trim()) {
3358
- const routeEntry = Object.values(consumedComments).find((entryRaw) => {
3359
- const entry = safeObject(entryRaw);
3360
- return String(entry.comment_id || "").trim() === commentID
3361
- && String(entry.route_key || "").trim() === String(routeKey || "").trim();
3362
- });
3363
- return safeObject(routeEntry);
3364
- }
3365
- return safeObject(consumedComments[commentID]);
3366
- }
3336
+ function buildRunnerConsumedCommentLedgerKey(commentIDRaw, routeKeyRaw = "", commentKindRaw = "") {
3337
+ const commentID = String(commentIDRaw || "").trim();
3338
+ if (!commentID) return "";
3339
+ const routeKey = String(routeKeyRaw || "").trim();
3340
+ if (routeKey) {
3341
+ return `${commentID}::${routeKey}`;
3342
+ }
3343
+ return commentID;
3344
+ }
3345
+
3346
+ function findRunnerConsumedCommentEntry(rawConsumed, commentIDRaw, { routeKey = "", commentKind = "" } = {}) {
3347
+ const commentID = String(commentIDRaw || "").trim();
3348
+ if (!commentID) return {};
3349
+ const consumedComments = normalizeBotRunnerConsumedComments(rawConsumed);
3350
+ const normalizedRouteKey = String(routeKey || "").trim();
3351
+ const ledgerKey = buildRunnerConsumedCommentLedgerKey(commentID, normalizedRouteKey, commentKind);
3352
+ const directEntry = safeObject(consumedComments[ledgerKey]);
3353
+ if (Object.keys(directEntry).length) {
3354
+ return directEntry;
3355
+ }
3356
+ if (normalizedRouteKey) {
3357
+ const routeEntry = Object.values(consumedComments).find((entryRaw) => {
3358
+ const entry = safeObject(entryRaw);
3359
+ return String(entry.comment_id || "").trim() === commentID
3360
+ && String(entry.route_key || "").trim() === normalizedRouteKey;
3361
+ });
3362
+ if (Object.keys(safeObject(routeEntry)).length > 0) {
3363
+ return safeObject(routeEntry);
3364
+ }
3365
+ }
3366
+ const legacyGlobalEntry = safeObject(consumedComments[commentID]);
3367
+ if (!Object.keys(legacyGlobalEntry).length) {
3368
+ return {};
3369
+ }
3370
+ const legacyRouteKey = String(legacyGlobalEntry.route_key || "").trim();
3371
+ if (normalizedRouteKey && legacyRouteKey && legacyRouteKey !== normalizedRouteKey) {
3372
+ return {};
3373
+ }
3374
+ return legacyGlobalEntry;
3375
+ }
3367
3376
 
3368
3377
  function runnerRouteMatchesProjectConversationScope(candidateRouteRaw, normalizedRouteRaw) {
3369
3378
  const candidateRoute = normalizeRunnerRoute(candidateRouteRaw);
@@ -51,6 +51,100 @@ function intFromRawAllowZero(raw, fallback = 0) {
51
51
  return Number.isFinite(parsed) ? parsed : fallback;
52
52
  }
53
53
 
54
+ function buildRunnerArchiveSourceMessageKey(recordRaw) {
55
+ const parsed = safeObject(safeObject(recordRaw).parsedArchive);
56
+ const chatID = String(parsed.chatID || parsed.chatId || "").trim();
57
+ const messageID = intFromRawAllowZero(parsed.messageID || parsed.messageId, 0);
58
+ if (!chatID || !(messageID > 0)) {
59
+ return "";
60
+ }
61
+ return `${chatID}:${messageID}`;
62
+ }
63
+
64
+ function buildRunnerLocalInboundReceiptKey(receiptRaw) {
65
+ const receipt = safeObject(receiptRaw);
66
+ const chatID = String(receipt.chat_id || receipt.chatID || "").trim();
67
+ const messageID = intFromRawAllowZero(receipt.message_id || receipt.messageID, 0);
68
+ if (!chatID || !(messageID > 0)) {
69
+ return "";
70
+ }
71
+ return `${chatID}:${messageID}`;
72
+ }
73
+
74
+ function buildRunnerReceiptReplaySortTime(receiptRaw) {
75
+ const receipt = safeObject(receiptRaw);
76
+ const receiptTime = Date.parse(firstNonEmptyString([
77
+ receipt.occurred_at,
78
+ receipt.occurredAt,
79
+ receipt.received_at,
80
+ receipt.receivedAt,
81
+ ]));
82
+ return Number.isFinite(receiptTime) ? receiptTime : 0;
83
+ }
84
+
85
+ function buildRunnerReceiptBackedReplayRecord(recordRaw, receiptRaw, pendingSelectionOptions = {}) {
86
+ const record = safeObject(recordRaw);
87
+ const receipt = safeObject(receiptRaw);
88
+ const replayOccurredAt = firstNonEmptyString([
89
+ receipt.occurred_at,
90
+ receipt.occurredAt,
91
+ receipt.received_at,
92
+ receipt.receivedAt,
93
+ record.sourceOccurredAt,
94
+ safeObject(record.parsedArchive).occurredAt,
95
+ safeObject(record.parsedArchive).occurred_at,
96
+ record.createdAt,
97
+ record.updatedAt,
98
+ ]);
99
+ let staleAfterAt = String(record.staleAfterAt || "").trim();
100
+ const maxPendingAgeMs = intFromRawAllowZero(safeObject(pendingSelectionOptions).maxPendingAgeMs, 0);
101
+ const replayOccurredAtMs = Date.parse(replayOccurredAt);
102
+ if (!staleAfterAt && maxPendingAgeMs > 0 && Number.isFinite(replayOccurredAtMs)) {
103
+ staleAfterAt = new Date(replayOccurredAtMs + maxPendingAgeMs).toISOString();
104
+ }
105
+ return {
106
+ ...record,
107
+ sourceOccurredAt: replayOccurredAt,
108
+ staleAfterAt,
109
+ replayTriggeredByLocalReceipt: true,
110
+ };
111
+ }
112
+
113
+ function buildRunnerReceiptBackedPendingArchiveComments({
114
+ orderedComments,
115
+ currentPollLocalInboundReceipts,
116
+ existingPendingIDs,
117
+ pendingSelectionOptions,
118
+ }) {
119
+ const pendingIDs = new Set(ensureArray(existingPendingIDs).map((value) => String(value || "").trim()).filter(Boolean));
120
+ const latestReceiptsByKey = new Map();
121
+ for (const receiptRaw of ensureArray(currentPollLocalInboundReceipts)) {
122
+ const receipt = safeObject(receiptRaw);
123
+ const receiptKey = buildRunnerLocalInboundReceiptKey(receipt);
124
+ if (!receiptKey) {
125
+ continue;
126
+ }
127
+ const previous = safeObject(latestReceiptsByKey.get(receiptKey));
128
+ if (buildRunnerReceiptReplaySortTime(receipt) >= buildRunnerReceiptReplaySortTime(previous)) {
129
+ latestReceiptsByKey.set(receiptKey, receipt);
130
+ }
131
+ }
132
+ const replayCandidates = [];
133
+ for (const [receiptKey, receipt] of latestReceiptsByKey.entries()) {
134
+ const matchedRecord = orderedComments.find((record) => buildRunnerArchiveSourceMessageKey(record) === receiptKey);
135
+ const matchedID = String(safeObject(matchedRecord).id || "").trim();
136
+ if (!matchedID || pendingIDs.has(matchedID)) {
137
+ continue;
138
+ }
139
+ replayCandidates.push(buildRunnerReceiptBackedReplayRecord(
140
+ matchedRecord,
141
+ receipt,
142
+ pendingSelectionOptions,
143
+ ));
144
+ }
145
+ return replayCandidates.sort(compareArchiveCommentRecords);
146
+ }
147
+
54
148
  function buildContextSpeakerType(parsedArchiveRaw) {
55
149
  const parsed = safeObject(parsedArchiveRaw);
56
150
  const kind = String(parsed.kind || "").trim();
@@ -862,10 +956,24 @@ export function selectRunnerPendingWork({
862
956
  pending: importedPendingAfterCursor,
863
957
  }, pendingSelectionOptions)
864
958
  : fullPending;
959
+ const receiptBackedPending = buildRunnerReceiptBackedPendingArchiveComments({
960
+ orderedComments,
961
+ currentPollLocalInboundReceipts: ensureArray(importOutcome?.currentPollLocalInboundReceipts),
962
+ existingPendingIDs: ensureArray(safeObject(pending).pending).map((record) => String(safeObject(record).id || "").trim()),
963
+ pendingSelectionOptions,
964
+ });
965
+ const finalPending = ensureArray(safeObject(pending).pending).length === 0 && receiptBackedPending.length > 0
966
+ ? applyPendingAgeSelection({
967
+ ...safeObject(pending),
968
+ shouldPrime: false,
969
+ pending: receiptBackedPending,
970
+ }, pendingSelectionOptions)
971
+ : pending;
865
972
  return {
866
973
  orderedComments,
867
974
  inboundComments,
868
975
  importedRecords,
869
- pending,
976
+ receiptBackedPending,
977
+ pending: finalPending,
870
978
  };
871
979
  }
@@ -575,6 +575,14 @@ function groupRunnerLocalInboundArtifactsByRoute(localInboundArtifacts) {
575
575
  return Object.fromEntries(grouped.entries());
576
576
  }
577
577
 
578
+ function buildRunnerCurrentPollLocalInboundReceipts(localInboundArtifactsByRoute, routeKey) {
579
+ return ensureArray(safeObject(localInboundArtifactsByRoute)[String(routeKey || "").trim()])
580
+ .map((artifactRaw) => ensureArray(safeObject(artifactRaw).receiptEntry))
581
+ .filter((entry) => entry.length === 2)
582
+ .map((entry) => safeObject(entry[1]))
583
+ .filter((receipt) => Object.keys(receipt).length > 0);
584
+ }
585
+
578
586
  function buildRunnerRecentLocalInboundEnvelopes(routeStateRaw, recentLocalInboundReceipts) {
579
587
  const routeState = safeObject(routeStateRaw);
580
588
  const relevantEnvelopes = Object.values(safeObject(recentLocalInboundReceipts))
@@ -1144,6 +1152,10 @@ export async function archiveLocalTelegramMessagesForRoute({
1144
1152
  managedConversationBots,
1145
1153
  );
1146
1154
  const localInboundArtifactsByRoute = groupRunnerLocalInboundArtifactsByRoute(localInboundArtifacts);
1155
+ const currentPollLocalInboundReceipts = buildRunnerCurrentPollLocalInboundReceipts(
1156
+ localInboundArtifactsByRoute,
1157
+ routeKey,
1158
+ );
1147
1159
  const recentLocalInboundReceipts = buildRunnerRecentLocalInboundReceipts(
1148
1160
  routeState,
1149
1161
  ensureArray(localInboundArtifactsByRoute[routeKey]),
@@ -1205,7 +1217,9 @@ export async function archiveLocalTelegramMessagesForRoute({
1205
1217
  persistPollingProgress([], lastUpdateID);
1206
1218
  return {
1207
1219
  importedCommentIDs: [],
1220
+ importedComments: [],
1208
1221
  importedCount: 0,
1222
+ currentPollLocalInboundReceipts,
1209
1223
  lastUpdateID,
1210
1224
  };
1211
1225
  }
@@ -1249,6 +1263,7 @@ export async function archiveLocalTelegramMessagesForRoute({
1249
1263
  importedCommentIDs,
1250
1264
  importedComments,
1251
1265
  importedCount: importedCommentIDs.length,
1266
+ currentPollLocalInboundReceipts,
1252
1267
  lastUpdateID: Math.max(lastUpdateID, handledUpdateID),
1253
1268
  };
1254
1269
  }
@@ -1007,6 +1007,63 @@ export async function runSelftestRunnerScenarios(push, deps) {
1007
1007
  );
1008
1008
  }
1009
1009
 
1010
+ try {
1011
+ const pendingWork = selectRunnerPendingWork({
1012
+ comments: [
1013
+ {
1014
+ id: "archived-current-message",
1015
+ createdAt: "2026-03-18T00:00:01.000Z",
1016
+ updatedAt: "2026-03-18T00:00:01.000Z",
1017
+ parsedArchive: { kind: "telegram_message", chatID: "-1001", messageID: 280, body: "@RyoAI3_bot 하이" },
1018
+ },
1019
+ {
1020
+ id: "cursor-comment",
1021
+ createdAt: "2026-03-18T00:04:00.000Z",
1022
+ updatedAt: "2026-03-18T00:04:00.000Z",
1023
+ parsedArchive: { kind: "telegram_message", chatID: "-1001", messageID: 281, body: "@RyoAI_bot already processed" },
1024
+ },
1025
+ ],
1026
+ importOutcome: {
1027
+ importedCommentIDs: [],
1028
+ currentPollLocalInboundReceipts: [
1029
+ {
1030
+ chat_id: "-1001",
1031
+ message_id: 280,
1032
+ occurred_at: "2026-03-18T00:05:00.000Z",
1033
+ received_at: "2026-03-18T00:05:00.000Z",
1034
+ update_id: 2001,
1035
+ },
1036
+ ],
1037
+ },
1038
+ refreshedState: {
1039
+ last_processed_comment_id: "cursor-comment",
1040
+ last_processed_created_at: "2026-03-18T00:04:00.000Z",
1041
+ },
1042
+ mode: "start",
1043
+ parseArchivedChatComment: () => null,
1044
+ deps: {
1045
+ normalizeArchiveCommentRecord: (record) => record,
1046
+ applyPendingAgeSelection: (selection) => selection,
1047
+ },
1048
+ pendingSelectionOptions: {
1049
+ maxPendingAgeMs: 15 * 60 * 1000,
1050
+ },
1051
+ });
1052
+ push(
1053
+ "runner_pending_selection_replays_current_poll_local_receipt_against_existing_archive_comment",
1054
+ pendingWork.pending.pending.length === 1
1055
+ && String(pendingWork.pending.pending[0]?.id || "").trim() === "archived-current-message"
1056
+ && pendingWork.pending.pending[0]?.replayTriggeredByLocalReceipt === true,
1057
+ `pending=${pendingWork.pending.pending.map((item) => `${item.id}:${String(item.replayTriggeredByLocalReceipt === true)}`).join(",") || "(none)"}`,
1058
+ );
1059
+ } catch (err) {
1060
+ push(
1061
+ "runner_pending_selection_replays_current_poll_local_receipt_against_existing_archive_comment",
1062
+ false,
1063
+ String(err?.message || err),
1064
+ );
1065
+ }
1066
+
1010
1067
  try {
1011
1068
  const selected = selectProjectChatDestination(
1012
1069
  [
@@ -2042,10 +2099,16 @@ export async function runSelftestRunnerScenarios(push, deps) {
2042
2099
  selectedRecord: humanRecord,
2043
2100
  selectedBotUsernames: ["ryoai_bot"],
2044
2101
  normalizedIntent: "coordination_request",
2045
- });
2046
- const claimedState = loadBotRunnerState();
2047
- const claimedRequest = safeObject(safeObject(claimedState.requests)[claimed.requestKey]);
2048
- const claimedConsumed = safeObject(safeObject(claimedState.consumedComments)[humanRecord.id]);
2102
+ });
2103
+ const claimedState = loadBotRunnerState();
2104
+ const claimedRequest = safeObject(safeObject(claimedState.requests)[claimed.requestKey]);
2105
+ const claimedConsumed = safeObject(
2106
+ Object.values(safeObject(claimedState.consumedComments)).find((entryRaw) => {
2107
+ const entry = safeObject(entryRaw);
2108
+ return String(entry.comment_id || "").trim() === humanRecord.id
2109
+ && String(entry.route_key || "").trim() === requestRouteKey;
2110
+ }),
2111
+ );
2049
2112
  push(
2050
2113
  "runner_request_claim_persists_request_and_consumed_comment",
2051
2114
  claimed.ok === true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.273",
3
+ "version": "0.2.274",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [