metheus-governance-mcp-cli 0.2.247 → 0.2.249

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
@@ -2075,9 +2075,10 @@ function migrateBotRunnerStateRoutes(routes, runnerConfig) {
2075
2075
  };
2076
2076
  }
2077
2077
 
2078
- function loadBotRunnerState() {
2079
- const filePath = botRunnerStateFilePath();
2080
- try {
2078
+ function loadBotRunnerState() {
2079
+ const filePath = botRunnerStateFilePath();
2080
+ waitForBotRunnerStateLockRelease(filePath);
2081
+ try {
2081
2082
  if (!fs.existsSync(filePath)) {
2082
2083
  return {
2083
2084
  filePath,
@@ -2127,8 +2128,97 @@ function loadBotRunnerState() {
2127
2128
  remainingAnonymousKeys: [],
2128
2129
  };
2129
2130
  }
2130
- }
2131
-
2131
+ }
2132
+
2133
+ function sleepSyncMs(delayMs) {
2134
+ const ms = Number(delayMs) || 0;
2135
+ if (!(ms > 0)) {
2136
+ return;
2137
+ }
2138
+ const sleepBuffer = new SharedArrayBuffer(4);
2139
+ const sleepArray = new Int32Array(sleepBuffer);
2140
+ Atomics.wait(sleepArray, 0, 0, ms);
2141
+ }
2142
+
2143
+ function botRunnerStateLockFilePath(filePath) {
2144
+ const normalizedPath = String(filePath || "").trim();
2145
+ return normalizedPath ? `${normalizedPath}.lock` : "";
2146
+ }
2147
+
2148
+ function tryReleaseStaleBotRunnerStateLock(lockPath, staleMs = 60000) {
2149
+ const normalizedPath = String(lockPath || "").trim();
2150
+ if (!normalizedPath) {
2151
+ return false;
2152
+ }
2153
+ try {
2154
+ const stats = fs.statSync(normalizedPath);
2155
+ const ageMs = Date.now() - Number(stats.mtimeMs || 0);
2156
+ if (Number.isFinite(ageMs) && ageMs >= staleMs) {
2157
+ fs.rmSync(normalizedPath, { force: true });
2158
+ return true;
2159
+ }
2160
+ } catch {}
2161
+ return false;
2162
+ }
2163
+
2164
+ function waitForBotRunnerStateLockRelease(filePath, timeoutMs = 5000, staleMs = 60000) {
2165
+ const lockPath = botRunnerStateLockFilePath(filePath);
2166
+ if (!lockPath) {
2167
+ return;
2168
+ }
2169
+ const deadlineMs = Date.now() + Math.max(0, Number(timeoutMs) || 0);
2170
+ while (fs.existsSync(lockPath)) {
2171
+ tryReleaseStaleBotRunnerStateLock(lockPath, staleMs);
2172
+ if (!fs.existsSync(lockPath)) {
2173
+ return;
2174
+ }
2175
+ if (Date.now() >= deadlineMs) {
2176
+ return;
2177
+ }
2178
+ sleepSyncMs(25);
2179
+ }
2180
+ }
2181
+
2182
+ function withBotRunnerStateFileLock(filePath, callback, timeoutMs = 5000, staleMs = 60000) {
2183
+ const lockPath = botRunnerStateLockFilePath(filePath);
2184
+ if (!lockPath) {
2185
+ return callback();
2186
+ }
2187
+ const deadlineMs = Date.now() + Math.max(0, Number(timeoutMs) || 0);
2188
+ let lockFD = null;
2189
+ while (lockFD === null) {
2190
+ try {
2191
+ lockFD = fs.openSync(lockPath, "wx");
2192
+ fs.writeFileSync(lockFD, `${process.pid} ${new Date().toISOString()}\n`, "utf8");
2193
+ break;
2194
+ } catch (error) {
2195
+ const errorCode = String(error?.code || "").trim().toUpperCase();
2196
+ if (!["EEXIST", "EPERM", "EBUSY"].includes(errorCode)) {
2197
+ throw error;
2198
+ }
2199
+ tryReleaseStaleBotRunnerStateLock(lockPath, staleMs);
2200
+ if (Date.now() >= deadlineMs) {
2201
+ throw error;
2202
+ }
2203
+ sleepSyncMs(25);
2204
+ }
2205
+ }
2206
+ try {
2207
+ return callback();
2208
+ } finally {
2209
+ try {
2210
+ if (lockFD !== null) {
2211
+ fs.closeSync(lockFD);
2212
+ }
2213
+ } catch {}
2214
+ try {
2215
+ if (fs.existsSync(lockPath)) {
2216
+ fs.rmSync(lockPath, { force: true });
2217
+ }
2218
+ } catch {}
2219
+ }
2220
+ }
2221
+
2132
2222
  function writeTextFileAtomic(filePath, text) {
2133
2223
  const normalizedPath = String(filePath || "").trim();
2134
2224
  if (!normalizedPath) {
@@ -2141,14 +2231,12 @@ function writeTextFileAtomic(filePath, text) {
2141
2231
  `.${path.basename(normalizedPath)}.${process.pid}.${Date.now()}.tmp`,
2142
2232
  );
2143
2233
  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;
2234
+ const renameRetryDelaysMs = [0, 20, 50, 100, 200];
2235
+ let lastRenameError = null;
2148
2236
  try {
2149
2237
  for (const delayMs of renameRetryDelaysMs) {
2150
2238
  if (delayMs > 0) {
2151
- Atomics.wait(sleepArray, 0, 0, delayMs);
2239
+ sleepSyncMs(delayMs);
2152
2240
  }
2153
2241
  try {
2154
2242
  fs.renameSync(tempPath, normalizedPath);
@@ -2190,186 +2278,188 @@ function writeTextFileAtomic(filePath, text) {
2190
2278
  }
2191
2279
  }
2192
2280
 
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
- }
2281
+ function saveBotRunnerState(nextState) {
2282
+ const filePath = botRunnerStateFilePath();
2283
+ return withBotRunnerStateFileLock(filePath, () => {
2284
+ let current = {};
2285
+ try {
2286
+ current = safeObject(tryJsonParse(fs.readFileSync(filePath, "utf8")));
2287
+ } catch {}
2288
+ const stateEntryTimestampMs = (...values) => {
2289
+ for (const value of values) {
2290
+ const ms = Date.parse(String(value || "").trim());
2291
+ if (Number.isFinite(ms)) {
2292
+ return ms;
2293
+ }
2294
+ }
2295
+ return 0;
2296
+ };
2297
+ const mergeRunnerStateRoutes = (currentRoutesRaw, nextRoutesRaw) => {
2298
+ const currentRoutes = safeObject(currentRoutesRaw);
2299
+ const nextRoutes = safeObject(nextRoutesRaw);
2300
+ const merged = {
2301
+ ...currentRoutes,
2302
+ };
2303
+ const mergeConversationSessions = (currentSessionsRaw, nextSessionsRaw) => {
2304
+ const currentSessions = safeObject(currentSessionsRaw);
2305
+ const nextSessions = safeObject(nextSessionsRaw);
2306
+ const mergedSessions = {
2307
+ ...currentSessions,
2308
+ };
2309
+ for (const [conversationID, nextSessionRaw] of Object.entries(nextSessions)) {
2310
+ const currentSession = safeObject(currentSessions[conversationID]);
2311
+ const nextSession = safeObject(nextSessionRaw);
2312
+ if (!Object.keys(currentSession).length) {
2313
+ mergedSessions[conversationID] = nextSession;
2314
+ continue;
2315
+ }
2316
+ const currentMs = stateEntryTimestampMs(
2317
+ currentSession.updated_at,
2318
+ currentSession.last_activity_at,
2319
+ currentSession.closed_at,
2320
+ currentSession.started_at,
2321
+ currentSession.expires_at,
2322
+ );
2323
+ const nextMs = stateEntryTimestampMs(
2324
+ nextSession.updated_at,
2325
+ nextSession.last_activity_at,
2326
+ nextSession.closed_at,
2327
+ nextSession.started_at,
2328
+ nextSession.expires_at,
2329
+ );
2330
+ mergedSessions[conversationID] = nextMs >= currentMs
2331
+ ? {
2332
+ ...currentSession,
2333
+ ...nextSession,
2334
+ }
2335
+ : {
2336
+ ...nextSession,
2337
+ ...currentSession,
2338
+ };
2339
+ }
2340
+ return mergedSessions;
2341
+ };
2342
+ for (const [routeKey, nextRouteRaw] of Object.entries(nextRoutes)) {
2343
+ const currentRoute = safeObject(currentRoutes[routeKey]);
2344
+ const nextRoute = safeObject(nextRouteRaw);
2345
+ if (!Object.keys(currentRoute).length) {
2346
+ merged[routeKey] = nextRoute;
2347
+ continue;
2348
+ }
2349
+ const preferredRoute = prefersRunnerStateRecord(nextRoute, currentRoute) ? nextRoute : currentRoute;
2350
+ const fallbackRoute = preferredRoute === nextRoute ? currentRoute : nextRoute;
2351
+ merged[routeKey] = cleanupRunnerStateRecord({
2352
+ ...mergeRunnerStateRecords(preferredRoute, fallbackRoute),
2353
+ conversation_sessions: mergeConversationSessions(
2354
+ currentRoute.conversation_sessions,
2355
+ nextRoute.conversation_sessions,
2356
+ ),
2357
+ });
2358
+ }
2359
+ return merged;
2360
+ };
2361
+ const mergeRunnerStateRequests = (currentRequestsRaw, nextRequestsRaw) => {
2362
+ const currentRequests = normalizeBotRunnerRequests(currentRequestsRaw);
2363
+ const nextRequests = normalizeBotRunnerRequests(nextRequestsRaw);
2364
+ const merged = {
2365
+ ...currentRequests,
2366
+ };
2367
+ for (const [requestKey, nextRequestRaw] of Object.entries(nextRequests)) {
2368
+ const currentRequest = safeObject(currentRequests[requestKey]);
2369
+ const nextRequest = safeObject(nextRequestRaw);
2370
+ const currentMs = stateEntryTimestampMs(
2371
+ currentRequest.updated_at,
2372
+ currentRequest.completed_at,
2373
+ currentRequest.closed_at,
2374
+ currentRequest.claimed_at,
2375
+ );
2376
+ const nextMs = stateEntryTimestampMs(
2377
+ nextRequest.updated_at,
2378
+ nextRequest.completed_at,
2379
+ nextRequest.closed_at,
2380
+ nextRequest.claimed_at,
2381
+ );
2382
+ if (!Object.keys(currentRequest).length) {
2383
+ merged[requestKey] = nextRequest;
2384
+ continue;
2385
+ }
2386
+ merged[requestKey] = nextMs >= currentMs
2387
+ ? {
2388
+ ...currentRequest,
2389
+ ...nextRequest,
2390
+ }
2391
+ : currentRequest;
2392
+ }
2393
+ return normalizeBotRunnerRequests(merged);
2394
+ };
2395
+ const mergeRunnerStateExcludedComments = (currentExcludedRaw, nextExcludedRaw) => {
2396
+ const currentExcluded = normalizeBotRunnerExcludedComments(currentExcludedRaw);
2397
+ const nextExcluded = normalizeBotRunnerExcludedComments(nextExcludedRaw);
2398
+ const merged = {
2399
+ ...currentExcluded,
2400
+ };
2401
+ for (const [commentID, nextEntryRaw] of Object.entries(nextExcluded)) {
2402
+ const currentEntry = safeObject(currentExcluded[commentID]);
2403
+ const nextEntry = safeObject(nextEntryRaw);
2404
+ const currentMs = stateEntryTimestampMs(currentEntry.excluded_at);
2405
+ const nextMs = stateEntryTimestampMs(nextEntry.excluded_at);
2406
+ if (!Object.keys(currentEntry).length || nextMs >= currentMs) {
2407
+ merged[commentID] = {
2408
+ ...currentEntry,
2409
+ ...nextEntry,
2410
+ };
2411
+ }
2412
+ }
2413
+ return normalizeBotRunnerExcludedComments(merged);
2414
+ };
2415
+ const mergeRunnerStateConsumedComments = (currentConsumedRaw, nextConsumedRaw) => {
2416
+ const currentConsumed = normalizeBotRunnerConsumedComments(currentConsumedRaw);
2417
+ const nextConsumed = normalizeBotRunnerConsumedComments(nextConsumedRaw);
2418
+ const merged = {
2419
+ ...currentConsumed,
2420
+ };
2421
+ for (const [commentID, nextEntryRaw] of Object.entries(nextConsumed)) {
2422
+ const currentEntry = safeObject(currentConsumed[commentID]);
2423
+ const nextEntry = safeObject(nextEntryRaw);
2424
+ const currentMs = stateEntryTimestampMs(currentEntry.consumed_at);
2425
+ const nextMs = stateEntryTimestampMs(nextEntry.consumed_at);
2426
+ if (!Object.keys(currentEntry).length || nextMs >= currentMs) {
2427
+ merged[commentID] = {
2428
+ ...currentEntry,
2429
+ ...nextEntry,
2430
+ };
2431
+ }
2432
+ }
2433
+ return normalizeBotRunnerConsumedComments(merged);
2434
+ };
2435
+ const payload = {
2436
+ version: 1,
2437
+ updated_at: new Date().toISOString(),
2438
+ routes: mergeRunnerStateRoutes(
2439
+ current.routes,
2440
+ nextState?.routes ?? current.routes,
2441
+ ),
2442
+ shared_inboxes: {
2443
+ ...safeObject(current.shared_inboxes ?? current.sharedInboxes),
2444
+ ...safeObject(nextState?.sharedInboxes ?? nextState?.shared_inboxes),
2445
+ },
2446
+ excluded_comments: mergeRunnerStateExcludedComments(
2447
+ current.excluded_comments ?? current.excludedComments,
2448
+ nextState?.excludedComments ?? nextState?.excluded_comments ?? current.excluded_comments ?? current.excludedComments,
2449
+ ),
2450
+ requests: mergeRunnerStateRequests(
2451
+ current.requests,
2452
+ nextState?.requests ?? current.requests,
2453
+ ),
2454
+ consumed_comments: mergeRunnerStateConsumedComments(
2455
+ current.consumed_comments ?? current.consumedComments,
2456
+ nextState?.consumedComments ?? nextState?.consumed_comments ?? current.consumed_comments ?? current.consumedComments,
2457
+ ),
2458
+ };
2459
+ writeTextFileAtomic(filePath, `${JSON.stringify(payload, null, 2)}\n`);
2460
+ return filePath;
2461
+ });
2462
+ }
2373
2463
 
2374
2464
  function normalizeBotRunnerExcludedComments(rawExcluded, nowMs = Date.now()) {
2375
2465
  const normalized = {};
@@ -2728,30 +2818,12 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2728
2818
  last_reply_message_envelope: normalizeRunnerTelegramMessageEnvelope(
2729
2819
  entry.last_reply_message_envelope
2730
2820
  || 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
- : {}),
2821
+ || {},
2741
2822
  ),
2742
2823
  attempted_delivery_envelope: normalizeRunnerTelegramMessageEnvelope(
2743
2824
  entry.attempted_delivery_envelope
2744
2825
  || 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
- : {}),
2826
+ || {},
2755
2827
  ),
2756
2828
  updated_at: updatedAt || new Date(nowMs).toISOString(),
2757
2829
  };
@@ -3836,20 +3908,10 @@ function buildRunnerReplyChainSnapshotFromRequestReply(requestRaw) {
3836
3908
  const explicitEnvelope = normalizeRunnerTelegramMessageEnvelope(
3837
3909
  request.last_reply_message_envelope || request.lastReplyMessageEnvelope,
3838
3910
  );
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
3911
  return buildRunnerReplyChainSnapshotFromMessageEnvelope(
3850
3912
  explicitEnvelope && intFromRawAllowZero(explicitEnvelope.message_id, 0) > 0
3851
3913
  ? explicitEnvelope
3852
- : fallbackEnvelope,
3914
+ : null,
3853
3915
  {
3854
3916
  speaker_type: "bot",
3855
3917
  speaker_label: firstNonEmptyString([
@@ -5217,15 +5279,17 @@ function buildRunnerRequestRecoveryPatchFromRouteState(currentStateRaw, requestR
5217
5279
  setFollowupStringPatch("followup_archive_status", ["last_followup_archive_status"]);
5218
5280
  setFollowupStringPatch("followup_transport_error", ["last_followup_transport_error"]);
5219
5281
  setFollowupStringPatch("followup_archive_error", ["last_followup_archive_error"]);
5282
+ const routeFollowupDeliveryStatus = String(routeState.last_followup_delivery_status || "").trim().toLowerCase();
5283
+ const routeFollowupDelivered = ["delivered", "dry_run"].includes(routeFollowupDeliveryStatus);
5220
5284
  if (!Object.keys(safeObject(request.source_message_envelope)).length) {
5221
5285
  const recoveredSourceEnvelope = normalizeRunnerTelegramMessageEnvelope(routeState.last_followup_source_message_envelope);
5222
5286
  if (Object.keys(recoveredSourceEnvelope).length) {
5223
5287
  patch.source_message_envelope = recoveredSourceEnvelope;
5224
5288
  }
5225
5289
  }
5226
- if (!Object.keys(safeObject(request.last_reply_message_envelope)).length) {
5290
+ if (!Object.keys(safeObject(request.last_reply_message_envelope)).length && routeFollowupDelivered) {
5227
5291
  const recoveredReplyEnvelope = normalizeRunnerTelegramMessageEnvelope(routeState.last_followup_last_reply_message_envelope);
5228
- if (Object.keys(recoveredReplyEnvelope).length) {
5292
+ if (intFromRawAllowZero(recoveredReplyEnvelope.message_id, 0) > 0) {
5229
5293
  patch.last_reply_message_envelope = recoveredReplyEnvelope;
5230
5294
  }
5231
5295
  }
@@ -5592,6 +5656,9 @@ function markRunnerRequestLifecycle({
5592
5656
  senderUsername: normalizedCurrentBotSelector,
5593
5657
  body: aiReplyPreview,
5594
5658
  });
5659
+ const normalizedDeliveryStatus = String(deliveryStatus || "").trim().toLowerCase();
5660
+ const persistSuccessfulReplyEnvelope = ["delivered", "dry_run"].includes(normalizedDeliveryStatus)
5661
+ && intFromRawAllowZero(lastReplyMessageEnvelope.message_id, 0) > 0;
5595
5662
  const attemptedDeliveryEnvelope = buildTelegramBotReplyEnvelope({
5596
5663
  sourceEnvelope: sourceMessageEnvelope,
5597
5664
  chatID: existing.chat_id,
@@ -5934,11 +6001,11 @@ function markRunnerRequestLifecycle({
5934
6001
  ? transportError || existing.followup_transport_error || ""
5935
6002
  : existing.followup_transport_error || "",
5936
6003
  ).trim(),
5937
- followup_archive_error: String(
5938
- isFollowupComment
5939
- ? archiveError || existing.followup_archive_error || ""
5940
- : existing.followup_archive_error || "",
5941
- ).trim(),
6004
+ followup_archive_error: String(
6005
+ isFollowupComment
6006
+ ? archiveError || existing.followup_archive_error || ""
6007
+ : existing.followup_archive_error || "",
6008
+ ).trim(),
5942
6009
  normalized_intent: nextNormalizedIntent,
5943
6010
  status: nextStatus,
5944
6011
  started_at: firstNonEmptyString([existing.started_at, nowISO]),
@@ -5964,7 +6031,7 @@ function markRunnerRequestLifecycle({
5964
6031
  last_reply_message_id: intFromRawAllowZero(lastReplyMessageID, 0) || existing.last_reply_message_id,
5965
6032
  last_reply_message_thread_id: intFromRawAllowZero(lastReplyMessageThreadID, 0) || existing.last_reply_message_thread_id,
5966
6033
  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
6034
+ last_reply_message_envelope: persistSuccessfulReplyEnvelope
5968
6035
  ? lastReplyMessageEnvelope
5969
6036
  : safeObject(existing.last_reply_message_envelope),
5970
6037
  attempted_delivery_envelope: shouldRefreshAttemptedDeliveryEnvelope
@@ -4724,14 +4724,6 @@ export async function processRunnerSelectedRecord({
4724
4724
  const replyMessageThreadID = intFromRawAllowZero(sourceMessageEnvelope.message_thread_id, 0);
4725
4725
  const replyToMessageID = intFromRawAllowZero(sourceMessageEnvelope.message_id, 0);
4726
4726
  const replyAnchorSource = replyToMessageID > 0 ? "source_message_envelope" : "";
4727
- const attemptedDeliveryEnvelope = buildTelegramBotReplyEnvelope({
4728
- sourceEnvelope: sourceMessageEnvelope,
4729
- messageThreadID: replyMessageThreadID,
4730
- replyToMessageID,
4731
- sender: bot?.username ? `@${String(bot.username || "").trim().replace(/^@+/, "")}` : String(bot?.name || "bot").trim(),
4732
- senderUsername: normalizeMentionSelector(bot?.username || bot?.name),
4733
- body: sanitizedReplyText,
4734
- });
4735
4727
  const normalizedPrecomputedHumanIntentContext = safeObject(precomputedHumanIntentContext);
4736
4728
  const validateWorkspaceArtifacts = typeof executionDeps.validateWorkspaceArtifacts === "function"
4737
4729
  ? executionDeps.validateWorkspaceArtifacts
@@ -5500,6 +5492,14 @@ export async function processRunnerSelectedRecord({
5500
5492
  triggerDecision: effectiveTriggerDecision,
5501
5493
  conversationContext: effectiveConversationContext,
5502
5494
  });
5495
+ const attemptedDeliveryEnvelope = buildTelegramBotReplyEnvelope({
5496
+ sourceEnvelope: sourceMessageEnvelope,
5497
+ messageThreadID: replyMessageThreadID,
5498
+ replyToMessageID,
5499
+ sender: bot?.username ? `@${String(bot.username || "").trim().replace(/^@+/, "")}` : String(bot?.name || "bot").trim(),
5500
+ senderUsername: normalizeMentionSelector(bot?.username || bot?.name),
5501
+ body: sanitizedReplyText,
5502
+ });
5503
5503
  const normalizedExecutionTargets = ensureArray(executionContract?.assignments)
5504
5504
  .map((item) => normalizeMentionSelector(item?.targetBot))
5505
5505
  .filter(Boolean);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.247",
3
+ "version": "0.2.249",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [