metheus-governance-mcp-cli 0.2.248 → 0.2.250

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
@@ -140,6 +140,7 @@ import {
140
140
  buildRunnerRouteStateFromComment,
141
141
  buildProcessableArchiveLogicalKey,
142
142
  findEarlierProcessableArchiveDuplicate,
143
+ findRecentTelegramMessageEnvelope,
143
144
  isInboundArchiveKind,
144
145
  normalizeTelegramMessageEnvelope as normalizeRunnerTelegramMessageEnvelope,
145
146
  normalizeArchiveCommentRecord,
@@ -1989,6 +1990,7 @@ function mergeRunnerStateRecords(preferred, fallback) {
1989
1990
  last_followup_source_message_envelope: pickObjectField("last_followup_source_message_envelope"),
1990
1991
  last_followup_last_reply_message_envelope: pickObjectField("last_followup_last_reply_message_envelope"),
1991
1992
  last_followup_attempted_delivery_envelope: pickObjectField("last_followup_attempted_delivery_envelope"),
1993
+ recent_local_inbound_envelopes: pickObjectField("recent_local_inbound_envelopes"),
1992
1994
  last_contract_validation_targets: pickArrayField("last_contract_validation_targets", normalizeTelegramMentionUsername),
1993
1995
  last_normalized_execution_contract_targets: pickArrayField("last_normalized_execution_contract_targets", normalizeTelegramMentionUsername),
1994
1996
  last_normalized_execution_next_responders: pickArrayField("last_normalized_execution_next_responders", normalizeTelegramMentionUsername),
@@ -2075,9 +2077,10 @@ function migrateBotRunnerStateRoutes(routes, runnerConfig) {
2075
2077
  };
2076
2078
  }
2077
2079
 
2078
- function loadBotRunnerState() {
2079
- const filePath = botRunnerStateFilePath();
2080
- try {
2080
+ function loadBotRunnerState() {
2081
+ const filePath = botRunnerStateFilePath();
2082
+ waitForBotRunnerStateLockRelease(filePath);
2083
+ try {
2081
2084
  if (!fs.existsSync(filePath)) {
2082
2085
  return {
2083
2086
  filePath,
@@ -2127,8 +2130,97 @@ function loadBotRunnerState() {
2127
2130
  remainingAnonymousKeys: [],
2128
2131
  };
2129
2132
  }
2130
- }
2131
-
2133
+ }
2134
+
2135
+ function sleepSyncMs(delayMs) {
2136
+ const ms = Number(delayMs) || 0;
2137
+ if (!(ms > 0)) {
2138
+ return;
2139
+ }
2140
+ const sleepBuffer = new SharedArrayBuffer(4);
2141
+ const sleepArray = new Int32Array(sleepBuffer);
2142
+ Atomics.wait(sleepArray, 0, 0, ms);
2143
+ }
2144
+
2145
+ function botRunnerStateLockFilePath(filePath) {
2146
+ const normalizedPath = String(filePath || "").trim();
2147
+ return normalizedPath ? `${normalizedPath}.lock` : "";
2148
+ }
2149
+
2150
+ function tryReleaseStaleBotRunnerStateLock(lockPath, staleMs = 60000) {
2151
+ const normalizedPath = String(lockPath || "").trim();
2152
+ if (!normalizedPath) {
2153
+ return false;
2154
+ }
2155
+ try {
2156
+ const stats = fs.statSync(normalizedPath);
2157
+ const ageMs = Date.now() - Number(stats.mtimeMs || 0);
2158
+ if (Number.isFinite(ageMs) && ageMs >= staleMs) {
2159
+ fs.rmSync(normalizedPath, { force: true });
2160
+ return true;
2161
+ }
2162
+ } catch {}
2163
+ return false;
2164
+ }
2165
+
2166
+ function waitForBotRunnerStateLockRelease(filePath, timeoutMs = 5000, staleMs = 60000) {
2167
+ const lockPath = botRunnerStateLockFilePath(filePath);
2168
+ if (!lockPath) {
2169
+ return;
2170
+ }
2171
+ const deadlineMs = Date.now() + Math.max(0, Number(timeoutMs) || 0);
2172
+ while (fs.existsSync(lockPath)) {
2173
+ tryReleaseStaleBotRunnerStateLock(lockPath, staleMs);
2174
+ if (!fs.existsSync(lockPath)) {
2175
+ return;
2176
+ }
2177
+ if (Date.now() >= deadlineMs) {
2178
+ return;
2179
+ }
2180
+ sleepSyncMs(25);
2181
+ }
2182
+ }
2183
+
2184
+ function withBotRunnerStateFileLock(filePath, callback, timeoutMs = 5000, staleMs = 60000) {
2185
+ const lockPath = botRunnerStateLockFilePath(filePath);
2186
+ if (!lockPath) {
2187
+ return callback();
2188
+ }
2189
+ const deadlineMs = Date.now() + Math.max(0, Number(timeoutMs) || 0);
2190
+ let lockFD = null;
2191
+ while (lockFD === null) {
2192
+ try {
2193
+ lockFD = fs.openSync(lockPath, "wx");
2194
+ fs.writeFileSync(lockFD, `${process.pid} ${new Date().toISOString()}\n`, "utf8");
2195
+ break;
2196
+ } catch (error) {
2197
+ const errorCode = String(error?.code || "").trim().toUpperCase();
2198
+ if (!["EEXIST", "EPERM", "EBUSY"].includes(errorCode)) {
2199
+ throw error;
2200
+ }
2201
+ tryReleaseStaleBotRunnerStateLock(lockPath, staleMs);
2202
+ if (Date.now() >= deadlineMs) {
2203
+ throw error;
2204
+ }
2205
+ sleepSyncMs(25);
2206
+ }
2207
+ }
2208
+ try {
2209
+ return callback();
2210
+ } finally {
2211
+ try {
2212
+ if (lockFD !== null) {
2213
+ fs.closeSync(lockFD);
2214
+ }
2215
+ } catch {}
2216
+ try {
2217
+ if (fs.existsSync(lockPath)) {
2218
+ fs.rmSync(lockPath, { force: true });
2219
+ }
2220
+ } catch {}
2221
+ }
2222
+ }
2223
+
2132
2224
  function writeTextFileAtomic(filePath, text) {
2133
2225
  const normalizedPath = String(filePath || "").trim();
2134
2226
  if (!normalizedPath) {
@@ -2141,14 +2233,12 @@ function writeTextFileAtomic(filePath, text) {
2141
2233
  `.${path.basename(normalizedPath)}.${process.pid}.${Date.now()}.tmp`,
2142
2234
  );
2143
2235
  fs.writeFileSync(tempPath, text, "utf8");
2144
- const renameRetryDelaysMs = [0, 20, 50, 100, 200];
2145
- const sleepBuffer = new SharedArrayBuffer(4);
2146
- const sleepArray = new Int32Array(sleepBuffer);
2147
- let lastRenameError = null;
2236
+ const renameRetryDelaysMs = [0, 20, 50, 100, 200];
2237
+ let lastRenameError = null;
2148
2238
  try {
2149
2239
  for (const delayMs of renameRetryDelaysMs) {
2150
2240
  if (delayMs > 0) {
2151
- Atomics.wait(sleepArray, 0, 0, delayMs);
2241
+ sleepSyncMs(delayMs);
2152
2242
  }
2153
2243
  try {
2154
2244
  fs.renameSync(tempPath, normalizedPath);
@@ -2190,186 +2280,188 @@ function writeTextFileAtomic(filePath, text) {
2190
2280
  }
2191
2281
  }
2192
2282
 
2193
- function saveBotRunnerState(nextState) {
2194
- const filePath = botRunnerStateFilePath();
2195
- let current = {};
2196
- try {
2197
- current = safeObject(tryJsonParse(fs.readFileSync(filePath, "utf8")));
2198
- } catch {}
2199
- const stateEntryTimestampMs = (...values) => {
2200
- for (const value of values) {
2201
- const ms = Date.parse(String(value || "").trim());
2202
- if (Number.isFinite(ms)) {
2203
- return ms;
2204
- }
2205
- }
2206
- return 0;
2207
- };
2208
- const mergeRunnerStateRoutes = (currentRoutesRaw, nextRoutesRaw) => {
2209
- const currentRoutes = safeObject(currentRoutesRaw);
2210
- const nextRoutes = safeObject(nextRoutesRaw);
2211
- const merged = {
2212
- ...currentRoutes,
2213
- };
2214
- const mergeConversationSessions = (currentSessionsRaw, nextSessionsRaw) => {
2215
- const currentSessions = safeObject(currentSessionsRaw);
2216
- const nextSessions = safeObject(nextSessionsRaw);
2217
- const mergedSessions = {
2218
- ...currentSessions,
2219
- };
2220
- for (const [conversationID, nextSessionRaw] of Object.entries(nextSessions)) {
2221
- const currentSession = safeObject(currentSessions[conversationID]);
2222
- const nextSession = safeObject(nextSessionRaw);
2223
- if (!Object.keys(currentSession).length) {
2224
- mergedSessions[conversationID] = nextSession;
2225
- continue;
2226
- }
2227
- const currentMs = stateEntryTimestampMs(
2228
- currentSession.updated_at,
2229
- currentSession.last_activity_at,
2230
- currentSession.closed_at,
2231
- currentSession.started_at,
2232
- currentSession.expires_at,
2233
- );
2234
- const nextMs = stateEntryTimestampMs(
2235
- nextSession.updated_at,
2236
- nextSession.last_activity_at,
2237
- nextSession.closed_at,
2238
- nextSession.started_at,
2239
- nextSession.expires_at,
2240
- );
2241
- mergedSessions[conversationID] = nextMs >= currentMs
2242
- ? {
2243
- ...currentSession,
2244
- ...nextSession,
2245
- }
2246
- : {
2247
- ...nextSession,
2248
- ...currentSession,
2249
- };
2250
- }
2251
- return mergedSessions;
2252
- };
2253
- for (const [routeKey, nextRouteRaw] of Object.entries(nextRoutes)) {
2254
- const currentRoute = safeObject(currentRoutes[routeKey]);
2255
- const nextRoute = safeObject(nextRouteRaw);
2256
- if (!Object.keys(currentRoute).length) {
2257
- merged[routeKey] = nextRoute;
2258
- continue;
2259
- }
2260
- const preferredRoute = prefersRunnerStateRecord(nextRoute, currentRoute) ? nextRoute : currentRoute;
2261
- const fallbackRoute = preferredRoute === nextRoute ? currentRoute : nextRoute;
2262
- merged[routeKey] = cleanupRunnerStateRecord({
2263
- ...mergeRunnerStateRecords(preferredRoute, fallbackRoute),
2264
- conversation_sessions: mergeConversationSessions(
2265
- currentRoute.conversation_sessions,
2266
- nextRoute.conversation_sessions,
2267
- ),
2268
- });
2269
- }
2270
- return merged;
2271
- };
2272
- const mergeRunnerStateRequests = (currentRequestsRaw, nextRequestsRaw) => {
2273
- const currentRequests = normalizeBotRunnerRequests(currentRequestsRaw);
2274
- const nextRequests = normalizeBotRunnerRequests(nextRequestsRaw);
2275
- const merged = {
2276
- ...currentRequests,
2277
- };
2278
- for (const [requestKey, nextRequestRaw] of Object.entries(nextRequests)) {
2279
- const currentRequest = safeObject(currentRequests[requestKey]);
2280
- const nextRequest = safeObject(nextRequestRaw);
2281
- const currentMs = stateEntryTimestampMs(
2282
- currentRequest.updated_at,
2283
- currentRequest.completed_at,
2284
- currentRequest.closed_at,
2285
- currentRequest.claimed_at,
2286
- );
2287
- const nextMs = stateEntryTimestampMs(
2288
- nextRequest.updated_at,
2289
- nextRequest.completed_at,
2290
- nextRequest.closed_at,
2291
- nextRequest.claimed_at,
2292
- );
2293
- if (!Object.keys(currentRequest).length) {
2294
- merged[requestKey] = nextRequest;
2295
- continue;
2296
- }
2297
- merged[requestKey] = nextMs >= currentMs
2298
- ? {
2299
- ...currentRequest,
2300
- ...nextRequest,
2301
- }
2302
- : currentRequest;
2303
- }
2304
- return normalizeBotRunnerRequests(merged);
2305
- };
2306
- const mergeRunnerStateExcludedComments = (currentExcludedRaw, nextExcludedRaw) => {
2307
- const currentExcluded = normalizeBotRunnerExcludedComments(currentExcludedRaw);
2308
- const nextExcluded = normalizeBotRunnerExcludedComments(nextExcludedRaw);
2309
- const merged = {
2310
- ...currentExcluded,
2311
- };
2312
- for (const [commentID, nextEntryRaw] of Object.entries(nextExcluded)) {
2313
- const currentEntry = safeObject(currentExcluded[commentID]);
2314
- const nextEntry = safeObject(nextEntryRaw);
2315
- const currentMs = stateEntryTimestampMs(currentEntry.excluded_at);
2316
- const nextMs = stateEntryTimestampMs(nextEntry.excluded_at);
2317
- if (!Object.keys(currentEntry).length || nextMs >= currentMs) {
2318
- merged[commentID] = {
2319
- ...currentEntry,
2320
- ...nextEntry,
2321
- };
2322
- }
2323
- }
2324
- return normalizeBotRunnerExcludedComments(merged);
2325
- };
2326
- const mergeRunnerStateConsumedComments = (currentConsumedRaw, nextConsumedRaw) => {
2327
- const currentConsumed = normalizeBotRunnerConsumedComments(currentConsumedRaw);
2328
- const nextConsumed = normalizeBotRunnerConsumedComments(nextConsumedRaw);
2329
- const merged = {
2330
- ...currentConsumed,
2331
- };
2332
- for (const [commentID, nextEntryRaw] of Object.entries(nextConsumed)) {
2333
- const currentEntry = safeObject(currentConsumed[commentID]);
2334
- const nextEntry = safeObject(nextEntryRaw);
2335
- const currentMs = stateEntryTimestampMs(currentEntry.consumed_at);
2336
- const nextMs = stateEntryTimestampMs(nextEntry.consumed_at);
2337
- if (!Object.keys(currentEntry).length || nextMs >= currentMs) {
2338
- merged[commentID] = {
2339
- ...currentEntry,
2340
- ...nextEntry,
2341
- };
2342
- }
2343
- }
2344
- return normalizeBotRunnerConsumedComments(merged);
2345
- };
2346
- const payload = {
2347
- version: 1,
2348
- updated_at: new Date().toISOString(),
2349
- routes: mergeRunnerStateRoutes(
2350
- current.routes,
2351
- nextState?.routes ?? current.routes,
2352
- ),
2353
- shared_inboxes: {
2354
- ...safeObject(current.shared_inboxes ?? current.sharedInboxes),
2355
- ...safeObject(nextState?.sharedInboxes ?? nextState?.shared_inboxes),
2356
- },
2357
- excluded_comments: mergeRunnerStateExcludedComments(
2358
- current.excluded_comments ?? current.excludedComments,
2359
- nextState?.excludedComments ?? nextState?.excluded_comments ?? current.excluded_comments ?? current.excludedComments,
2360
- ),
2361
- requests: mergeRunnerStateRequests(
2362
- current.requests,
2363
- nextState?.requests ?? current.requests,
2364
- ),
2365
- consumed_comments: mergeRunnerStateConsumedComments(
2366
- current.consumed_comments ?? current.consumedComments,
2367
- nextState?.consumedComments ?? nextState?.consumed_comments ?? current.consumed_comments ?? current.consumedComments,
2368
- ),
2369
- };
2370
- writeTextFileAtomic(filePath, `${JSON.stringify(payload, null, 2)}\n`);
2371
- return filePath;
2372
- }
2283
+ function saveBotRunnerState(nextState) {
2284
+ const filePath = botRunnerStateFilePath();
2285
+ return withBotRunnerStateFileLock(filePath, () => {
2286
+ let current = {};
2287
+ try {
2288
+ current = safeObject(tryJsonParse(fs.readFileSync(filePath, "utf8")));
2289
+ } catch {}
2290
+ const stateEntryTimestampMs = (...values) => {
2291
+ for (const value of values) {
2292
+ const ms = Date.parse(String(value || "").trim());
2293
+ if (Number.isFinite(ms)) {
2294
+ return ms;
2295
+ }
2296
+ }
2297
+ return 0;
2298
+ };
2299
+ const mergeRunnerStateRoutes = (currentRoutesRaw, nextRoutesRaw) => {
2300
+ const currentRoutes = safeObject(currentRoutesRaw);
2301
+ const nextRoutes = safeObject(nextRoutesRaw);
2302
+ const merged = {
2303
+ ...currentRoutes,
2304
+ };
2305
+ const mergeConversationSessions = (currentSessionsRaw, nextSessionsRaw) => {
2306
+ const currentSessions = safeObject(currentSessionsRaw);
2307
+ const nextSessions = safeObject(nextSessionsRaw);
2308
+ const mergedSessions = {
2309
+ ...currentSessions,
2310
+ };
2311
+ for (const [conversationID, nextSessionRaw] of Object.entries(nextSessions)) {
2312
+ const currentSession = safeObject(currentSessions[conversationID]);
2313
+ const nextSession = safeObject(nextSessionRaw);
2314
+ if (!Object.keys(currentSession).length) {
2315
+ mergedSessions[conversationID] = nextSession;
2316
+ continue;
2317
+ }
2318
+ const currentMs = stateEntryTimestampMs(
2319
+ currentSession.updated_at,
2320
+ currentSession.last_activity_at,
2321
+ currentSession.closed_at,
2322
+ currentSession.started_at,
2323
+ currentSession.expires_at,
2324
+ );
2325
+ const nextMs = stateEntryTimestampMs(
2326
+ nextSession.updated_at,
2327
+ nextSession.last_activity_at,
2328
+ nextSession.closed_at,
2329
+ nextSession.started_at,
2330
+ nextSession.expires_at,
2331
+ );
2332
+ mergedSessions[conversationID] = nextMs >= currentMs
2333
+ ? {
2334
+ ...currentSession,
2335
+ ...nextSession,
2336
+ }
2337
+ : {
2338
+ ...nextSession,
2339
+ ...currentSession,
2340
+ };
2341
+ }
2342
+ return mergedSessions;
2343
+ };
2344
+ for (const [routeKey, nextRouteRaw] of Object.entries(nextRoutes)) {
2345
+ const currentRoute = safeObject(currentRoutes[routeKey]);
2346
+ const nextRoute = safeObject(nextRouteRaw);
2347
+ if (!Object.keys(currentRoute).length) {
2348
+ merged[routeKey] = nextRoute;
2349
+ continue;
2350
+ }
2351
+ const preferredRoute = prefersRunnerStateRecord(nextRoute, currentRoute) ? nextRoute : currentRoute;
2352
+ const fallbackRoute = preferredRoute === nextRoute ? currentRoute : nextRoute;
2353
+ merged[routeKey] = cleanupRunnerStateRecord({
2354
+ ...mergeRunnerStateRecords(preferredRoute, fallbackRoute),
2355
+ conversation_sessions: mergeConversationSessions(
2356
+ currentRoute.conversation_sessions,
2357
+ nextRoute.conversation_sessions,
2358
+ ),
2359
+ });
2360
+ }
2361
+ return merged;
2362
+ };
2363
+ const mergeRunnerStateRequests = (currentRequestsRaw, nextRequestsRaw) => {
2364
+ const currentRequests = normalizeBotRunnerRequests(currentRequestsRaw);
2365
+ const nextRequests = normalizeBotRunnerRequests(nextRequestsRaw);
2366
+ const merged = {
2367
+ ...currentRequests,
2368
+ };
2369
+ for (const [requestKey, nextRequestRaw] of Object.entries(nextRequests)) {
2370
+ const currentRequest = safeObject(currentRequests[requestKey]);
2371
+ const nextRequest = safeObject(nextRequestRaw);
2372
+ const currentMs = stateEntryTimestampMs(
2373
+ currentRequest.updated_at,
2374
+ currentRequest.completed_at,
2375
+ currentRequest.closed_at,
2376
+ currentRequest.claimed_at,
2377
+ );
2378
+ const nextMs = stateEntryTimestampMs(
2379
+ nextRequest.updated_at,
2380
+ nextRequest.completed_at,
2381
+ nextRequest.closed_at,
2382
+ nextRequest.claimed_at,
2383
+ );
2384
+ if (!Object.keys(currentRequest).length) {
2385
+ merged[requestKey] = nextRequest;
2386
+ continue;
2387
+ }
2388
+ merged[requestKey] = nextMs >= currentMs
2389
+ ? {
2390
+ ...currentRequest,
2391
+ ...nextRequest,
2392
+ }
2393
+ : currentRequest;
2394
+ }
2395
+ return normalizeBotRunnerRequests(merged);
2396
+ };
2397
+ const mergeRunnerStateExcludedComments = (currentExcludedRaw, nextExcludedRaw) => {
2398
+ const currentExcluded = normalizeBotRunnerExcludedComments(currentExcludedRaw);
2399
+ const nextExcluded = normalizeBotRunnerExcludedComments(nextExcludedRaw);
2400
+ const merged = {
2401
+ ...currentExcluded,
2402
+ };
2403
+ for (const [commentID, nextEntryRaw] of Object.entries(nextExcluded)) {
2404
+ const currentEntry = safeObject(currentExcluded[commentID]);
2405
+ const nextEntry = safeObject(nextEntryRaw);
2406
+ const currentMs = stateEntryTimestampMs(currentEntry.excluded_at);
2407
+ const nextMs = stateEntryTimestampMs(nextEntry.excluded_at);
2408
+ if (!Object.keys(currentEntry).length || nextMs >= currentMs) {
2409
+ merged[commentID] = {
2410
+ ...currentEntry,
2411
+ ...nextEntry,
2412
+ };
2413
+ }
2414
+ }
2415
+ return normalizeBotRunnerExcludedComments(merged);
2416
+ };
2417
+ const mergeRunnerStateConsumedComments = (currentConsumedRaw, nextConsumedRaw) => {
2418
+ const currentConsumed = normalizeBotRunnerConsumedComments(currentConsumedRaw);
2419
+ const nextConsumed = normalizeBotRunnerConsumedComments(nextConsumedRaw);
2420
+ const merged = {
2421
+ ...currentConsumed,
2422
+ };
2423
+ for (const [commentID, nextEntryRaw] of Object.entries(nextConsumed)) {
2424
+ const currentEntry = safeObject(currentConsumed[commentID]);
2425
+ const nextEntry = safeObject(nextEntryRaw);
2426
+ const currentMs = stateEntryTimestampMs(currentEntry.consumed_at);
2427
+ const nextMs = stateEntryTimestampMs(nextEntry.consumed_at);
2428
+ if (!Object.keys(currentEntry).length || nextMs >= currentMs) {
2429
+ merged[commentID] = {
2430
+ ...currentEntry,
2431
+ ...nextEntry,
2432
+ };
2433
+ }
2434
+ }
2435
+ return normalizeBotRunnerConsumedComments(merged);
2436
+ };
2437
+ const payload = {
2438
+ version: 1,
2439
+ updated_at: new Date().toISOString(),
2440
+ routes: mergeRunnerStateRoutes(
2441
+ current.routes,
2442
+ nextState?.routes ?? current.routes,
2443
+ ),
2444
+ shared_inboxes: {
2445
+ ...safeObject(current.shared_inboxes ?? current.sharedInboxes),
2446
+ ...safeObject(nextState?.sharedInboxes ?? nextState?.shared_inboxes),
2447
+ },
2448
+ excluded_comments: mergeRunnerStateExcludedComments(
2449
+ current.excluded_comments ?? current.excludedComments,
2450
+ nextState?.excludedComments ?? nextState?.excluded_comments ?? current.excluded_comments ?? current.excludedComments,
2451
+ ),
2452
+ requests: mergeRunnerStateRequests(
2453
+ current.requests,
2454
+ nextState?.requests ?? current.requests,
2455
+ ),
2456
+ consumed_comments: mergeRunnerStateConsumedComments(
2457
+ current.consumed_comments ?? current.consumedComments,
2458
+ nextState?.consumedComments ?? nextState?.consumed_comments ?? current.consumed_comments ?? current.consumedComments,
2459
+ ),
2460
+ };
2461
+ writeTextFileAtomic(filePath, `${JSON.stringify(payload, null, 2)}\n`);
2462
+ return filePath;
2463
+ });
2464
+ }
2373
2465
 
2374
2466
  function normalizeBotRunnerExcludedComments(rawExcluded, nowMs = Date.now()) {
2375
2467
  const normalized = {};
@@ -2552,6 +2644,42 @@ function normalizeRunnerReplyChainContext(rawContext) {
2552
2644
  return normalized;
2553
2645
  }
2554
2646
 
2647
+ function findRunnerRouteLocalInboundEnvelope(routeStateRaw, parsedArchiveRaw) {
2648
+ const routeState = safeObject(routeStateRaw);
2649
+ const parsedArchive = safeObject(parsedArchiveRaw);
2650
+ const chatID = String(parsedArchive.chatID || parsedArchive.chatId || "").trim();
2651
+ const messageID = intFromRawAllowZero(parsedArchive.messageID, 0);
2652
+ if (!chatID || !(messageID > 0)) {
2653
+ return {};
2654
+ }
2655
+ return findRecentTelegramMessageEnvelope(routeState.recent_local_inbound_envelopes, {
2656
+ chatID,
2657
+ messageID,
2658
+ });
2659
+ }
2660
+
2661
+ function buildRunnerSourceMessageEnvelope({
2662
+ routeState = {},
2663
+ routeKey = "",
2664
+ normalizedRoute = null,
2665
+ parsedArchive = null,
2666
+ }) {
2667
+ const localEnvelope = findRunnerRouteLocalInboundEnvelope(routeState, parsedArchive);
2668
+ if (String(localEnvelope.source_origin || "").trim().toLowerCase() === "local_telegram_inbound") {
2669
+ return localEnvelope;
2670
+ }
2671
+ const fallbackBotSelector = normalizeTelegramMentionUsername(
2672
+ normalizedRoute?.botName
2673
+ || normalizedRoute?.serverBotName
2674
+ || "",
2675
+ );
2676
+ return buildRunnerTelegramMessageEnvelopeFromParsedArchive(parsedArchive, {
2677
+ source_origin: "archive_reconstructed",
2678
+ source_route_key: String(routeKey || "").trim(),
2679
+ source_bot_username: fallbackBotSelector,
2680
+ });
2681
+ }
2682
+
2555
2683
  function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2556
2684
  const normalized = {};
2557
2685
  for (const [requestKeyRaw, entryRaw] of Object.entries(safeObject(rawRequests))) {
@@ -2586,6 +2714,7 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2586
2714
  body: entry.source_message_body || entry.sourceMessageBody,
2587
2715
  sender: "human",
2588
2716
  sender_is_bot: false,
2717
+ source_origin: "archive_reconstructed",
2589
2718
  }
2590
2719
  : {}),
2591
2720
  ),
@@ -2728,30 +2857,12 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2728
2857
  last_reply_message_envelope: normalizeRunnerTelegramMessageEnvelope(
2729
2858
  entry.last_reply_message_envelope
2730
2859
  || entry.lastReplyMessageEnvelope
2731
- || (intFromRawAllowZero(entry.last_reply_message_id || entry.lastReplyMessageID, 0) > 0
2732
- ? buildTelegramBotReplyEnvelope({
2733
- chatID: entry.chat_id || entry.chatID,
2734
- messageID: entry.last_reply_message_id || entry.lastReplyMessageID,
2735
- messageThreadID: entry.last_reply_message_thread_id || entry.lastReplyMessageThreadID,
2736
- replyToMessageID: entry.last_reply_to_message_id || entry.lastReplyToMessageID,
2737
- senderUsername: entry.conversation_summary_bot || ensureArray(entry.selected_bot_usernames || entry.selectedBotUsernames)[0] || "",
2738
- body: entry.followup_ai_reply_preview || entry.followupAiReplyPreview || entry.ai_reply_preview || entry.aiReplyPreview,
2739
- })
2740
- : {}),
2860
+ || {},
2741
2861
  ),
2742
2862
  attempted_delivery_envelope: normalizeRunnerTelegramMessageEnvelope(
2743
2863
  entry.attempted_delivery_envelope
2744
2864
  || entry.attemptedDeliveryEnvelope
2745
- || ((intFromRawAllowZero(entry.last_reply_to_message_id || entry.lastReplyToMessageID, 0) > 0
2746
- || intFromRawAllowZero(entry.last_reply_message_thread_id || entry.lastReplyMessageThreadID, 0) > 0)
2747
- ? buildTelegramBotReplyEnvelope({
2748
- chatID: entry.chat_id || entry.chatID,
2749
- messageThreadID: entry.last_reply_message_thread_id || entry.lastReplyMessageThreadID,
2750
- replyToMessageID: entry.last_reply_to_message_id || entry.lastReplyToMessageID,
2751
- senderUsername: entry.conversation_summary_bot || ensureArray(entry.selected_bot_usernames || entry.selectedBotUsernames)[0] || "",
2752
- body: entry.followup_ai_reply_preview || entry.followupAiReplyPreview || entry.ai_reply_preview || entry.aiReplyPreview,
2753
- })
2754
- : {}),
2865
+ || {},
2755
2866
  ),
2756
2867
  updated_at: updatedAt || new Date(nowMs).toISOString(),
2757
2868
  };
@@ -3836,20 +3947,10 @@ function buildRunnerReplyChainSnapshotFromRequestReply(requestRaw) {
3836
3947
  const explicitEnvelope = normalizeRunnerTelegramMessageEnvelope(
3837
3948
  request.last_reply_message_envelope || request.lastReplyMessageEnvelope,
3838
3949
  );
3839
- const fallbackEnvelope = intFromRawAllowZero(request.last_reply_message_id, 0) > 0
3840
- ? buildTelegramBotReplyEnvelope({
3841
- chatID: request.chat_id,
3842
- messageID: request.last_reply_message_id,
3843
- messageThreadID: request.last_reply_message_thread_id,
3844
- replyToMessageID: request.last_reply_to_message_id || request.last_source_message_id || request.source_message_id,
3845
- senderUsername: request.conversation_summary_bot || ensureArray(request.selected_bot_usernames)[0] || "",
3846
- body: preview,
3847
- })
3848
- : null;
3849
3950
  return buildRunnerReplyChainSnapshotFromMessageEnvelope(
3850
3951
  explicitEnvelope && intFromRawAllowZero(explicitEnvelope.message_id, 0) > 0
3851
3952
  ? explicitEnvelope
3852
- : fallbackEnvelope,
3953
+ : null,
3853
3954
  {
3854
3955
  speaker_type: "bot",
3855
3956
  speaker_label: firstNonEmptyString([
@@ -4346,9 +4447,10 @@ async function claimRunnerRequestForHumanComment({
4346
4447
  reason: "non_human_comment_cannot_create_request",
4347
4448
  };
4348
4449
  }
4349
- const currentState = loadBotRunnerState();
4350
- const replyChainResolution = await resolveRunnerReplyChainConversationContextWithServerFallback({
4351
- state: currentState,
4450
+ const currentState = loadBotRunnerState();
4451
+ const currentRouteState = safeObject(safeObject(currentState.routes)[String(routeKey || "").trim()]);
4452
+ const replyChainResolution = await resolveRunnerReplyChainConversationContextWithServerFallback({
4453
+ state: currentState,
4352
4454
  normalizedRoute,
4353
4455
  selectedRecord,
4354
4456
  runtime,
@@ -4468,14 +4570,19 @@ async function claimRunnerRequestForHumanComment({
4468
4570
  };
4469
4571
  }
4470
4572
  const nowISO = new Date().toISOString();
4471
- const { requests: nextRequests, request } = upsertRunnerRequest(stateForClaim, requestKey, {
4573
+ const { requests: nextRequests, request } = upsertRunnerRequest(stateForClaim, requestKey, {
4472
4574
  project_id: String(normalizedRoute?.projectID || "").trim(),
4473
4575
  provider: String(normalizedRoute?.provider || "").trim(),
4474
4576
  chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
4475
4577
  source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
4476
4578
  source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
4477
4579
  source_message_body: String(parsed.body || "").trim(),
4478
- source_message_envelope: buildRunnerTelegramMessageEnvelopeFromParsedArchive(parsed),
4580
+ source_message_envelope: buildRunnerSourceMessageEnvelope({
4581
+ routeState: currentRouteState,
4582
+ routeKey,
4583
+ normalizedRoute,
4584
+ parsedArchive: parsed,
4585
+ }),
4479
4586
  root_comment_id: String(selectedRecord?.id || "").trim(),
4480
4587
  root_comment_kind: commentKind,
4481
4588
  conversation_id: resolvedConversationID,
@@ -5217,15 +5324,17 @@ function buildRunnerRequestRecoveryPatchFromRouteState(currentStateRaw, requestR
5217
5324
  setFollowupStringPatch("followup_archive_status", ["last_followup_archive_status"]);
5218
5325
  setFollowupStringPatch("followup_transport_error", ["last_followup_transport_error"]);
5219
5326
  setFollowupStringPatch("followup_archive_error", ["last_followup_archive_error"]);
5327
+ const routeFollowupDeliveryStatus = String(routeState.last_followup_delivery_status || "").trim().toLowerCase();
5328
+ const routeFollowupDelivered = ["delivered", "dry_run"].includes(routeFollowupDeliveryStatus);
5220
5329
  if (!Object.keys(safeObject(request.source_message_envelope)).length) {
5221
5330
  const recoveredSourceEnvelope = normalizeRunnerTelegramMessageEnvelope(routeState.last_followup_source_message_envelope);
5222
5331
  if (Object.keys(recoveredSourceEnvelope).length) {
5223
5332
  patch.source_message_envelope = recoveredSourceEnvelope;
5224
5333
  }
5225
5334
  }
5226
- if (!Object.keys(safeObject(request.last_reply_message_envelope)).length) {
5335
+ if (!Object.keys(safeObject(request.last_reply_message_envelope)).length && routeFollowupDelivered) {
5227
5336
  const recoveredReplyEnvelope = normalizeRunnerTelegramMessageEnvelope(routeState.last_followup_last_reply_message_envelope);
5228
- if (Object.keys(recoveredReplyEnvelope).length) {
5337
+ if (intFromRawAllowZero(recoveredReplyEnvelope.message_id, 0) > 0) {
5229
5338
  patch.last_reply_message_envelope = recoveredReplyEnvelope;
5230
5339
  }
5231
5340
  }
@@ -5592,6 +5701,9 @@ function markRunnerRequestLifecycle({
5592
5701
  senderUsername: normalizedCurrentBotSelector,
5593
5702
  body: aiReplyPreview,
5594
5703
  });
5704
+ const normalizedDeliveryStatus = String(deliveryStatus || "").trim().toLowerCase();
5705
+ const persistSuccessfulReplyEnvelope = ["delivered", "dry_run"].includes(normalizedDeliveryStatus)
5706
+ && intFromRawAllowZero(lastReplyMessageEnvelope.message_id, 0) > 0;
5595
5707
  const attemptedDeliveryEnvelope = buildTelegramBotReplyEnvelope({
5596
5708
  sourceEnvelope: sourceMessageEnvelope,
5597
5709
  chatID: existing.chat_id,
@@ -5934,11 +6046,11 @@ function markRunnerRequestLifecycle({
5934
6046
  ? transportError || existing.followup_transport_error || ""
5935
6047
  : existing.followup_transport_error || "",
5936
6048
  ).trim(),
5937
- followup_archive_error: String(
5938
- isFollowupComment
5939
- ? archiveError || existing.followup_archive_error || ""
5940
- : existing.followup_archive_error || "",
5941
- ).trim(),
6049
+ followup_archive_error: String(
6050
+ isFollowupComment
6051
+ ? archiveError || existing.followup_archive_error || ""
6052
+ : existing.followup_archive_error || "",
6053
+ ).trim(),
5942
6054
  normalized_intent: nextNormalizedIntent,
5943
6055
  status: nextStatus,
5944
6056
  started_at: firstNonEmptyString([existing.started_at, nowISO]),
@@ -5964,7 +6076,7 @@ function markRunnerRequestLifecycle({
5964
6076
  last_reply_message_id: intFromRawAllowZero(lastReplyMessageID, 0) || existing.last_reply_message_id,
5965
6077
  last_reply_message_thread_id: intFromRawAllowZero(lastReplyMessageThreadID, 0) || existing.last_reply_message_thread_id,
5966
6078
  last_reply_to_message_id: intFromRawAllowZero(replyToMessageID, 0) || existing.last_reply_to_message_id,
5967
- last_reply_message_envelope: intFromRawAllowZero(lastReplyMessageEnvelope.message_id, 0) > 0
6079
+ last_reply_message_envelope: persistSuccessfulReplyEnvelope
5968
6080
  ? lastReplyMessageEnvelope
5969
6081
  : safeObject(existing.last_reply_message_envelope),
5970
6082
  attempted_delivery_envelope: shouldRefreshAttemptedDeliveryEnvelope
@@ -7485,11 +7597,11 @@ function parseArchivedChatComment(rawBody) {
7485
7597
  .filter(Boolean),
7486
7598
  }
7487
7599
  : null;
7488
- return {
7489
- kind,
7490
- header,
7491
- metadata,
7492
- body,
7600
+ return {
7601
+ kind,
7602
+ header,
7603
+ metadata,
7604
+ body,
7493
7605
  chatID: String(metadata.chat_id || "").trim(),
7494
7606
  chatType: String(metadata.chat_type || "").trim().toLowerCase(),
7495
7607
  messageID: intFromRawAllowZero(metadata.message_id, 0),
@@ -7502,8 +7614,11 @@ function parseArchivedChatComment(rawBody) {
7502
7614
  .split(",")
7503
7615
  .map((value) => normalizeTelegramMentionUsername(value))
7504
7616
  .filter(Boolean),
7505
- occurredAt: String(metadata.occurred_at || "").trim(),
7506
- replyToMessageID: intFromRawAllowZero(metadata.reply_to_message_id, 0),
7617
+ occurredAt: String(metadata.occurred_at || "").trim(),
7618
+ sourceOrigin: String(metadata.source_origin || "").trim().toLowerCase(),
7619
+ sourceRouteKey: String(metadata.source_route_key || "").trim(),
7620
+ sourceBotUsername: normalizeTelegramMentionUsername(metadata.source_bot_username || ""),
7621
+ replyToMessageID: intFromRawAllowZero(metadata.reply_to_message_id, 0),
7507
7622
  replyToSender: String(metadata.reply_to_sender || "").trim(),
7508
7623
  replyToUsername: String(metadata.reply_to_telegram_username || "").trim(),
7509
7624
  replyToSenderIsBot: boolFromRaw(metadata.reply_to_sender_is_bot, false),
@@ -7819,18 +7934,27 @@ function buildArchivedInboundMessageKey(chatID, messageID) {
7819
7934
  return `${String(chatID || "").trim()}:${intFromRawAllowZero(messageID, 0)}`;
7820
7935
  }
7821
7936
 
7822
- function formatTelegramInboundArchiveComment(normalized) {
7823
- const headerLines = [
7937
+ function formatTelegramInboundArchiveComment(normalized) {
7938
+ const headerLines = [
7824
7939
  `[Telegram ${normalized.eventName === "telegram.message.updated" ? "edited" : "message"}]`,
7825
7940
  `chat_id: ${normalized.chatID || "<missing>"}`,
7826
7941
  `chat_type: ${normalized.chatType || "unknown"}`,
7827
7942
  `message_id: ${normalized.messageID || "<missing>"}`,
7828
7943
  `occurred_at: ${normalized.occurredAt || new Date().toISOString()}`,
7829
7944
  `sender_id: ${normalized.fromID || "<missing>"}`,
7830
- `sender: ${normalized.fromName || normalized.fromUsername || normalized.fromID || "unknown"}`,
7831
- `sender_is_bot: ${normalized.fromIsBot ? "true" : "false"}`,
7832
- ];
7833
- if (normalized.fromUsername) {
7945
+ `sender: ${normalized.fromName || normalized.fromUsername || normalized.fromID || "unknown"}`,
7946
+ `sender_is_bot: ${normalized.fromIsBot ? "true" : "false"}`,
7947
+ ];
7948
+ if (String(normalized.sourceOrigin || "").trim()) {
7949
+ headerLines.push(`source_origin: ${String(normalized.sourceOrigin || "").trim()}`);
7950
+ }
7951
+ if (String(normalized.sourceRouteKey || "").trim()) {
7952
+ headerLines.push(`source_route_key: ${String(normalized.sourceRouteKey || "").trim()}`);
7953
+ }
7954
+ if (String(normalized.sourceBotUsername || "").trim()) {
7955
+ headerLines.push(`source_bot_username: @${String(normalized.sourceBotUsername || "").trim().replace(/^@+/, "")}`);
7956
+ }
7957
+ if (normalized.fromUsername) {
7834
7958
  headerLines.push(`telegram_username: @${normalized.fromUsername.replace(/^@+/, "")}`);
7835
7959
  }
7836
7960
  if (normalized.mentionUsernames.length > 0) {