metheus-governance-mcp-cli 0.2.256 → 0.2.258

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
@@ -133,21 +133,21 @@ import {
133
133
  tryRegister,
134
134
  withWorkspaceDirArg,
135
135
  } from "./lib/client-registration.mjs";
136
- import {
137
- applyPendingAgeSelection,
138
- buildTelegramBotReplyEnvelope,
139
- buildTelegramMessageEnvelopeFromParsedArchive as buildRunnerTelegramMessageEnvelopeFromParsedArchive,
140
- buildRunnerRouteStateFromComment,
141
- buildProcessableArchiveLogicalKey,
142
- findEarlierProcessableArchiveDuplicate,
143
- findRecentTelegramMessageEnvelope,
144
- isTelegramLocalInboundEnvelopeForRoute,
145
- isInboundArchiveKind,
146
- normalizeTelegramMessageEnvelope as normalizeRunnerTelegramMessageEnvelope,
147
- normalizeArchiveCommentRecord,
148
- selectPendingArchiveComments,
149
- printRunnerResult,
150
- } from "./lib/runner-helpers.mjs";
136
+ import {
137
+ applyPendingAgeSelection,
138
+ buildTelegramBotReplyEnvelope,
139
+ buildTelegramMessageEnvelopeFromParsedArchive as buildRunnerTelegramMessageEnvelopeFromParsedArchive,
140
+ buildRunnerRouteStateFromComment,
141
+ buildProcessableArchiveLogicalKey,
142
+ findEarlierProcessableArchiveDuplicate,
143
+ findRecentTelegramMessageEnvelope,
144
+ isTelegramLocalInboundEnvelopeForRoute,
145
+ isInboundArchiveKind,
146
+ normalizeTelegramMessageEnvelope as normalizeRunnerTelegramMessageEnvelope,
147
+ normalizeArchiveCommentRecord,
148
+ selectPendingArchiveComments,
149
+ printRunnerResult,
150
+ } from "./lib/runner-helpers.mjs";
151
151
  import {
152
152
  createProjectContextItem as createProjectContextItemImpl,
153
153
  createProjectEvidence as createProjectEvidenceImpl,
@@ -186,30 +186,30 @@ import {
186
186
  resolveRunnerWorkspaceSelection,
187
187
  runRunnerAIExecution,
188
188
  } from "./lib/runner-execution.mjs";
189
- import {
190
- buildRunnerResponderAdjudicationFromConversationContext,
191
- buildRunnerResponderAdjudicationFromHumanIntent,
192
- processRunnerSelectedRecord,
193
- resolveHumanIntentContext,
194
- resolvePublicConversationContext,
195
- resolveRunnerHumanInboundVisibility,
196
- resolveRunnerResponderAdjudication,
197
- resolveRunnerStartupLoopAdjudication,
198
- selectRunnerPendingWork,
199
- } from "./lib/runner-orchestration.mjs";
189
+ import {
190
+ buildRunnerResponderAdjudicationFromConversationContext,
191
+ buildRunnerResponderAdjudicationFromHumanIntent,
192
+ processRunnerSelectedRecord,
193
+ resolveHumanIntentContext,
194
+ resolvePublicConversationContext,
195
+ resolveRunnerHumanInboundVisibility,
196
+ resolveRunnerResponderAdjudication,
197
+ resolveRunnerStartupLoopAdjudication,
198
+ selectRunnerPendingWork,
199
+ } from "./lib/runner-orchestration.mjs";
200
200
  import {
201
201
  archiveLocalTelegramMessagesForRoute,
202
202
  maybeSendRunnerChatAction,
203
203
  startRunnerTypingHeartbeat,
204
204
  } from "./lib/runner-runtime.mjs";
205
- import {
206
- deleteTelegramWebhook,
207
- getTelegramBotMe,
208
- getTelegramUpdates,
209
- listTelegramChatAdministrators,
210
- getTelegramWebhookInfo,
211
- sendLocalProviderChatAction,
212
- verifyLocalProviderToken,
205
+ import {
206
+ deleteTelegramWebhook,
207
+ getTelegramBotMe,
208
+ getTelegramUpdates,
209
+ listTelegramChatAdministrators,
210
+ getTelegramWebhookInfo,
211
+ sendLocalProviderChatAction,
212
+ verifyLocalProviderToken,
213
213
  } from "./lib/provider-local-transport.mjs";
214
214
 
215
215
  const DEFAULT_SITE_URL = "https://metheus.gesiaplatform.com";
@@ -1922,24 +1922,24 @@ function mergeRunnerStateRecords(preferred, fallback) {
1922
1922
  }
1923
1923
  return allowUndefined ? undefined : 0;
1924
1924
  };
1925
- const pickArrayField = (key, normalizer = (value) => String(value || "").trim()) => {
1926
- if (hasOwn(primary, key)) {
1927
- const value = uniqueOrderedStrings(ensureArray(primary[key]), normalizer).filter(Boolean);
1928
- if (value.length) {
1929
- return value;
1930
- }
1931
- }
1932
- return uniqueOrderedStrings(ensureArray(secondary[key]), normalizer).filter(Boolean);
1933
- };
1934
- const pickObjectField = (key) => {
1935
- if (hasOwn(primary, key)) {
1936
- const value = safeObject(primary[key]);
1937
- if (Object.keys(value).length) {
1938
- return value;
1939
- }
1940
- }
1941
- return safeObject(secondary[key]);
1942
- };
1925
+ const pickArrayField = (key, normalizer = (value) => String(value || "").trim()) => {
1926
+ if (hasOwn(primary, key)) {
1927
+ const value = uniqueOrderedStrings(ensureArray(primary[key]), normalizer).filter(Boolean);
1928
+ if (value.length) {
1929
+ return value;
1930
+ }
1931
+ }
1932
+ return uniqueOrderedStrings(ensureArray(secondary[key]), normalizer).filter(Boolean);
1933
+ };
1934
+ const pickObjectField = (key) => {
1935
+ if (hasOwn(primary, key)) {
1936
+ const value = safeObject(primary[key]);
1937
+ if (Object.keys(value).length) {
1938
+ return value;
1939
+ }
1940
+ }
1941
+ return safeObject(secondary[key]);
1942
+ };
1943
1943
  return {
1944
1944
  last_processed_comment_id: pickStringField("last_processed_comment_id"),
1945
1945
  last_processed_created_at: pickStringField("last_processed_created_at"),
@@ -1947,16 +1947,16 @@ function mergeRunnerStateRecords(preferred, fallback) {
1947
1947
  last_source_kind: pickStringField("last_source_kind"),
1948
1948
  last_error: pickStringField("last_error", { allowBlank: true }),
1949
1949
  updated_at: firstNonEmptyString([primary.updated_at, secondary.updated_at, new Date().toISOString()]),
1950
- last_action: pickStringField("last_action"),
1951
- last_reason: pickStringField("last_reason"),
1952
- last_trigger: pickStringField("last_trigger"),
1953
- last_visibility_status: pickStringField("last_visibility_status"),
1954
- last_visibility_reason: pickStringField("last_visibility_reason", { allowBlank: true }),
1955
- last_visibility_source: pickStringField("last_visibility_source", { allowBlank: true }),
1956
- last_reply_message_id: pickNumberField("last_reply_message_id", { allowUndefined: true }),
1957
- last_conversation_id: pickStringField("last_conversation_id"),
1958
- last_conversation_stage: pickStringField("last_conversation_stage"),
1959
- last_speaker_bot_username: pickStringField("last_speaker_bot_username"),
1950
+ last_action: pickStringField("last_action"),
1951
+ last_reason: pickStringField("last_reason"),
1952
+ last_trigger: pickStringField("last_trigger"),
1953
+ last_visibility_status: pickStringField("last_visibility_status"),
1954
+ last_visibility_reason: pickStringField("last_visibility_reason", { allowBlank: true }),
1955
+ last_visibility_source: pickStringField("last_visibility_source", { allowBlank: true }),
1956
+ last_reply_message_id: pickNumberField("last_reply_message_id", { allowUndefined: true }),
1957
+ last_conversation_id: pickStringField("last_conversation_id"),
1958
+ last_conversation_stage: pickStringField("last_conversation_stage"),
1959
+ last_speaker_bot_username: pickStringField("last_speaker_bot_username"),
1960
1960
  local_receive_mode: pickStringField("local_receive_mode"),
1961
1961
  local_telegram_polling_ready: Boolean(primary.local_telegram_polling_ready || secondary.local_telegram_polling_ready),
1962
1962
  local_telegram_webhook_cleared_url: pickStringField("local_telegram_webhook_cleared_url", { allowBlank: true }),
@@ -1989,16 +1989,16 @@ function mergeRunnerStateRecords(preferred, fallback) {
1989
1989
  last_followup_response_contract_validation_reason: pickStringField("last_followup_response_contract_validation_reason"),
1990
1990
  last_followup_assignment_validation_status: pickStringField("last_followup_assignment_validation_status"),
1991
1991
  last_followup_assignment_validation_reason: pickStringField("last_followup_assignment_validation_reason"),
1992
- last_followup_delivery_status: pickStringField("last_followup_delivery_status"),
1993
- last_followup_archive_status: pickStringField("last_followup_archive_status"),
1994
- last_followup_transport_error: pickStringField("last_followup_transport_error"),
1995
- last_followup_archive_error: pickStringField("last_followup_archive_error"),
1996
- last_followup_source_message_envelope: pickObjectField("last_followup_source_message_envelope"),
1997
- last_followup_last_reply_message_envelope: pickObjectField("last_followup_last_reply_message_envelope"),
1998
- last_followup_attempted_delivery_envelope: pickObjectField("last_followup_attempted_delivery_envelope"),
1999
- recent_local_inbound_envelopes: pickObjectField("recent_local_inbound_envelopes"),
2000
- last_candidate_bot_usernames: pickArrayField("last_candidate_bot_usernames", normalizeTelegramMentionUsername),
2001
- last_contract_validation_targets: pickArrayField("last_contract_validation_targets", normalizeTelegramMentionUsername),
1992
+ last_followup_delivery_status: pickStringField("last_followup_delivery_status"),
1993
+ last_followup_archive_status: pickStringField("last_followup_archive_status"),
1994
+ last_followup_transport_error: pickStringField("last_followup_transport_error"),
1995
+ last_followup_archive_error: pickStringField("last_followup_archive_error"),
1996
+ last_followup_source_message_envelope: pickObjectField("last_followup_source_message_envelope"),
1997
+ last_followup_last_reply_message_envelope: pickObjectField("last_followup_last_reply_message_envelope"),
1998
+ last_followup_attempted_delivery_envelope: pickObjectField("last_followup_attempted_delivery_envelope"),
1999
+ recent_local_inbound_envelopes: pickObjectField("recent_local_inbound_envelopes"),
2000
+ last_candidate_bot_usernames: pickArrayField("last_candidate_bot_usernames", normalizeTelegramMentionUsername),
2001
+ last_contract_validation_targets: pickArrayField("last_contract_validation_targets", normalizeTelegramMentionUsername),
2002
2002
  last_normalized_execution_contract_targets: pickArrayField("last_normalized_execution_contract_targets", normalizeTelegramMentionUsername),
2003
2003
  last_normalized_execution_next_responders: pickArrayField("last_normalized_execution_next_responders", normalizeTelegramMentionUsername),
2004
2004
  last_followup_execution_contract_targets: pickArrayField("last_followup_execution_contract_targets", normalizeTelegramMentionUsername),
@@ -2084,10 +2084,10 @@ function migrateBotRunnerStateRoutes(routes, runnerConfig) {
2084
2084
  };
2085
2085
  }
2086
2086
 
2087
- function loadBotRunnerState() {
2088
- const filePath = botRunnerStateFilePath();
2089
- waitForBotRunnerStateLockRelease(filePath);
2090
- try {
2087
+ function loadBotRunnerState() {
2088
+ const filePath = botRunnerStateFilePath();
2089
+ waitForBotRunnerStateLockRelease(filePath);
2090
+ try {
2091
2091
  if (!fs.existsSync(filePath)) {
2092
2092
  return {
2093
2093
  filePath,
@@ -2137,102 +2137,102 @@ function loadBotRunnerState() {
2137
2137
  remainingAnonymousKeys: [],
2138
2138
  };
2139
2139
  }
2140
- }
2141
-
2142
- function sleepSyncMs(delayMs) {
2143
- const ms = Number(delayMs) || 0;
2144
- if (!(ms > 0)) {
2145
- return;
2146
- }
2147
- const sleepBuffer = new SharedArrayBuffer(4);
2148
- const sleepArray = new Int32Array(sleepBuffer);
2149
- Atomics.wait(sleepArray, 0, 0, ms);
2150
- }
2151
-
2152
- function botRunnerStateLockFilePath(filePath) {
2153
- const normalizedPath = String(filePath || "").trim();
2154
- return normalizedPath ? `${normalizedPath}.lock` : "";
2155
- }
2156
-
2157
- function tryReleaseStaleBotRunnerStateLock(lockPath, staleMs = 60000) {
2158
- const normalizedPath = String(lockPath || "").trim();
2159
- if (!normalizedPath) {
2160
- return false;
2161
- }
2162
- try {
2163
- const stats = fs.statSync(normalizedPath);
2164
- const ageMs = Date.now() - Number(stats.mtimeMs || 0);
2165
- if (Number.isFinite(ageMs) && ageMs >= staleMs) {
2166
- fs.rmSync(normalizedPath, { force: true });
2167
- return true;
2168
- }
2169
- } catch {}
2170
- return false;
2171
- }
2172
-
2173
- function waitForBotRunnerStateLockRelease(filePath, timeoutMs = 5000, staleMs = 60000) {
2174
- const lockPath = botRunnerStateLockFilePath(filePath);
2175
- if (!lockPath) {
2176
- return;
2177
- }
2178
- const deadlineMs = Date.now() + Math.max(0, Number(timeoutMs) || 0);
2179
- while (fs.existsSync(lockPath)) {
2180
- tryReleaseStaleBotRunnerStateLock(lockPath, staleMs);
2181
- if (!fs.existsSync(lockPath)) {
2182
- return;
2183
- }
2184
- if (Date.now() >= deadlineMs) {
2185
- return;
2186
- }
2187
- sleepSyncMs(25);
2188
- }
2189
- }
2190
-
2191
- function withBotRunnerStateFileLock(filePath, callback, timeoutMs = 5000, staleMs = 60000) {
2192
- const lockPath = botRunnerStateLockFilePath(filePath);
2193
- if (!lockPath) {
2194
- return callback();
2195
- }
2196
- const deadlineMs = Date.now() + Math.max(0, Number(timeoutMs) || 0);
2197
- let lockFD = null;
2198
- while (lockFD === null) {
2199
- try {
2200
- lockFD = fs.openSync(lockPath, "wx");
2201
- fs.writeFileSync(lockFD, `${process.pid} ${new Date().toISOString()}\n`, "utf8");
2202
- break;
2203
- } catch (error) {
2204
- const errorCode = String(error?.code || "").trim().toUpperCase();
2205
- if (!["EEXIST", "EPERM", "EBUSY"].includes(errorCode)) {
2206
- throw error;
2207
- }
2208
- tryReleaseStaleBotRunnerStateLock(lockPath, staleMs);
2209
- if (Date.now() >= deadlineMs) {
2210
- throw error;
2211
- }
2212
- sleepSyncMs(25);
2213
- }
2214
- }
2215
- try {
2216
- return callback();
2217
- } finally {
2218
- try {
2219
- if (lockFD !== null) {
2220
- fs.closeSync(lockFD);
2221
- }
2222
- } catch {}
2223
- try {
2224
- if (fs.existsSync(lockPath)) {
2225
- fs.rmSync(lockPath, { force: true });
2226
- }
2227
- } catch {}
2228
- }
2229
- }
2230
-
2231
- function writeTextFileAtomic(filePath, text) {
2232
- const normalizedPath = String(filePath || "").trim();
2233
- if (!normalizedPath) {
2234
- throw new Error("filePath is required");
2235
- }
2140
+ }
2141
+
2142
+ function sleepSyncMs(delayMs) {
2143
+ const ms = Number(delayMs) || 0;
2144
+ if (!(ms > 0)) {
2145
+ return;
2146
+ }
2147
+ const sleepBuffer = new SharedArrayBuffer(4);
2148
+ const sleepArray = new Int32Array(sleepBuffer);
2149
+ Atomics.wait(sleepArray, 0, 0, ms);
2150
+ }
2151
+
2152
+ function botRunnerStateLockFilePath(filePath) {
2153
+ const normalizedPath = String(filePath || "").trim();
2154
+ return normalizedPath ? `${normalizedPath}.lock` : "";
2155
+ }
2156
+
2157
+ function tryReleaseStaleBotRunnerStateLock(lockPath, staleMs = 60000) {
2158
+ const normalizedPath = String(lockPath || "").trim();
2159
+ if (!normalizedPath) {
2160
+ return false;
2161
+ }
2162
+ try {
2163
+ const stats = fs.statSync(normalizedPath);
2164
+ const ageMs = Date.now() - Number(stats.mtimeMs || 0);
2165
+ if (Number.isFinite(ageMs) && ageMs >= staleMs) {
2166
+ fs.rmSync(normalizedPath, { force: true });
2167
+ return true;
2168
+ }
2169
+ } catch {}
2170
+ return false;
2171
+ }
2172
+
2173
+ function waitForBotRunnerStateLockRelease(filePath, timeoutMs = 5000, staleMs = 60000) {
2174
+ const lockPath = botRunnerStateLockFilePath(filePath);
2175
+ if (!lockPath) {
2176
+ return;
2177
+ }
2178
+ const deadlineMs = Date.now() + Math.max(0, Number(timeoutMs) || 0);
2179
+ while (fs.existsSync(lockPath)) {
2180
+ tryReleaseStaleBotRunnerStateLock(lockPath, staleMs);
2181
+ if (!fs.existsSync(lockPath)) {
2182
+ return;
2183
+ }
2184
+ if (Date.now() >= deadlineMs) {
2185
+ return;
2186
+ }
2187
+ sleepSyncMs(25);
2188
+ }
2189
+ }
2190
+
2191
+ function withBotRunnerStateFileLock(filePath, callback, timeoutMs = 5000, staleMs = 60000) {
2192
+ const lockPath = botRunnerStateLockFilePath(filePath);
2193
+ if (!lockPath) {
2194
+ return callback();
2195
+ }
2196
+ const deadlineMs = Date.now() + Math.max(0, Number(timeoutMs) || 0);
2197
+ let lockFD = null;
2198
+ while (lockFD === null) {
2199
+ try {
2200
+ lockFD = fs.openSync(lockPath, "wx");
2201
+ fs.writeFileSync(lockFD, `${process.pid} ${new Date().toISOString()}\n`, "utf8");
2202
+ break;
2203
+ } catch (error) {
2204
+ const errorCode = String(error?.code || "").trim().toUpperCase();
2205
+ if (!["EEXIST", "EPERM", "EBUSY"].includes(errorCode)) {
2206
+ throw error;
2207
+ }
2208
+ tryReleaseStaleBotRunnerStateLock(lockPath, staleMs);
2209
+ if (Date.now() >= deadlineMs) {
2210
+ throw error;
2211
+ }
2212
+ sleepSyncMs(25);
2213
+ }
2214
+ }
2215
+ try {
2216
+ return callback();
2217
+ } finally {
2218
+ try {
2219
+ if (lockFD !== null) {
2220
+ fs.closeSync(lockFD);
2221
+ }
2222
+ } catch {}
2223
+ try {
2224
+ if (fs.existsSync(lockPath)) {
2225
+ fs.rmSync(lockPath, { force: true });
2226
+ }
2227
+ } catch {}
2228
+ }
2229
+ }
2230
+
2231
+ function writeTextFileAtomic(filePath, text) {
2232
+ const normalizedPath = String(filePath || "").trim();
2233
+ if (!normalizedPath) {
2234
+ throw new Error("filePath is required");
2235
+ }
2236
2236
  const directoryPath = path.dirname(normalizedPath);
2237
2237
  fs.mkdirSync(directoryPath, { recursive: true });
2238
2238
  const tempPath = path.join(
@@ -2240,41 +2240,41 @@ function writeTextFileAtomic(filePath, text) {
2240
2240
  `.${path.basename(normalizedPath)}.${process.pid}.${Date.now()}.tmp`,
2241
2241
  );
2242
2242
  fs.writeFileSync(tempPath, text, "utf8");
2243
- const renameRetryDelaysMs = [0, 20, 50, 100, 200];
2244
- let lastRenameError = null;
2245
- try {
2246
- for (const delayMs of renameRetryDelaysMs) {
2247
- if (delayMs > 0) {
2248
- sleepSyncMs(delayMs);
2249
- }
2250
- try {
2251
- fs.renameSync(tempPath, normalizedPath);
2252
- lastRenameError = null;
2253
- break;
2254
- } catch (error) {
2255
- lastRenameError = error;
2256
- try {
2257
- fs.copyFileSync(tempPath, normalizedPath);
2258
- lastRenameError = null;
2259
- break;
2260
- } catch (copyError) {
2261
- lastRenameError = copyError;
2262
- try {
2263
- fs.writeFileSync(normalizedPath, text, "utf8");
2264
- lastRenameError = null;
2265
- break;
2266
- } catch (writeError) {
2267
- lastRenameError = writeError;
2268
- }
2269
- const errorCode = String(
2270
- lastRenameError?.code || copyError?.code || error?.code || "",
2271
- ).trim().toUpperCase();
2272
- if (!["EPERM", "EBUSY", "ENOTEMPTY"].includes(errorCode)) {
2273
- throw lastRenameError;
2274
- }
2275
- }
2276
- }
2277
- }
2243
+ const renameRetryDelaysMs = [0, 20, 50, 100, 200];
2244
+ let lastRenameError = null;
2245
+ try {
2246
+ for (const delayMs of renameRetryDelaysMs) {
2247
+ if (delayMs > 0) {
2248
+ sleepSyncMs(delayMs);
2249
+ }
2250
+ try {
2251
+ fs.renameSync(tempPath, normalizedPath);
2252
+ lastRenameError = null;
2253
+ break;
2254
+ } catch (error) {
2255
+ lastRenameError = error;
2256
+ try {
2257
+ fs.copyFileSync(tempPath, normalizedPath);
2258
+ lastRenameError = null;
2259
+ break;
2260
+ } catch (copyError) {
2261
+ lastRenameError = copyError;
2262
+ try {
2263
+ fs.writeFileSync(normalizedPath, text, "utf8");
2264
+ lastRenameError = null;
2265
+ break;
2266
+ } catch (writeError) {
2267
+ lastRenameError = writeError;
2268
+ }
2269
+ const errorCode = String(
2270
+ lastRenameError?.code || copyError?.code || error?.code || "",
2271
+ ).trim().toUpperCase();
2272
+ if (!["EPERM", "EBUSY", "ENOTEMPTY"].includes(errorCode)) {
2273
+ throw lastRenameError;
2274
+ }
2275
+ }
2276
+ }
2277
+ }
2278
2278
  if (lastRenameError) {
2279
2279
  throw lastRenameError;
2280
2280
  }
@@ -2287,188 +2287,188 @@ function writeTextFileAtomic(filePath, text) {
2287
2287
  }
2288
2288
  }
2289
2289
 
2290
- function saveBotRunnerState(nextState) {
2291
- const filePath = botRunnerStateFilePath();
2292
- return withBotRunnerStateFileLock(filePath, () => {
2293
- let current = {};
2294
- try {
2295
- current = safeObject(tryJsonParse(fs.readFileSync(filePath, "utf8")));
2296
- } catch {}
2297
- const stateEntryTimestampMs = (...values) => {
2298
- for (const value of values) {
2299
- const ms = Date.parse(String(value || "").trim());
2300
- if (Number.isFinite(ms)) {
2301
- return ms;
2302
- }
2303
- }
2304
- return 0;
2305
- };
2306
- const mergeRunnerStateRoutes = (currentRoutesRaw, nextRoutesRaw) => {
2307
- const currentRoutes = safeObject(currentRoutesRaw);
2308
- const nextRoutes = safeObject(nextRoutesRaw);
2309
- const merged = {
2310
- ...currentRoutes,
2311
- };
2312
- const mergeConversationSessions = (currentSessionsRaw, nextSessionsRaw) => {
2313
- const currentSessions = safeObject(currentSessionsRaw);
2314
- const nextSessions = safeObject(nextSessionsRaw);
2315
- const mergedSessions = {
2316
- ...currentSessions,
2317
- };
2318
- for (const [conversationID, nextSessionRaw] of Object.entries(nextSessions)) {
2319
- const currentSession = safeObject(currentSessions[conversationID]);
2320
- const nextSession = safeObject(nextSessionRaw);
2321
- if (!Object.keys(currentSession).length) {
2322
- mergedSessions[conversationID] = nextSession;
2323
- continue;
2324
- }
2325
- const currentMs = stateEntryTimestampMs(
2326
- currentSession.updated_at,
2327
- currentSession.last_activity_at,
2328
- currentSession.closed_at,
2329
- currentSession.started_at,
2330
- currentSession.expires_at,
2331
- );
2332
- const nextMs = stateEntryTimestampMs(
2333
- nextSession.updated_at,
2334
- nextSession.last_activity_at,
2335
- nextSession.closed_at,
2336
- nextSession.started_at,
2337
- nextSession.expires_at,
2338
- );
2339
- mergedSessions[conversationID] = nextMs >= currentMs
2340
- ? {
2341
- ...currentSession,
2342
- ...nextSession,
2343
- }
2344
- : {
2345
- ...nextSession,
2346
- ...currentSession,
2347
- };
2348
- }
2349
- return mergedSessions;
2350
- };
2351
- for (const [routeKey, nextRouteRaw] of Object.entries(nextRoutes)) {
2352
- const currentRoute = safeObject(currentRoutes[routeKey]);
2353
- const nextRoute = safeObject(nextRouteRaw);
2354
- if (!Object.keys(currentRoute).length) {
2355
- merged[routeKey] = nextRoute;
2356
- continue;
2357
- }
2358
- const preferredRoute = prefersRunnerStateRecord(nextRoute, currentRoute) ? nextRoute : currentRoute;
2359
- const fallbackRoute = preferredRoute === nextRoute ? currentRoute : nextRoute;
2360
- merged[routeKey] = cleanupRunnerStateRecord({
2361
- ...mergeRunnerStateRecords(preferredRoute, fallbackRoute),
2362
- conversation_sessions: mergeConversationSessions(
2363
- currentRoute.conversation_sessions,
2364
- nextRoute.conversation_sessions,
2365
- ),
2366
- });
2367
- }
2368
- return merged;
2369
- };
2370
- const mergeRunnerStateRequests = (currentRequestsRaw, nextRequestsRaw) => {
2371
- const currentRequests = normalizeBotRunnerRequests(currentRequestsRaw);
2372
- const nextRequests = normalizeBotRunnerRequests(nextRequestsRaw);
2373
- const merged = {
2374
- ...currentRequests,
2375
- };
2376
- for (const [requestKey, nextRequestRaw] of Object.entries(nextRequests)) {
2377
- const currentRequest = safeObject(currentRequests[requestKey]);
2378
- const nextRequest = safeObject(nextRequestRaw);
2379
- const currentMs = stateEntryTimestampMs(
2380
- currentRequest.updated_at,
2381
- currentRequest.completed_at,
2382
- currentRequest.closed_at,
2383
- currentRequest.claimed_at,
2384
- );
2385
- const nextMs = stateEntryTimestampMs(
2386
- nextRequest.updated_at,
2387
- nextRequest.completed_at,
2388
- nextRequest.closed_at,
2389
- nextRequest.claimed_at,
2390
- );
2391
- if (!Object.keys(currentRequest).length) {
2392
- merged[requestKey] = nextRequest;
2393
- continue;
2394
- }
2395
- merged[requestKey] = nextMs >= currentMs
2396
- ? {
2397
- ...currentRequest,
2398
- ...nextRequest,
2399
- }
2400
- : currentRequest;
2401
- }
2402
- return normalizeBotRunnerRequests(merged);
2403
- };
2404
- const mergeRunnerStateExcludedComments = (currentExcludedRaw, nextExcludedRaw) => {
2405
- const currentExcluded = normalizeBotRunnerExcludedComments(currentExcludedRaw);
2406
- const nextExcluded = normalizeBotRunnerExcludedComments(nextExcludedRaw);
2407
- const merged = {
2408
- ...currentExcluded,
2409
- };
2410
- for (const [commentID, nextEntryRaw] of Object.entries(nextExcluded)) {
2411
- const currentEntry = safeObject(currentExcluded[commentID]);
2412
- const nextEntry = safeObject(nextEntryRaw);
2413
- const currentMs = stateEntryTimestampMs(currentEntry.excluded_at);
2414
- const nextMs = stateEntryTimestampMs(nextEntry.excluded_at);
2415
- if (!Object.keys(currentEntry).length || nextMs >= currentMs) {
2416
- merged[commentID] = {
2417
- ...currentEntry,
2418
- ...nextEntry,
2419
- };
2420
- }
2421
- }
2422
- return normalizeBotRunnerExcludedComments(merged);
2423
- };
2424
- const mergeRunnerStateConsumedComments = (currentConsumedRaw, nextConsumedRaw) => {
2425
- const currentConsumed = normalizeBotRunnerConsumedComments(currentConsumedRaw);
2426
- const nextConsumed = normalizeBotRunnerConsumedComments(nextConsumedRaw);
2427
- const merged = {
2428
- ...currentConsumed,
2429
- };
2430
- for (const [commentID, nextEntryRaw] of Object.entries(nextConsumed)) {
2431
- const currentEntry = safeObject(currentConsumed[commentID]);
2432
- const nextEntry = safeObject(nextEntryRaw);
2433
- const currentMs = stateEntryTimestampMs(currentEntry.consumed_at);
2434
- const nextMs = stateEntryTimestampMs(nextEntry.consumed_at);
2435
- if (!Object.keys(currentEntry).length || nextMs >= currentMs) {
2436
- merged[commentID] = {
2437
- ...currentEntry,
2438
- ...nextEntry,
2439
- };
2440
- }
2441
- }
2442
- return normalizeBotRunnerConsumedComments(merged);
2443
- };
2444
- const payload = {
2445
- version: 1,
2446
- updated_at: new Date().toISOString(),
2447
- routes: mergeRunnerStateRoutes(
2448
- current.routes,
2449
- nextState?.routes ?? current.routes,
2450
- ),
2451
- shared_inboxes: {
2452
- ...safeObject(current.shared_inboxes ?? current.sharedInboxes),
2453
- ...safeObject(nextState?.sharedInboxes ?? nextState?.shared_inboxes),
2454
- },
2455
- excluded_comments: mergeRunnerStateExcludedComments(
2456
- current.excluded_comments ?? current.excludedComments,
2457
- nextState?.excludedComments ?? nextState?.excluded_comments ?? current.excluded_comments ?? current.excludedComments,
2458
- ),
2459
- requests: mergeRunnerStateRequests(
2460
- current.requests,
2461
- nextState?.requests ?? current.requests,
2462
- ),
2463
- consumed_comments: mergeRunnerStateConsumedComments(
2464
- current.consumed_comments ?? current.consumedComments,
2465
- nextState?.consumedComments ?? nextState?.consumed_comments ?? current.consumed_comments ?? current.consumedComments,
2466
- ),
2467
- };
2468
- writeTextFileAtomic(filePath, `${JSON.stringify(payload, null, 2)}\n`);
2469
- return filePath;
2470
- });
2471
- }
2290
+ function saveBotRunnerState(nextState) {
2291
+ const filePath = botRunnerStateFilePath();
2292
+ return withBotRunnerStateFileLock(filePath, () => {
2293
+ let current = {};
2294
+ try {
2295
+ current = safeObject(tryJsonParse(fs.readFileSync(filePath, "utf8")));
2296
+ } catch {}
2297
+ const stateEntryTimestampMs = (...values) => {
2298
+ for (const value of values) {
2299
+ const ms = Date.parse(String(value || "").trim());
2300
+ if (Number.isFinite(ms)) {
2301
+ return ms;
2302
+ }
2303
+ }
2304
+ return 0;
2305
+ };
2306
+ const mergeRunnerStateRoutes = (currentRoutesRaw, nextRoutesRaw) => {
2307
+ const currentRoutes = safeObject(currentRoutesRaw);
2308
+ const nextRoutes = safeObject(nextRoutesRaw);
2309
+ const merged = {
2310
+ ...currentRoutes,
2311
+ };
2312
+ const mergeConversationSessions = (currentSessionsRaw, nextSessionsRaw) => {
2313
+ const currentSessions = safeObject(currentSessionsRaw);
2314
+ const nextSessions = safeObject(nextSessionsRaw);
2315
+ const mergedSessions = {
2316
+ ...currentSessions,
2317
+ };
2318
+ for (const [conversationID, nextSessionRaw] of Object.entries(nextSessions)) {
2319
+ const currentSession = safeObject(currentSessions[conversationID]);
2320
+ const nextSession = safeObject(nextSessionRaw);
2321
+ if (!Object.keys(currentSession).length) {
2322
+ mergedSessions[conversationID] = nextSession;
2323
+ continue;
2324
+ }
2325
+ const currentMs = stateEntryTimestampMs(
2326
+ currentSession.updated_at,
2327
+ currentSession.last_activity_at,
2328
+ currentSession.closed_at,
2329
+ currentSession.started_at,
2330
+ currentSession.expires_at,
2331
+ );
2332
+ const nextMs = stateEntryTimestampMs(
2333
+ nextSession.updated_at,
2334
+ nextSession.last_activity_at,
2335
+ nextSession.closed_at,
2336
+ nextSession.started_at,
2337
+ nextSession.expires_at,
2338
+ );
2339
+ mergedSessions[conversationID] = nextMs >= currentMs
2340
+ ? {
2341
+ ...currentSession,
2342
+ ...nextSession,
2343
+ }
2344
+ : {
2345
+ ...nextSession,
2346
+ ...currentSession,
2347
+ };
2348
+ }
2349
+ return mergedSessions;
2350
+ };
2351
+ for (const [routeKey, nextRouteRaw] of Object.entries(nextRoutes)) {
2352
+ const currentRoute = safeObject(currentRoutes[routeKey]);
2353
+ const nextRoute = safeObject(nextRouteRaw);
2354
+ if (!Object.keys(currentRoute).length) {
2355
+ merged[routeKey] = nextRoute;
2356
+ continue;
2357
+ }
2358
+ const preferredRoute = prefersRunnerStateRecord(nextRoute, currentRoute) ? nextRoute : currentRoute;
2359
+ const fallbackRoute = preferredRoute === nextRoute ? currentRoute : nextRoute;
2360
+ merged[routeKey] = cleanupRunnerStateRecord({
2361
+ ...mergeRunnerStateRecords(preferredRoute, fallbackRoute),
2362
+ conversation_sessions: mergeConversationSessions(
2363
+ currentRoute.conversation_sessions,
2364
+ nextRoute.conversation_sessions,
2365
+ ),
2366
+ });
2367
+ }
2368
+ return merged;
2369
+ };
2370
+ const mergeRunnerStateRequests = (currentRequestsRaw, nextRequestsRaw) => {
2371
+ const currentRequests = normalizeBotRunnerRequests(currentRequestsRaw);
2372
+ const nextRequests = normalizeBotRunnerRequests(nextRequestsRaw);
2373
+ const merged = {
2374
+ ...currentRequests,
2375
+ };
2376
+ for (const [requestKey, nextRequestRaw] of Object.entries(nextRequests)) {
2377
+ const currentRequest = safeObject(currentRequests[requestKey]);
2378
+ const nextRequest = safeObject(nextRequestRaw);
2379
+ const currentMs = stateEntryTimestampMs(
2380
+ currentRequest.updated_at,
2381
+ currentRequest.completed_at,
2382
+ currentRequest.closed_at,
2383
+ currentRequest.claimed_at,
2384
+ );
2385
+ const nextMs = stateEntryTimestampMs(
2386
+ nextRequest.updated_at,
2387
+ nextRequest.completed_at,
2388
+ nextRequest.closed_at,
2389
+ nextRequest.claimed_at,
2390
+ );
2391
+ if (!Object.keys(currentRequest).length) {
2392
+ merged[requestKey] = nextRequest;
2393
+ continue;
2394
+ }
2395
+ merged[requestKey] = nextMs >= currentMs
2396
+ ? {
2397
+ ...currentRequest,
2398
+ ...nextRequest,
2399
+ }
2400
+ : currentRequest;
2401
+ }
2402
+ return normalizeBotRunnerRequests(merged);
2403
+ };
2404
+ const mergeRunnerStateExcludedComments = (currentExcludedRaw, nextExcludedRaw) => {
2405
+ const currentExcluded = normalizeBotRunnerExcludedComments(currentExcludedRaw);
2406
+ const nextExcluded = normalizeBotRunnerExcludedComments(nextExcludedRaw);
2407
+ const merged = {
2408
+ ...currentExcluded,
2409
+ };
2410
+ for (const [commentID, nextEntryRaw] of Object.entries(nextExcluded)) {
2411
+ const currentEntry = safeObject(currentExcluded[commentID]);
2412
+ const nextEntry = safeObject(nextEntryRaw);
2413
+ const currentMs = stateEntryTimestampMs(currentEntry.excluded_at);
2414
+ const nextMs = stateEntryTimestampMs(nextEntry.excluded_at);
2415
+ if (!Object.keys(currentEntry).length || nextMs >= currentMs) {
2416
+ merged[commentID] = {
2417
+ ...currentEntry,
2418
+ ...nextEntry,
2419
+ };
2420
+ }
2421
+ }
2422
+ return normalizeBotRunnerExcludedComments(merged);
2423
+ };
2424
+ const mergeRunnerStateConsumedComments = (currentConsumedRaw, nextConsumedRaw) => {
2425
+ const currentConsumed = normalizeBotRunnerConsumedComments(currentConsumedRaw);
2426
+ const nextConsumed = normalizeBotRunnerConsumedComments(nextConsumedRaw);
2427
+ const merged = {
2428
+ ...currentConsumed,
2429
+ };
2430
+ for (const [commentID, nextEntryRaw] of Object.entries(nextConsumed)) {
2431
+ const currentEntry = safeObject(currentConsumed[commentID]);
2432
+ const nextEntry = safeObject(nextEntryRaw);
2433
+ const currentMs = stateEntryTimestampMs(currentEntry.consumed_at);
2434
+ const nextMs = stateEntryTimestampMs(nextEntry.consumed_at);
2435
+ if (!Object.keys(currentEntry).length || nextMs >= currentMs) {
2436
+ merged[commentID] = {
2437
+ ...currentEntry,
2438
+ ...nextEntry,
2439
+ };
2440
+ }
2441
+ }
2442
+ return normalizeBotRunnerConsumedComments(merged);
2443
+ };
2444
+ const payload = {
2445
+ version: 1,
2446
+ updated_at: new Date().toISOString(),
2447
+ routes: mergeRunnerStateRoutes(
2448
+ current.routes,
2449
+ nextState?.routes ?? current.routes,
2450
+ ),
2451
+ shared_inboxes: {
2452
+ ...safeObject(current.shared_inboxes ?? current.sharedInboxes),
2453
+ ...safeObject(nextState?.sharedInboxes ?? nextState?.shared_inboxes),
2454
+ },
2455
+ excluded_comments: mergeRunnerStateExcludedComments(
2456
+ current.excluded_comments ?? current.excludedComments,
2457
+ nextState?.excludedComments ?? nextState?.excluded_comments ?? current.excluded_comments ?? current.excludedComments,
2458
+ ),
2459
+ requests: mergeRunnerStateRequests(
2460
+ current.requests,
2461
+ nextState?.requests ?? current.requests,
2462
+ ),
2463
+ consumed_comments: mergeRunnerStateConsumedComments(
2464
+ current.consumed_comments ?? current.consumedComments,
2465
+ nextState?.consumedComments ?? nextState?.consumed_comments ?? current.consumed_comments ?? current.consumedComments,
2466
+ ),
2467
+ };
2468
+ writeTextFileAtomic(filePath, `${JSON.stringify(payload, null, 2)}\n`);
2469
+ return filePath;
2470
+ });
2471
+ }
2472
2472
 
2473
2473
  function normalizeBotRunnerExcludedComments(rawExcluded, nowMs = Date.now()) {
2474
2474
  const normalized = {};
@@ -2525,388 +2525,382 @@ function isActiveRunnerRequestStatus(rawStatus) {
2525
2525
  return status === "planned" || status === "claimed" || status === "running";
2526
2526
  }
2527
2527
 
2528
- function normalizeRunnerWorkItemStatus(rawStatus) {
2529
- const status = String(rawStatus || "").trim().toLowerCase();
2530
- return ["backlog", "doing", "review", "done", "canceled"].includes(status)
2531
- ? status
2532
- : "";
2533
- }
2534
-
2535
- function truncateRunnerReplyChainText(rawValue, maxLength = 280) {
2536
- const text = String(rawValue || "").trim().replace(/\s+/g, " ");
2537
- if (!text) return "";
2538
- if (!(maxLength > 0) || text.length <= maxLength) {
2539
- return text;
2540
- }
2541
- return `${text.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
2542
- }
2543
-
2544
- function buildRunnerReplyChainSpeakerType(parsedArchiveRaw) {
2545
- const parsedArchive = safeObject(parsedArchiveRaw);
2546
- const kind = String(parsedArchive.kind || "").trim().toLowerCase();
2547
- if (kind === "bot_reply" || parsedArchive.senderIsBot === true) {
2548
- return "bot";
2549
- }
2550
- return "human";
2551
- }
2552
-
2553
- function buildRunnerReplyChainSpeakerLabel(parsedArchiveRaw) {
2554
- const parsedArchive = safeObject(parsedArchiveRaw);
2555
- const username = normalizeTelegramMentionUsername(
2556
- parsedArchive.botUsername
2557
- || parsedArchive.username
2558
- || parsedArchive.replyToUsername
2559
- || parsedArchive.conversationTargetBotUsername,
2560
- );
2561
- if (username) {
2562
- return `@${username}`;
2563
- }
2564
- return firstNonEmptyString([
2565
- parsedArchive.sender,
2566
- parsedArchive.botName,
2567
- parsedArchive.replyToSender,
2568
- parsedArchive.chatTitle,
2569
- buildRunnerReplyChainSpeakerType(parsedArchive) === "bot" ? "bot" : "human",
2570
- ]);
2571
- }
2572
-
2573
- function normalizeRunnerReplyChainSnapshot(rawSnapshot) {
2574
- const snapshot = safeObject(rawSnapshot);
2575
- const messageID = intFromRawAllowZero(snapshot.message_id || snapshot.messageID, 0);
2576
- const messageThreadID = intFromRawAllowZero(snapshot.message_thread_id || snapshot.messageThreadID, 0);
2577
- const replyToMessageID = intFromRawAllowZero(snapshot.reply_to_message_id || snapshot.replyToMessageID, 0);
2578
- const speakerType = String(snapshot.speaker_type || snapshot.speakerType || "").trim().toLowerCase();
2579
- const body = truncateRunnerReplyChainText(snapshot.body || "");
2580
- if (!(messageID > 0) && !body) {
2581
- return null;
2582
- }
2583
- return {
2584
- message_id: messageID || undefined,
2585
- message_thread_id: messageThreadID || undefined,
2586
- reply_to_message_id: replyToMessageID || undefined,
2587
- speaker_type: speakerType === "bot" ? "bot" : "human",
2588
- speaker_label: String(snapshot.speaker_label || snapshot.speakerLabel || "").trim(),
2589
- body,
2590
- };
2591
- }
2592
-
2593
- function buildRunnerReplyChainSnapshotFromMessageEnvelope(envelopeRaw, overrides = {}) {
2594
- const envelope = normalizeRunnerTelegramMessageEnvelope(envelopeRaw);
2595
- if (!Object.keys(envelope).length) {
2596
- return null;
2597
- }
2598
- return normalizeRunnerReplyChainSnapshot({
2599
- message_id: envelope.message_id,
2600
- message_thread_id: envelope.message_thread_id,
2601
- reply_to_message_id: envelope.reply_to_message_id,
2602
- speaker_type: firstNonEmptyString([
2603
- overrides.speaker_type,
2604
- envelope.sender_is_bot === true ? "bot" : "human",
2605
- ]),
2606
- speaker_label: firstNonEmptyString([
2607
- overrides.speaker_label,
2608
- envelope.sender_username ? `@${String(envelope.sender_username || "").trim().replace(/^@+/, "")}` : "",
2609
- envelope.sender,
2610
- ]),
2611
- body: firstNonEmptyString([overrides.body, envelope.body]),
2612
- });
2613
- }
2614
-
2615
- function normalizeRunnerReplyChainContext(rawContext) {
2616
- const context = safeObject(rawContext);
2617
- const normalized = {
2618
- conversation_id: String(context.conversation_id || context.conversationID || "").trim(),
2619
- current_message_id: intFromRawAllowZero(context.current_message_id || context.currentMessageID, 0) || undefined,
2620
- current_message_thread_id: intFromRawAllowZero(
2621
- context.current_message_thread_id || context.currentMessageThreadID,
2622
- 0,
2623
- ) || undefined,
2624
- parent_message_id: intFromRawAllowZero(context.parent_message_id || context.parentMessageID, 0) || undefined,
2625
- root_message_id: intFromRawAllowZero(context.root_message_id || context.rootMessageID, 0) || undefined,
2626
- current_turn_relation: String(context.current_turn_relation || context.currentTurnRelation || "").trim(),
2627
- root_message: normalizeRunnerReplyChainSnapshot(context.root_message || context.rootMessage),
2628
- parent_message: normalizeRunnerReplyChainSnapshot(context.parent_message || context.parentMessage),
2629
- latest_prior_bot_reply: normalizeRunnerReplyChainSnapshot(
2630
- context.latest_prior_bot_reply || context.latestPriorBotReply,
2631
- ),
2632
- recent_chain_messages: ensureArray(context.recent_chain_messages || context.recentChainMessages)
2633
- .map((item) => normalizeRunnerReplyChainSnapshot(item))
2634
- .filter(Boolean)
2635
- .slice(-4),
2636
- };
2637
- if (
2638
- !normalized.conversation_id
2639
- && !(normalized.current_message_id > 0)
2640
- && !(normalized.current_message_thread_id > 0)
2641
- && !(normalized.parent_message_id > 0)
2642
- && !(normalized.root_message_id > 0)
2643
- && !normalized.current_turn_relation
2644
- && !normalized.root_message
2645
- && !normalized.parent_message
2646
- && !normalized.latest_prior_bot_reply
2647
- && !normalized.recent_chain_messages.length
2648
- ) {
2649
- return {};
2650
- }
2651
- return normalized;
2652
- }
2653
-
2654
- function findRunnerRouteLocalInboundEnvelope(routeStateRaw, parsedArchiveRaw) {
2655
- const routeState = safeObject(routeStateRaw);
2656
- const parsedArchive = safeObject(parsedArchiveRaw);
2657
- const chatID = String(parsedArchive.chatID || parsedArchive.chatId || "").trim();
2658
- const messageID = intFromRawAllowZero(parsedArchive.messageID, 0);
2659
- return findRunnerRouteLocalInboundEnvelopeByMessage(routeState, {
2660
- chatID,
2661
- messageID,
2662
- });
2663
- }
2664
-
2665
- function findRunnerRouteLocalInboundEnvelopeByMessage(routeStateRaw, {
2666
- chatID = "",
2667
- messageID = 0,
2668
- } = {}) {
2669
- const routeState = safeObject(routeStateRaw);
2670
- const normalizedChatID = String(chatID || "").trim();
2671
- const normalizedMessageID = intFromRawAllowZero(messageID, 0);
2672
- if (!normalizedChatID || !(normalizedMessageID > 0)) {
2673
- return {};
2674
- }
2675
- return findRecentTelegramMessageEnvelope(routeState.recent_local_inbound_envelopes, {
2676
- chatID: normalizedChatID,
2677
- messageID: normalizedMessageID,
2678
- });
2679
- }
2680
-
2681
- function mergeRunnerSourceMessageEnvelopes(primaryRaw, fallbackRaw) {
2682
- const primary = normalizeRunnerTelegramMessageEnvelope(primaryRaw);
2683
- const fallback = normalizeRunnerTelegramMessageEnvelope(fallbackRaw);
2684
- return normalizeRunnerTelegramMessageEnvelope({
2685
- ...fallback,
2686
- ...primary,
2687
- chat_id: firstNonEmptyString([primary.chat_id, fallback.chat_id]),
2688
- message_id: intFromRawAllowZero(primary.message_id || fallback.message_id, 0) || undefined,
2689
- message_thread_id: intFromRawAllowZero(
2690
- primary.message_thread_id || fallback.message_thread_id,
2691
- 0,
2692
- ) || undefined,
2693
- reply_to_message_id: intFromRawAllowZero(
2694
- primary.reply_to_message_id || fallback.reply_to_message_id,
2695
- 0,
2696
- ) || undefined,
2697
- body: firstNonEmptyString([primary.body, fallback.body]),
2698
- source_origin: firstNonEmptyString([primary.source_origin, fallback.source_origin]),
2699
- source_route_key: firstNonEmptyString([primary.source_route_key, fallback.source_route_key]),
2700
- source_bot_username: firstNonEmptyString([primary.source_bot_username, fallback.source_bot_username]),
2701
- });
2702
- }
2703
-
2704
- function scoreRunnerSourceMessageEnvelopeAuthority(rawEnvelope) {
2705
- const envelope = normalizeRunnerTelegramMessageEnvelope(rawEnvelope);
2706
- if (!String(envelope.chat_id || "").trim() || !(intFromRawAllowZero(envelope.message_id, 0) > 0)) {
2707
- return 0;
2708
- }
2709
- const origin = String(envelope.source_origin || "").trim().toLowerCase();
2710
- let score = 0;
2711
- switch (origin) {
2712
- case "local_telegram_inbound":
2713
- score = 500;
2714
- break;
2715
- case "trigger_addressed_human_inbound":
2716
- score = 450;
2717
- break;
2718
- case "group_read_visible_human_inbound":
2719
- score = 400;
2720
- break;
2721
- case "archive_reconstructed":
2722
- score = 100;
2723
- break;
2724
- default:
2725
- score = 250;
2726
- break;
2727
- }
2728
- if (String(envelope.source_route_key || "").trim()) score += 10;
2729
- if (String(envelope.source_bot_username || "").trim()) score += 10;
2730
- if (intFromRawAllowZero(envelope.reply_to_message_id, 0) > 0) score += 1;
2731
- return score;
2732
- }
2733
-
2734
- function selectAuthoritativeRunnerSourceMessageEnvelope(primaryRaw, fallbackRaw) {
2735
- const primary = normalizeRunnerTelegramMessageEnvelope(primaryRaw);
2736
- const fallback = normalizeRunnerTelegramMessageEnvelope(fallbackRaw);
2737
- const primaryScore = scoreRunnerSourceMessageEnvelopeAuthority(primary);
2738
- const fallbackScore = scoreRunnerSourceMessageEnvelopeAuthority(fallback);
2739
- if (!(primaryScore > 0)) {
2740
- return fallback;
2741
- }
2742
- if (!(fallbackScore > 0)) {
2743
- return primary;
2744
- }
2745
- if (primaryScore === fallbackScore) {
2746
- return mergeRunnerSourceMessageEnvelopes(primary, fallback);
2747
- }
2748
- return primaryScore > fallbackScore ? primary : fallback;
2749
- }
2750
-
2751
- function buildRunnerSourceMessageEnvelope({
2752
- routeState = {},
2753
- routeKey = "",
2754
- normalizedRoute = null,
2755
- parsedArchive = null,
2756
- }) {
2757
- const localEnvelope = findRunnerRouteLocalInboundEnvelope(routeState, parsedArchive);
2758
- const fallbackBotSelector = normalizeTelegramMentionUsername(
2759
- normalizedRoute?.botName
2760
- || normalizedRoute?.serverBotName
2761
- || "",
2762
- );
2763
- const archivedSourceEnvelope = buildRunnerTelegramMessageEnvelopeFromParsedArchive(parsedArchive);
2764
- if (isTelegramLocalInboundEnvelopeForRoute(localEnvelope, {
2765
- routeKey,
2766
- botUsername: fallbackBotSelector,
2767
- })) {
2768
- return localEnvelope;
2769
- }
2770
- if (isTelegramLocalInboundEnvelopeForRoute(archivedSourceEnvelope, {
2771
- routeKey,
2772
- botUsername: fallbackBotSelector,
2773
- })) {
2774
- return archivedSourceEnvelope;
2775
- }
2776
- return buildRunnerTelegramMessageEnvelopeFromParsedArchive(parsedArchive, {
2777
- source_origin: "archive_reconstructed",
2778
- });
2779
- }
2780
-
2781
- function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2782
- const normalized = {};
2783
- for (const [requestKeyRaw, entryRaw] of Object.entries(safeObject(rawRequests))) {
2528
+ function normalizeRunnerWorkItemStatus(rawStatus) {
2529
+ const status = String(rawStatus || "").trim().toLowerCase();
2530
+ return ["backlog", "doing", "review", "done", "canceled"].includes(status)
2531
+ ? status
2532
+ : "";
2533
+ }
2534
+
2535
+ function truncateRunnerReplyChainText(rawValue, maxLength = 280) {
2536
+ const text = String(rawValue || "").trim().replace(/\s+/g, " ");
2537
+ if (!text) return "";
2538
+ if (!(maxLength > 0) || text.length <= maxLength) {
2539
+ return text;
2540
+ }
2541
+ return `${text.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
2542
+ }
2543
+
2544
+ function buildRunnerReplyChainSpeakerType(parsedArchiveRaw) {
2545
+ const parsedArchive = safeObject(parsedArchiveRaw);
2546
+ const kind = String(parsedArchive.kind || "").trim().toLowerCase();
2547
+ if (kind === "bot_reply" || parsedArchive.senderIsBot === true) {
2548
+ return "bot";
2549
+ }
2550
+ return "human";
2551
+ }
2552
+
2553
+ function buildRunnerReplyChainSpeakerLabel(parsedArchiveRaw) {
2554
+ const parsedArchive = safeObject(parsedArchiveRaw);
2555
+ const username = normalizeTelegramMentionUsername(
2556
+ parsedArchive.botUsername
2557
+ || parsedArchive.username
2558
+ || parsedArchive.replyToUsername
2559
+ || parsedArchive.conversationTargetBotUsername,
2560
+ );
2561
+ if (username) {
2562
+ return `@${username}`;
2563
+ }
2564
+ return firstNonEmptyString([
2565
+ parsedArchive.sender,
2566
+ parsedArchive.botName,
2567
+ parsedArchive.replyToSender,
2568
+ parsedArchive.chatTitle,
2569
+ buildRunnerReplyChainSpeakerType(parsedArchive) === "bot" ? "bot" : "human",
2570
+ ]);
2571
+ }
2572
+
2573
+ function normalizeRunnerReplyChainSnapshot(rawSnapshot) {
2574
+ const snapshot = safeObject(rawSnapshot);
2575
+ const messageID = intFromRawAllowZero(snapshot.message_id || snapshot.messageID, 0);
2576
+ const messageThreadID = intFromRawAllowZero(snapshot.message_thread_id || snapshot.messageThreadID, 0);
2577
+ const replyToMessageID = intFromRawAllowZero(snapshot.reply_to_message_id || snapshot.replyToMessageID, 0);
2578
+ const speakerType = String(snapshot.speaker_type || snapshot.speakerType || "").trim().toLowerCase();
2579
+ const body = truncateRunnerReplyChainText(snapshot.body || "");
2580
+ if (!(messageID > 0) && !body) {
2581
+ return null;
2582
+ }
2583
+ return {
2584
+ message_id: messageID || undefined,
2585
+ message_thread_id: messageThreadID || undefined,
2586
+ reply_to_message_id: replyToMessageID || undefined,
2587
+ speaker_type: speakerType === "bot" ? "bot" : "human",
2588
+ speaker_label: String(snapshot.speaker_label || snapshot.speakerLabel || "").trim(),
2589
+ body,
2590
+ };
2591
+ }
2592
+
2593
+ function buildRunnerReplyChainSnapshotFromMessageEnvelope(envelopeRaw, overrides = {}) {
2594
+ const envelope = normalizeRunnerTelegramMessageEnvelope(envelopeRaw);
2595
+ if (!Object.keys(envelope).length) {
2596
+ return null;
2597
+ }
2598
+ return normalizeRunnerReplyChainSnapshot({
2599
+ message_id: envelope.message_id,
2600
+ message_thread_id: envelope.message_thread_id,
2601
+ reply_to_message_id: envelope.reply_to_message_id,
2602
+ speaker_type: firstNonEmptyString([
2603
+ overrides.speaker_type,
2604
+ envelope.sender_is_bot === true ? "bot" : "human",
2605
+ ]),
2606
+ speaker_label: firstNonEmptyString([
2607
+ overrides.speaker_label,
2608
+ envelope.sender_username ? `@${String(envelope.sender_username || "").trim().replace(/^@+/, "")}` : "",
2609
+ envelope.sender,
2610
+ ]),
2611
+ body: firstNonEmptyString([overrides.body, envelope.body]),
2612
+ });
2613
+ }
2614
+
2615
+ function normalizeRunnerReplyChainContext(rawContext) {
2616
+ const context = safeObject(rawContext);
2617
+ const normalized = {
2618
+ conversation_id: String(context.conversation_id || context.conversationID || "").trim(),
2619
+ current_message_id: intFromRawAllowZero(context.current_message_id || context.currentMessageID, 0) || undefined,
2620
+ current_message_thread_id: intFromRawAllowZero(
2621
+ context.current_message_thread_id || context.currentMessageThreadID,
2622
+ 0,
2623
+ ) || undefined,
2624
+ parent_message_id: intFromRawAllowZero(context.parent_message_id || context.parentMessageID, 0) || undefined,
2625
+ root_message_id: intFromRawAllowZero(context.root_message_id || context.rootMessageID, 0) || undefined,
2626
+ current_turn_relation: String(context.current_turn_relation || context.currentTurnRelation || "").trim(),
2627
+ root_message: normalizeRunnerReplyChainSnapshot(context.root_message || context.rootMessage),
2628
+ parent_message: normalizeRunnerReplyChainSnapshot(context.parent_message || context.parentMessage),
2629
+ latest_prior_bot_reply: normalizeRunnerReplyChainSnapshot(
2630
+ context.latest_prior_bot_reply || context.latestPriorBotReply,
2631
+ ),
2632
+ recent_chain_messages: ensureArray(context.recent_chain_messages || context.recentChainMessages)
2633
+ .map((item) => normalizeRunnerReplyChainSnapshot(item))
2634
+ .filter(Boolean)
2635
+ .slice(-4),
2636
+ };
2637
+ if (
2638
+ !normalized.conversation_id
2639
+ && !(normalized.current_message_id > 0)
2640
+ && !(normalized.current_message_thread_id > 0)
2641
+ && !(normalized.parent_message_id > 0)
2642
+ && !(normalized.root_message_id > 0)
2643
+ && !normalized.current_turn_relation
2644
+ && !normalized.root_message
2645
+ && !normalized.parent_message
2646
+ && !normalized.latest_prior_bot_reply
2647
+ && !normalized.recent_chain_messages.length
2648
+ ) {
2649
+ return {};
2650
+ }
2651
+ return normalized;
2652
+ }
2653
+
2654
+ function findRunnerRouteLocalInboundEnvelope(routeStateRaw, parsedArchiveRaw) {
2655
+ const routeState = safeObject(routeStateRaw);
2656
+ const parsedArchive = safeObject(parsedArchiveRaw);
2657
+ const chatID = String(parsedArchive.chatID || parsedArchive.chatId || "").trim();
2658
+ const messageID = intFromRawAllowZero(parsedArchive.messageID, 0);
2659
+ return findRunnerRouteLocalInboundEnvelopeByMessage(routeState, {
2660
+ chatID,
2661
+ messageID,
2662
+ });
2663
+ }
2664
+
2665
+ function findRunnerRouteLocalInboundEnvelopeByMessage(routeStateRaw, {
2666
+ chatID = "",
2667
+ messageID = 0,
2668
+ } = {}) {
2669
+ const routeState = safeObject(routeStateRaw);
2670
+ const normalizedChatID = String(chatID || "").trim();
2671
+ const normalizedMessageID = intFromRawAllowZero(messageID, 0);
2672
+ if (!normalizedChatID || !(normalizedMessageID > 0)) {
2673
+ return {};
2674
+ }
2675
+ return findRecentTelegramMessageEnvelope(routeState.recent_local_inbound_envelopes, {
2676
+ chatID: normalizedChatID,
2677
+ messageID: normalizedMessageID,
2678
+ });
2679
+ }
2680
+
2681
+ function mergeRunnerSourceMessageEnvelopes(primaryRaw, fallbackRaw) {
2682
+ const primary = normalizeRunnerTelegramMessageEnvelope(primaryRaw);
2683
+ const fallback = normalizeRunnerTelegramMessageEnvelope(fallbackRaw);
2684
+ return normalizeRunnerTelegramMessageEnvelope({
2685
+ ...fallback,
2686
+ ...primary,
2687
+ chat_id: firstNonEmptyString([primary.chat_id, fallback.chat_id]),
2688
+ message_id: intFromRawAllowZero(primary.message_id || fallback.message_id, 0) || undefined,
2689
+ message_thread_id: intFromRawAllowZero(
2690
+ primary.message_thread_id || fallback.message_thread_id,
2691
+ 0,
2692
+ ) || undefined,
2693
+ reply_to_message_id: intFromRawAllowZero(
2694
+ primary.reply_to_message_id || fallback.reply_to_message_id,
2695
+ 0,
2696
+ ) || undefined,
2697
+ body: firstNonEmptyString([primary.body, fallback.body]),
2698
+ source_origin: firstNonEmptyString([primary.source_origin, fallback.source_origin]),
2699
+ source_route_key: firstNonEmptyString([primary.source_route_key, fallback.source_route_key]),
2700
+ source_bot_username: firstNonEmptyString([primary.source_bot_username, fallback.source_bot_username]),
2701
+ });
2702
+ }
2703
+
2704
+ function scoreRunnerSourceMessageEnvelopeAuthority(rawEnvelope) {
2705
+ const envelope = normalizeRunnerTelegramMessageEnvelope(rawEnvelope);
2706
+ if (!String(envelope.chat_id || "").trim() || !(intFromRawAllowZero(envelope.message_id, 0) > 0)) {
2707
+ return 0;
2708
+ }
2709
+ const origin = String(envelope.source_origin || "").trim().toLowerCase();
2710
+ let score = 0;
2711
+ switch (origin) {
2712
+ case "local_telegram_inbound":
2713
+ score = 500;
2714
+ break;
2715
+ case "archive_reconstructed":
2716
+ score = 100;
2717
+ break;
2718
+ default:
2719
+ score = 50;
2720
+ break;
2721
+ }
2722
+ if (String(envelope.source_route_key || "").trim()) score += 10;
2723
+ if (String(envelope.source_bot_username || "").trim()) score += 10;
2724
+ if (intFromRawAllowZero(envelope.reply_to_message_id, 0) > 0) score += 1;
2725
+ return score;
2726
+ }
2727
+
2728
+ function selectAuthoritativeRunnerSourceMessageEnvelope(primaryRaw, fallbackRaw) {
2729
+ const primary = normalizeRunnerTelegramMessageEnvelope(primaryRaw);
2730
+ const fallback = normalizeRunnerTelegramMessageEnvelope(fallbackRaw);
2731
+ const primaryScore = scoreRunnerSourceMessageEnvelopeAuthority(primary);
2732
+ const fallbackScore = scoreRunnerSourceMessageEnvelopeAuthority(fallback);
2733
+ if (!(primaryScore > 0)) {
2734
+ return fallback;
2735
+ }
2736
+ if (!(fallbackScore > 0)) {
2737
+ return primary;
2738
+ }
2739
+ if (primaryScore === fallbackScore) {
2740
+ return mergeRunnerSourceMessageEnvelopes(primary, fallback);
2741
+ }
2742
+ return primaryScore > fallbackScore ? primary : fallback;
2743
+ }
2744
+
2745
+ function buildRunnerSourceMessageEnvelope({
2746
+ routeState = {},
2747
+ routeKey = "",
2748
+ normalizedRoute = null,
2749
+ parsedArchive = null,
2750
+ }) {
2751
+ const localEnvelope = findRunnerRouteLocalInboundEnvelope(routeState, parsedArchive);
2752
+ const fallbackBotSelector = normalizeTelegramMentionUsername(
2753
+ normalizedRoute?.botName
2754
+ || normalizedRoute?.serverBotName
2755
+ || "",
2756
+ );
2757
+ const archivedSourceEnvelope = buildRunnerTelegramMessageEnvelopeFromParsedArchive(parsedArchive);
2758
+ if (isTelegramLocalInboundEnvelopeForRoute(localEnvelope, {
2759
+ routeKey,
2760
+ botUsername: fallbackBotSelector,
2761
+ })) {
2762
+ return localEnvelope;
2763
+ }
2764
+ if (isTelegramLocalInboundEnvelopeForRoute(archivedSourceEnvelope, {
2765
+ routeKey,
2766
+ botUsername: fallbackBotSelector,
2767
+ })) {
2768
+ return archivedSourceEnvelope;
2769
+ }
2770
+ return buildRunnerTelegramMessageEnvelopeFromParsedArchive(parsedArchive, {
2771
+ source_origin: "archive_reconstructed",
2772
+ });
2773
+ }
2774
+
2775
+ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2776
+ const normalized = {};
2777
+ for (const [requestKeyRaw, entryRaw] of Object.entries(safeObject(rawRequests))) {
2784
2778
  const requestKey = String(requestKeyRaw || safeObject(entryRaw).request_key || "").trim();
2785
2779
  if (!requestKey) continue;
2786
2780
  const entry = safeObject(entryRaw);
2787
2781
  const status = normalizeRunnerRequestStatus(entry.status);
2788
2782
  const updatedAt = firstNonEmptyString([entry.updated_at, entry.updatedAt, entry.completed_at, entry.completedAt, entry.closed_at, entry.closedAt, entry.claimed_at, entry.claimedAt]);
2789
2783
  const updatedAtMs = Date.parse(updatedAt);
2790
- if (isFinalRunnerRequestStatus(status) && Number.isFinite(updatedAtMs) && nowMs - updatedAtMs > BOT_RUNNER_REQUEST_KEEP_MS) {
2791
- continue;
2792
- }
2793
- const fallbackSourceMessageEnvelope = intFromRawAllowZero(entry.source_message_id || entry.sourceMessageID, 0) > 0
2794
- ? {
2795
- chat_id: entry.chat_id || entry.chatID,
2796
- message_id: entry.source_message_id || entry.sourceMessageID,
2797
- message_thread_id: entry.source_message_thread_id || entry.sourceMessageThreadID,
2798
- body: entry.source_message_body || entry.sourceMessageBody,
2799
- sender: "human",
2800
- sender_is_bot: false,
2801
- source_origin: entry.source_message_origin || entry.sourceMessageOrigin,
2802
- source_route_key: entry.source_message_route_key || entry.sourceMessageRouteKey,
2803
- source_bot_username: entry.source_message_bot_username || entry.sourceMessageBotUsername,
2804
- }
2805
- : {};
2806
- const normalizedSourceMessageEnvelope = normalizeRunnerTelegramMessageEnvelope({
2807
- ...safeObject(
2808
- normalizeRunnerTelegramMessageEnvelope(
2809
- entry.source_message_envelope
2810
- || entry.sourceMessageEnvelope
2811
- || fallbackSourceMessageEnvelope,
2812
- ),
2813
- ),
2814
- chat_id: firstNonEmptyString([
2815
- safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).chat_id,
2816
- safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).chatID,
2817
- entry.chat_id,
2818
- entry.chatID,
2819
- ]),
2820
- message_id: intFromRawAllowZero(
2821
- safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).message_id
2822
- || safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).messageID
2823
- || entry.source_message_id
2824
- || entry.sourceMessageID,
2825
- 0,
2826
- ) || undefined,
2827
- message_thread_id: intFromRawAllowZero(
2828
- safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).message_thread_id
2829
- || safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).messageThreadID
2830
- || entry.source_message_thread_id
2831
- || entry.sourceMessageThreadID,
2832
- 0,
2833
- ) || undefined,
2834
- body: firstNonEmptyString([
2835
- safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).body,
2836
- safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).text,
2837
- entry.source_message_body,
2838
- entry.sourceMessageBody,
2839
- ]),
2840
- source_origin: firstNonEmptyString([
2841
- safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).source_origin,
2842
- safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).sourceOrigin,
2843
- entry.source_message_origin,
2844
- entry.sourceMessageOrigin,
2845
- ]),
2846
- source_route_key: firstNonEmptyString([
2847
- safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).source_route_key,
2848
- safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).sourceRouteKey,
2849
- entry.source_message_route_key,
2850
- entry.sourceMessageRouteKey,
2851
- ]),
2852
- source_bot_username: firstNonEmptyString([
2853
- safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).source_bot_username,
2854
- safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).sourceBotUsername,
2855
- entry.source_message_bot_username,
2856
- entry.sourceMessageBotUsername,
2857
- ]),
2858
- });
2859
- normalized[requestKey] = {
2860
- request_key: requestKey,
2861
- project_id: String(entry.project_id || entry.projectID || "").trim(),
2862
- provider: String(entry.provider || "").trim(),
2863
- chat_id: firstNonEmptyString([
2864
- entry.chat_id,
2865
- entry.chatID,
2866
- normalizedSourceMessageEnvelope.chat_id,
2867
- ]),
2868
- source_message_id: intFromRawAllowZero(
2869
- entry.source_message_id
2870
- || entry.sourceMessageID
2871
- || normalizedSourceMessageEnvelope.message_id,
2872
- 0,
2873
- ) || undefined,
2874
- source_message_thread_id: intFromRawAllowZero(
2875
- entry.source_message_thread_id
2876
- || entry.sourceMessageThreadID
2877
- || normalizedSourceMessageEnvelope.message_thread_id,
2878
- 0,
2879
- ) || undefined,
2880
- source_message_body: firstNonEmptyString([
2881
- entry.source_message_body,
2882
- entry.sourceMessageBody,
2883
- normalizedSourceMessageEnvelope.body,
2884
- ]),
2885
- source_message_origin: String(
2886
- entry.source_message_origin
2887
- || entry.sourceMessageOrigin
2888
- || normalizedSourceMessageEnvelope.source_origin
2889
- || "",
2890
- ).trim().toLowerCase(),
2891
- source_message_route_key: String(
2892
- entry.source_message_route_key
2893
- || entry.sourceMessageRouteKey
2894
- || normalizedSourceMessageEnvelope.source_route_key
2895
- || "",
2896
- ).trim(),
2897
- source_message_bot_username: normalizeTelegramMentionUsername(
2898
- entry.source_message_bot_username
2899
- || entry.sourceMessageBotUsername
2900
- || normalizedSourceMessageEnvelope.source_bot_username,
2901
- ),
2902
- source_message_envelope: normalizedSourceMessageEnvelope,
2903
- root_comment_id: String(entry.root_comment_id || entry.rootCommentID || "").trim(),
2904
- root_comment_kind: String(entry.root_comment_kind || entry.rootCommentKind || "").trim().toLowerCase(),
2905
- conversation_id: String(entry.conversation_id || entry.conversationId || "").trim(),
2906
- reply_chain_context: normalizeRunnerReplyChainContext(entry.reply_chain_context || entry.replyChainContext),
2907
- selected_bot_usernames: ensureArray(entry.selected_bot_usernames || entry.selectedBotUsernames)
2908
- .map((value) => normalizeTelegramMentionUsername(value))
2909
- .filter(Boolean),
2784
+ if (isFinalRunnerRequestStatus(status) && Number.isFinite(updatedAtMs) && nowMs - updatedAtMs > BOT_RUNNER_REQUEST_KEEP_MS) {
2785
+ continue;
2786
+ }
2787
+ const fallbackSourceMessageEnvelope = intFromRawAllowZero(entry.source_message_id || entry.sourceMessageID, 0) > 0
2788
+ ? {
2789
+ chat_id: entry.chat_id || entry.chatID,
2790
+ message_id: entry.source_message_id || entry.sourceMessageID,
2791
+ message_thread_id: entry.source_message_thread_id || entry.sourceMessageThreadID,
2792
+ body: entry.source_message_body || entry.sourceMessageBody,
2793
+ sender: "human",
2794
+ sender_is_bot: false,
2795
+ source_origin: entry.source_message_origin || entry.sourceMessageOrigin,
2796
+ source_route_key: entry.source_message_route_key || entry.sourceMessageRouteKey,
2797
+ source_bot_username: entry.source_message_bot_username || entry.sourceMessageBotUsername,
2798
+ }
2799
+ : {};
2800
+ const normalizedSourceMessageEnvelope = normalizeRunnerTelegramMessageEnvelope({
2801
+ ...safeObject(
2802
+ normalizeRunnerTelegramMessageEnvelope(
2803
+ entry.source_message_envelope
2804
+ || entry.sourceMessageEnvelope
2805
+ || fallbackSourceMessageEnvelope,
2806
+ ),
2807
+ ),
2808
+ chat_id: firstNonEmptyString([
2809
+ safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).chat_id,
2810
+ safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).chatID,
2811
+ entry.chat_id,
2812
+ entry.chatID,
2813
+ ]),
2814
+ message_id: intFromRawAllowZero(
2815
+ safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).message_id
2816
+ || safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).messageID
2817
+ || entry.source_message_id
2818
+ || entry.sourceMessageID,
2819
+ 0,
2820
+ ) || undefined,
2821
+ message_thread_id: intFromRawAllowZero(
2822
+ safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).message_thread_id
2823
+ || safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).messageThreadID
2824
+ || entry.source_message_thread_id
2825
+ || entry.sourceMessageThreadID,
2826
+ 0,
2827
+ ) || undefined,
2828
+ body: firstNonEmptyString([
2829
+ safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).body,
2830
+ safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).text,
2831
+ entry.source_message_body,
2832
+ entry.sourceMessageBody,
2833
+ ]),
2834
+ source_origin: firstNonEmptyString([
2835
+ safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).source_origin,
2836
+ safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).sourceOrigin,
2837
+ entry.source_message_origin,
2838
+ entry.sourceMessageOrigin,
2839
+ ]),
2840
+ source_route_key: firstNonEmptyString([
2841
+ safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).source_route_key,
2842
+ safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).sourceRouteKey,
2843
+ entry.source_message_route_key,
2844
+ entry.sourceMessageRouteKey,
2845
+ ]),
2846
+ source_bot_username: firstNonEmptyString([
2847
+ safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).source_bot_username,
2848
+ safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).sourceBotUsername,
2849
+ entry.source_message_bot_username,
2850
+ entry.sourceMessageBotUsername,
2851
+ ]),
2852
+ });
2853
+ normalized[requestKey] = {
2854
+ request_key: requestKey,
2855
+ project_id: String(entry.project_id || entry.projectID || "").trim(),
2856
+ provider: String(entry.provider || "").trim(),
2857
+ chat_id: firstNonEmptyString([
2858
+ entry.chat_id,
2859
+ entry.chatID,
2860
+ normalizedSourceMessageEnvelope.chat_id,
2861
+ ]),
2862
+ source_message_id: intFromRawAllowZero(
2863
+ entry.source_message_id
2864
+ || entry.sourceMessageID
2865
+ || normalizedSourceMessageEnvelope.message_id,
2866
+ 0,
2867
+ ) || undefined,
2868
+ source_message_thread_id: intFromRawAllowZero(
2869
+ entry.source_message_thread_id
2870
+ || entry.sourceMessageThreadID
2871
+ || normalizedSourceMessageEnvelope.message_thread_id,
2872
+ 0,
2873
+ ) || undefined,
2874
+ source_message_body: firstNonEmptyString([
2875
+ entry.source_message_body,
2876
+ entry.sourceMessageBody,
2877
+ normalizedSourceMessageEnvelope.body,
2878
+ ]),
2879
+ source_message_origin: String(
2880
+ entry.source_message_origin
2881
+ || entry.sourceMessageOrigin
2882
+ || normalizedSourceMessageEnvelope.source_origin
2883
+ || "",
2884
+ ).trim().toLowerCase(),
2885
+ source_message_route_key: String(
2886
+ entry.source_message_route_key
2887
+ || entry.sourceMessageRouteKey
2888
+ || normalizedSourceMessageEnvelope.source_route_key
2889
+ || "",
2890
+ ).trim(),
2891
+ source_message_bot_username: normalizeTelegramMentionUsername(
2892
+ entry.source_message_bot_username
2893
+ || entry.sourceMessageBotUsername
2894
+ || normalizedSourceMessageEnvelope.source_bot_username,
2895
+ ),
2896
+ source_message_envelope: normalizedSourceMessageEnvelope,
2897
+ root_comment_id: String(entry.root_comment_id || entry.rootCommentID || "").trim(),
2898
+ root_comment_kind: String(entry.root_comment_kind || entry.rootCommentKind || "").trim().toLowerCase(),
2899
+ conversation_id: String(entry.conversation_id || entry.conversationId || "").trim(),
2900
+ reply_chain_context: normalizeRunnerReplyChainContext(entry.reply_chain_context || entry.replyChainContext),
2901
+ selected_bot_usernames: ensureArray(entry.selected_bot_usernames || entry.selectedBotUsernames)
2902
+ .map((value) => normalizeTelegramMentionUsername(value))
2903
+ .filter(Boolean),
2910
2904
  conversation_allowed_responders: ensureArray(entry.conversation_allowed_responders || entry.conversationAllowedResponders)
2911
2905
  .map((value) => normalizeTelegramMentionUsername(value))
2912
2906
  .filter(Boolean),
@@ -3020,37 +3014,37 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
3020
3014
  root_thread_id: String(entry.root_thread_id || entry.rootThreadID || "").trim(),
3021
3015
  root_work_item_created_at: firstNonEmptyString([entry.root_work_item_created_at, entry.rootWorkItemCreatedAt]),
3022
3016
  root_work_item_last_error: String(entry.root_work_item_last_error || entry.rootWorkItemLastError || "").trim(),
3023
- last_comment_id: String(entry.last_comment_id || entry.lastCommentID || "").trim(),
3024
- last_comment_kind: String(entry.last_comment_kind || entry.lastCommentKind || "").trim().toLowerCase(),
3025
- last_source_message_id: intFromRawAllowZero(entry.last_source_message_id || entry.lastSourceMessageID, 0) || undefined,
3026
- last_source_message_thread_id: intFromRawAllowZero(
3027
- entry.last_source_message_thread_id || entry.lastSourceMessageThreadID,
3028
- 0,
3029
- ) || undefined,
3030
- last_reply_message_id: intFromRawAllowZero(entry.last_reply_message_id || entry.lastReplyMessageID, 0) || undefined,
3031
- last_reply_message_thread_id: intFromRawAllowZero(
3032
- entry.last_reply_message_thread_id || entry.lastReplyMessageThreadID,
3033
- 0,
3034
- ) || undefined,
3035
- last_reply_to_message_id: intFromRawAllowZero(
3036
- entry.last_reply_to_message_id || entry.lastReplyToMessageID,
3037
- 0,
3038
- ) || undefined,
3039
- last_reply_message_envelope: normalizeRunnerTelegramMessageEnvelope(
3040
- entry.last_reply_message_envelope
3041
- || entry.lastReplyMessageEnvelope
3042
- || {},
3043
- ),
3044
- attempted_delivery_envelope: normalizeRunnerTelegramMessageEnvelope(
3045
- entry.attempted_delivery_envelope
3046
- || entry.attemptedDeliveryEnvelope
3047
- || {},
3048
- ),
3049
- updated_at: updatedAt || new Date(nowMs).toISOString(),
3050
- };
3051
- }
3052
- return normalized;
3053
- }
3017
+ last_comment_id: String(entry.last_comment_id || entry.lastCommentID || "").trim(),
3018
+ last_comment_kind: String(entry.last_comment_kind || entry.lastCommentKind || "").trim().toLowerCase(),
3019
+ last_source_message_id: intFromRawAllowZero(entry.last_source_message_id || entry.lastSourceMessageID, 0) || undefined,
3020
+ last_source_message_thread_id: intFromRawAllowZero(
3021
+ entry.last_source_message_thread_id || entry.lastSourceMessageThreadID,
3022
+ 0,
3023
+ ) || undefined,
3024
+ last_reply_message_id: intFromRawAllowZero(entry.last_reply_message_id || entry.lastReplyMessageID, 0) || undefined,
3025
+ last_reply_message_thread_id: intFromRawAllowZero(
3026
+ entry.last_reply_message_thread_id || entry.lastReplyMessageThreadID,
3027
+ 0,
3028
+ ) || undefined,
3029
+ last_reply_to_message_id: intFromRawAllowZero(
3030
+ entry.last_reply_to_message_id || entry.lastReplyToMessageID,
3031
+ 0,
3032
+ ) || undefined,
3033
+ last_reply_message_envelope: normalizeRunnerTelegramMessageEnvelope(
3034
+ entry.last_reply_message_envelope
3035
+ || entry.lastReplyMessageEnvelope
3036
+ || {},
3037
+ ),
3038
+ attempted_delivery_envelope: normalizeRunnerTelegramMessageEnvelope(
3039
+ entry.attempted_delivery_envelope
3040
+ || entry.attemptedDeliveryEnvelope
3041
+ || {},
3042
+ ),
3043
+ updated_at: updatedAt || new Date(nowMs).toISOString(),
3044
+ };
3045
+ }
3046
+ return normalized;
3047
+ }
3054
3048
 
3055
3049
  function normalizeBotRunnerConsumedComments(rawConsumed, nowMs = Date.now()) {
3056
3050
  const normalized = {};
@@ -3257,40 +3251,40 @@ function uniqueOrderedStrings(values, normalizer = (value) => String(value || ""
3257
3251
  return output;
3258
3252
  }
3259
3253
 
3260
- function buildRunnerRequestKey({
3261
- normalizedRoute,
3262
- selectedRecord,
3263
- selectedBotUsernames = [],
3264
- normalizedIntent = "",
3265
- conversationID = "",
3266
- }) {
3267
- const parsed = safeObject(selectedRecord?.parsedArchive);
3268
- const chatID = String(parsed.chatID || parsed.chatId || "").trim() || "-";
3269
- const messageID = intFromRawAllowZero(parsed.messageID, 0);
3270
- const kind = String(parsed.kind || "").trim().toLowerCase() || "-";
3271
- const responders = uniqueOrderedStrings(selectedBotUsernames, normalizeTelegramMentionUsername).join(",") || "-";
3272
- const intent = String(normalizedIntent || "").trim().toLowerCase() || "-";
3273
- const resolvedConversationID = String(conversationID || parsed.conversationID || "").trim() || "-";
3274
- return [
3275
- String(normalizedRoute?.projectID || "").trim() || "-",
3276
- String(normalizedRoute?.provider || "").trim() || "-",
3277
- chatID,
3278
- String(messageID || "-"),
3279
- kind,
3280
- responders,
3281
- intent,
3282
- resolvedConversationID,
3283
- ].join("::");
3284
- }
3285
-
3286
- function resolveRunnerRequestClaimIntent({
3287
- normalizedIntent = "",
3288
- selectedRecord,
3289
- }) {
3290
- void selectedRecord;
3291
- const explicitIntent = String(normalizedIntent || "").trim().toLowerCase();
3292
- return explicitIntent;
3293
- }
3254
+ function buildRunnerRequestKey({
3255
+ normalizedRoute,
3256
+ selectedRecord,
3257
+ selectedBotUsernames = [],
3258
+ normalizedIntent = "",
3259
+ conversationID = "",
3260
+ }) {
3261
+ const parsed = safeObject(selectedRecord?.parsedArchive);
3262
+ const chatID = String(parsed.chatID || parsed.chatId || "").trim() || "-";
3263
+ const messageID = intFromRawAllowZero(parsed.messageID, 0);
3264
+ const kind = String(parsed.kind || "").trim().toLowerCase() || "-";
3265
+ const responders = uniqueOrderedStrings(selectedBotUsernames, normalizeTelegramMentionUsername).join(",") || "-";
3266
+ const intent = String(normalizedIntent || "").trim().toLowerCase() || "-";
3267
+ const resolvedConversationID = String(conversationID || parsed.conversationID || "").trim() || "-";
3268
+ return [
3269
+ String(normalizedRoute?.projectID || "").trim() || "-",
3270
+ String(normalizedRoute?.provider || "").trim() || "-",
3271
+ chatID,
3272
+ String(messageID || "-"),
3273
+ kind,
3274
+ responders,
3275
+ intent,
3276
+ resolvedConversationID,
3277
+ ].join("::");
3278
+ }
3279
+
3280
+ function resolveRunnerRequestClaimIntent({
3281
+ normalizedIntent = "",
3282
+ selectedRecord,
3283
+ }) {
3284
+ void selectedRecord;
3285
+ const explicitIntent = String(normalizedIntent || "").trim().toLowerCase();
3286
+ return explicitIntent;
3287
+ }
3294
3288
 
3295
3289
  function buildSyntheticReplyChainConversationID(normalizedRoute, chatID, anchorMessageID) {
3296
3290
  const provider = String(normalizedRoute?.provider || "").trim() || "unknown";
@@ -3595,9 +3589,9 @@ function runnerRequestPreferredExecutionContractTargets(entryRaw) {
3595
3589
  );
3596
3590
  }
3597
3591
 
3598
- function runnerRequestPreferredNextExpectedResponders(entryRaw) {
3599
- const entry = safeObject(entryRaw);
3600
- return uniqueOrderedStrings(
3592
+ function runnerRequestPreferredNextExpectedResponders(entryRaw) {
3593
+ const entry = safeObject(entryRaw);
3594
+ return uniqueOrderedStrings(
3601
3595
  ensureArray(entry.next_expected_responders).length
3602
3596
  ? entry.next_expected_responders
3603
3597
  : ensureArray(entry.followup_next_expected_responders).length
@@ -3605,40 +3599,40 @@ function runnerRequestPreferredNextExpectedResponders(entryRaw) {
3605
3599
  : ensureArray(entry.root_next_expected_responders).length
3606
3600
  ? entry.root_next_expected_responders
3607
3601
  : [],
3608
- normalizeTelegramMentionUsername,
3609
- );
3610
- }
3611
-
3612
- function runnerRequestPreferredAuthoritySelectedBotUsernames(entryRaw) {
3613
- const entry = safeObject(entryRaw);
3614
- const normalizedSummaryBot = normalizeTelegramMentionUsername(entry.conversation_summary_bot);
3615
- return uniqueOrderedStrings(
3616
- runnerRequestPreferredNextExpectedResponders(entry).length
3617
- ? runnerRequestPreferredNextExpectedResponders(entry)
3618
- : runnerRequestPreferredExecutionContractTargets(entry).length
3619
- ? runnerRequestPreferredExecutionContractTargets(entry)
3620
- : ensureArray(entry.selected_bot_usernames).length
3621
- ? entry.selected_bot_usernames
3622
- : ensureArray(entry.conversation_initial_responders).length
3623
- ? entry.conversation_initial_responders
3624
- : normalizedSummaryBot
3625
- ? [normalizedSummaryBot]
3626
- : [],
3627
- normalizeTelegramMentionUsername,
3628
- );
3629
- }
3630
-
3631
- function extractRunnerExplicitSelectedBotUsernamesFromParsed(parsedArchiveRaw) {
3632
- const parsedArchive = safeObject(parsedArchiveRaw);
3633
- return uniqueOrderedStrings(
3634
- ensureArray(parsedArchive.mentionUsernames).length
3635
- ? parsedArchive.mentionUsernames
3636
- : String(parsedArchive.replyToUsername || "").trim()
3637
- ? [parsedArchive.replyToUsername]
3638
- : [],
3639
- normalizeTelegramMentionUsername,
3640
- );
3641
- }
3602
+ normalizeTelegramMentionUsername,
3603
+ );
3604
+ }
3605
+
3606
+ function runnerRequestPreferredAuthoritySelectedBotUsernames(entryRaw) {
3607
+ const entry = safeObject(entryRaw);
3608
+ const normalizedSummaryBot = normalizeTelegramMentionUsername(entry.conversation_summary_bot);
3609
+ return uniqueOrderedStrings(
3610
+ runnerRequestPreferredNextExpectedResponders(entry).length
3611
+ ? runnerRequestPreferredNextExpectedResponders(entry)
3612
+ : runnerRequestPreferredExecutionContractTargets(entry).length
3613
+ ? runnerRequestPreferredExecutionContractTargets(entry)
3614
+ : ensureArray(entry.selected_bot_usernames).length
3615
+ ? entry.selected_bot_usernames
3616
+ : ensureArray(entry.conversation_initial_responders).length
3617
+ ? entry.conversation_initial_responders
3618
+ : normalizedSummaryBot
3619
+ ? [normalizedSummaryBot]
3620
+ : [],
3621
+ normalizeTelegramMentionUsername,
3622
+ );
3623
+ }
3624
+
3625
+ function extractRunnerExplicitSelectedBotUsernamesFromParsed(parsedArchiveRaw) {
3626
+ const parsedArchive = safeObject(parsedArchiveRaw);
3627
+ return uniqueOrderedStrings(
3628
+ ensureArray(parsedArchive.mentionUsernames).length
3629
+ ? parsedArchive.mentionUsernames
3630
+ : String(parsedArchive.replyToUsername || "").trim()
3631
+ ? [parsedArchive.replyToUsername]
3632
+ : [],
3633
+ normalizeTelegramMentionUsername,
3634
+ );
3635
+ }
3642
3636
 
3643
3637
  function runnerRequestPreferredAIReplyPreview(entryRaw) {
3644
3638
  const entry = safeObject(entryRaw);
@@ -3750,26 +3744,26 @@ function runnerRequestPreferredArchiveError(entryRaw) {
3750
3744
  ).trim();
3751
3745
  }
3752
3746
 
3753
- function buildRunnerValidationAndDeliverySummary({
3754
- aiReplyPreview = "",
3755
- executionContractType = "",
3756
- executionContractTargets = [],
3757
- nextExpectedResponders = [],
3747
+ function buildRunnerValidationAndDeliverySummary({
3748
+ aiReplyPreview = "",
3749
+ executionContractType = "",
3750
+ executionContractTargets = [],
3751
+ nextExpectedResponders = [],
3758
3752
  responseContractValidationStatus = "",
3759
3753
  responseContractValidationReason = "",
3760
3754
  responseContractValidationTargets = [],
3761
3755
  assignmentValidationStatus = "",
3762
3756
  assignmentValidationReason = "",
3763
3757
  assignmentValidationModes = [],
3764
- deliveryStatus = "",
3765
- archiveStatus = "",
3766
- transportError = "",
3767
- archiveError = "",
3768
- sourceMessageEnvelope = {},
3769
- lastReplyMessageEnvelope = {},
3770
- attemptedDeliveryEnvelope = {},
3771
- } = {}) {
3772
- return {
3758
+ deliveryStatus = "",
3759
+ archiveStatus = "",
3760
+ transportError = "",
3761
+ archiveError = "",
3762
+ sourceMessageEnvelope = {},
3763
+ lastReplyMessageEnvelope = {},
3764
+ attemptedDeliveryEnvelope = {},
3765
+ } = {}) {
3766
+ return {
3773
3767
  ai_reply_preview: String(aiReplyPreview || "").trim(),
3774
3768
  execution_contract_type: String(executionContractType || "").trim().toLowerCase(),
3775
3769
  execution_contract_targets: uniqueOrderedStrings(
@@ -3792,15 +3786,15 @@ function buildRunnerValidationAndDeliverySummary({
3792
3786
  ensureArray(assignmentValidationModes),
3793
3787
  (value) => String(value || "").trim().toLowerCase(),
3794
3788
  ),
3795
- delivery_status: String(deliveryStatus || "").trim().toLowerCase(),
3796
- archive_status: String(archiveStatus || "").trim().toLowerCase(),
3797
- transport_error: String(transportError || "").trim(),
3798
- archive_error: String(archiveError || "").trim(),
3799
- source_message_envelope: safeObject(sourceMessageEnvelope),
3800
- last_reply_message_envelope: safeObject(lastReplyMessageEnvelope),
3801
- attempted_delivery_envelope: safeObject(attemptedDeliveryEnvelope),
3802
- };
3803
- }
3789
+ delivery_status: String(deliveryStatus || "").trim().toLowerCase(),
3790
+ archive_status: String(archiveStatus || "").trim().toLowerCase(),
3791
+ transport_error: String(transportError || "").trim(),
3792
+ archive_error: String(archiveError || "").trim(),
3793
+ source_message_envelope: safeObject(sourceMessageEnvelope),
3794
+ last_reply_message_envelope: safeObject(lastReplyMessageEnvelope),
3795
+ attempted_delivery_envelope: safeObject(attemptedDeliveryEnvelope),
3796
+ };
3797
+ }
3804
3798
 
3805
3799
  function buildRunnerRequestRootWorkItemSummary(entryRaw) {
3806
3800
  const entry = safeObject(entryRaw);
@@ -3816,8 +3810,8 @@ function buildRunnerRequestRootWorkItemSummary(entryRaw) {
3816
3810
  };
3817
3811
  }
3818
3812
 
3819
- function buildRunnerRouteLastResultSummary(routeStateRaw) {
3820
- const routeState = safeObject(routeStateRaw);
3813
+ function buildRunnerRouteLastResultSummary(routeStateRaw) {
3814
+ const routeState = safeObject(routeStateRaw);
3821
3815
  const executionContractType = String(
3822
3816
  routeState.last_followup_execution_contract_type
3823
3817
  || routeState.last_normalized_execution_contract_type
@@ -3865,18 +3859,18 @@ function buildRunnerRouteLastResultSummary(routeStateRaw) {
3865
3859
  ensureArray(routeState.last_followup_assignment_validation_modes),
3866
3860
  (value) => String(value || "").trim().toLowerCase(),
3867
3861
  );
3868
- return {
3869
- action: String(routeState.last_action || "").trim(),
3870
- reason: String(routeState.last_reason || "").trim(),
3871
- visibility_status: String(routeState.last_visibility_status || "").trim(),
3872
- visibility_reason: String(routeState.last_visibility_reason || "").trim(),
3873
- visibility_source: String(routeState.last_visibility_source || "").trim(),
3874
- candidate_bot_usernames: uniqueOrderedStrings(
3875
- ensureArray(routeState.last_candidate_bot_usernames),
3876
- normalizeTelegramMentionUsername,
3877
- ),
3878
- intent_type: String(routeState.last_intent_type || "").trim(),
3879
- ...buildRunnerValidationAndDeliverySummary({
3862
+ return {
3863
+ action: String(routeState.last_action || "").trim(),
3864
+ reason: String(routeState.last_reason || "").trim(),
3865
+ visibility_status: String(routeState.last_visibility_status || "").trim(),
3866
+ visibility_reason: String(routeState.last_visibility_reason || "").trim(),
3867
+ visibility_source: String(routeState.last_visibility_source || "").trim(),
3868
+ candidate_bot_usernames: uniqueOrderedStrings(
3869
+ ensureArray(routeState.last_candidate_bot_usernames),
3870
+ normalizeTelegramMentionUsername,
3871
+ ),
3872
+ intent_type: String(routeState.last_intent_type || "").trim(),
3873
+ ...buildRunnerValidationAndDeliverySummary({
3880
3874
  aiReplyPreview: routeState.last_followup_ai_reply_preview,
3881
3875
  executionContractType,
3882
3876
  executionContractTargets,
@@ -3886,15 +3880,15 @@ function buildRunnerRouteLastResultSummary(routeStateRaw) {
3886
3880
  responseContractValidationTargets,
3887
3881
  assignmentValidationStatus,
3888
3882
  assignmentValidationReason,
3889
- assignmentValidationModes,
3890
- deliveryStatus: routeState.last_followup_delivery_status,
3891
- archiveStatus: routeState.last_followup_archive_status,
3892
- transportError: routeState.last_followup_transport_error,
3893
- archiveError: routeState.last_followup_archive_error,
3894
- sourceMessageEnvelope: routeState.last_followup_source_message_envelope,
3895
- lastReplyMessageEnvelope: routeState.last_followup_last_reply_message_envelope,
3896
- attemptedDeliveryEnvelope: routeState.last_followup_attempted_delivery_envelope,
3897
- }),
3883
+ assignmentValidationModes,
3884
+ deliveryStatus: routeState.last_followup_delivery_status,
3885
+ archiveStatus: routeState.last_followup_archive_status,
3886
+ transportError: routeState.last_followup_transport_error,
3887
+ archiveError: routeState.last_followup_archive_error,
3888
+ sourceMessageEnvelope: routeState.last_followup_source_message_envelope,
3889
+ lastReplyMessageEnvelope: routeState.last_followup_last_reply_message_envelope,
3890
+ attemptedDeliveryEnvelope: routeState.last_followup_attempted_delivery_envelope,
3891
+ }),
3898
3892
  workspace_dir: String(routeState.last_workspace_dir || "").trim(),
3899
3893
  artifact_validation: String(routeState.last_artifact_validation || "").trim(),
3900
3894
  artifact_paths: ensureArray(routeState.last_artifact_paths).map((item) => String(item || "").trim()).filter(Boolean),
@@ -4058,10 +4052,10 @@ async function loadRunnerArchiveThreadMessageIndex({
4058
4052
  }
4059
4053
  }
4060
4054
 
4061
- async function findRunnerArchiveThreadMessageByID({
4062
- runtime,
4063
- threadID,
4064
- messageID,
4055
+ async function findRunnerArchiveThreadMessageByID({
4056
+ runtime,
4057
+ threadID,
4058
+ messageID,
4065
4059
  cache = null,
4066
4060
  }) {
4067
4061
  const nextCache = await loadRunnerArchiveThreadMessageIndex({
@@ -4069,244 +4063,244 @@ async function findRunnerArchiveThreadMessageByID({
4069
4063
  threadID,
4070
4064
  cache,
4071
4065
  });
4072
- return {
4073
- cache: nextCache,
4074
- record: safeObject(nextCache.messageIndex.get(intFromRawAllowZero(messageID, 0))),
4075
- };
4076
- }
4077
-
4078
- function buildRunnerReplyChainSnapshotFromParsedArchive(parsedArchiveRaw, overrides = {}) {
4079
- const parsedArchive = safeObject(parsedArchiveRaw);
4080
- return buildRunnerReplyChainSnapshotFromMessageEnvelope(
4081
- buildRunnerTelegramMessageEnvelopeFromParsedArchive(parsedArchive, overrides),
4082
- {
4083
- speaker_type: firstNonEmptyString([
4084
- overrides.speaker_type,
4085
- buildRunnerReplyChainSpeakerType(parsedArchive),
4086
- ]),
4087
- speaker_label: firstNonEmptyString([
4088
- overrides.speaker_label,
4089
- buildRunnerReplyChainSpeakerLabel(parsedArchive),
4090
- ]),
4091
- body: truncateRunnerReplyChainText(
4092
- firstNonEmptyString([overrides.body, parsedArchive.body]),
4093
- ),
4094
- },
4095
- );
4096
- }
4097
-
4098
- function buildRunnerReplyChainSnapshotFromArchiveRecord(recordRaw) {
4099
- const record = safeObject(recordRaw);
4100
- const parsedArchive = safeObject(record.parsedArchive || parseArchivedChatComment(record.body));
4101
- return buildRunnerReplyChainSnapshotFromParsedArchive(parsedArchive);
4102
- }
4103
-
4104
- function buildRunnerReplyChainSnapshotFromRequestSource(requestRaw) {
4105
- const request = safeObject(requestRaw);
4106
- const explicitEnvelope = normalizeRunnerTelegramMessageEnvelope(
4107
- request.source_message_envelope || request.sourceMessageEnvelope,
4108
- );
4109
- const fallbackEnvelope = intFromRawAllowZero(request.source_message_id, 0) > 0
4110
- ? {
4111
- chat_id: request.chat_id,
4112
- message_id: request.source_message_id,
4113
- message_thread_id: request.source_message_thread_id,
4114
- body: request.source_message_body,
4115
- sender: "human",
4116
- sender_is_bot: false,
4117
- }
4118
- : null;
4119
- return buildRunnerReplyChainSnapshotFromMessageEnvelope(
4120
- explicitEnvelope && Object.keys(explicitEnvelope).length
4121
- ? explicitEnvelope
4122
- : fallbackEnvelope,
4123
- {
4124
- speaker_type: "human",
4125
- speaker_label: "human",
4126
- },
4127
- );
4128
- }
4129
-
4130
- function buildRunnerReplyChainSnapshotFromRequestReply(requestRaw) {
4131
- const request = safeObject(requestRaw);
4132
- const preview = runnerRequestPreferredAIReplyPreview(request);
4133
- if (!preview) {
4134
- return null;
4135
- }
4136
- const explicitEnvelope = normalizeRunnerTelegramMessageEnvelope(
4137
- request.last_reply_message_envelope || request.lastReplyMessageEnvelope,
4138
- );
4139
- return buildRunnerReplyChainSnapshotFromMessageEnvelope(
4140
- explicitEnvelope && intFromRawAllowZero(explicitEnvelope.message_id, 0) > 0
4141
- ? explicitEnvelope
4142
- : null,
4143
- {
4144
- speaker_type: "bot",
4145
- speaker_label: firstNonEmptyString([
4146
- request.conversation_summary_bot ? `@${String(request.conversation_summary_bot || "").trim().replace(/^@+/, "")}` : "",
4147
- ensureArray(request.selected_bot_usernames)[0]
4148
- ? `@${String(ensureArray(request.selected_bot_usernames)[0] || "").trim().replace(/^@+/, "")}`
4149
- : "",
4150
- "bot",
4151
- ]),
4152
- body: preview,
4153
- },
4154
- );
4155
- }
4156
-
4157
- function uniqueRunnerReplyChainSnapshots(snapshots, limit = 4) {
4158
- const normalized = [];
4159
- const seen = new Set();
4160
- for (const snapshotRaw of ensureArray(snapshots)) {
4161
- const snapshot = normalizeRunnerReplyChainSnapshot(snapshotRaw);
4162
- if (!snapshot) continue;
4163
- const key = snapshot.message_id
4164
- ? `message:${snapshot.message_id}`
4165
- : `body:${snapshot.speaker_type}:${String(snapshot.body || "").trim()}`;
4166
- if (seen.has(key)) continue;
4167
- seen.add(key);
4168
- normalized.push(snapshot);
4169
- }
4170
- return normalized.slice(-(Math.max(1, intFromRawAllowZero(limit, 4) || 4)));
4171
- }
4172
-
4173
- async function buildRunnerAuthorityReplyChainContext({
4174
- selectedRecord,
4175
- replyChainContext = null,
4176
- authoritySource = null,
4177
- runtime,
4178
- archiveThreadID = "",
4179
- }) {
4180
- const parsed = safeObject(selectedRecord?.parsedArchive);
4181
- const normalizedReplyChainContext = safeObject(replyChainContext);
4182
- const referencedRequest = safeObject(
4183
- authoritySource?.request_key
4184
- ? authoritySource
4185
- : normalizedReplyChainContext.referencedRequest,
4186
- );
4187
- const normalizedArchiveThreadID = firstNonEmptyString([
4188
- archiveThreadID,
4189
- selectedRecord?.thread_id,
4190
- selectedRecord?.threadID,
4191
- selectedRecord?.threadId,
4192
- ]);
4193
- const conversationID = firstNonEmptyString([
4194
- normalizedReplyChainContext.conversationID,
4195
- parsed.conversationID,
4196
- referencedRequest.conversation_id,
4197
- ]);
4198
- const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
4199
- const parentMessageID = intFromRawAllowZero(
4200
- parsed.replyToMessageID || normalizedReplyChainContext.replyToMessageID,
4201
- 0,
4202
- );
4203
- let archiveThreadCache = null;
4204
- const loadArchiveSnapshot = async (messageID) => {
4205
- if (!(messageID > 0) || !normalizedArchiveThreadID || !runtime?.baseURL || !runtime?.token) {
4206
- return null;
4207
- }
4208
- const lookup = await findRunnerArchiveThreadMessageByID({
4209
- runtime,
4210
- threadID: normalizedArchiveThreadID,
4211
- messageID,
4212
- cache: archiveThreadCache,
4213
- });
4214
- archiveThreadCache = lookup.cache;
4215
- return buildRunnerReplyChainSnapshotFromArchiveRecord(lookup.record);
4216
- };
4217
-
4218
- const ancestorSnapshots = [];
4219
- let nextReplyToMessageID = parentMessageID;
4220
- const visitedMessageIDs = new Set();
4221
- for (let hop = 0; hop < 8 && nextReplyToMessageID > 0; hop += 1) {
4222
- if (visitedMessageIDs.has(nextReplyToMessageID)) {
4223
- break;
4224
- }
4225
- visitedMessageIDs.add(nextReplyToMessageID);
4226
- const archivedLookup = await findRunnerArchiveThreadMessageByID({
4227
- runtime,
4228
- threadID: normalizedArchiveThreadID,
4229
- messageID: nextReplyToMessageID,
4230
- cache: archiveThreadCache,
4231
- });
4232
- archiveThreadCache = archivedLookup.cache;
4233
- const archivedRecord = safeObject(archivedLookup.record);
4234
- if (!Object.keys(archivedRecord).length) {
4235
- break;
4236
- }
4237
- const snapshot = buildRunnerReplyChainSnapshotFromArchiveRecord(archivedRecord);
4238
- if (snapshot) {
4239
- ancestorSnapshots.unshift(snapshot);
4240
- }
4241
- const archivedParsed = safeObject(archivedRecord.parsedArchive || parseArchivedChatComment(archivedRecord.body));
4242
- nextReplyToMessageID = intFromRawAllowZero(archivedParsed.replyToMessageID, 0);
4243
- }
4244
-
4245
- let parentMessage = ancestorSnapshots.length
4246
- ? safeObject(ancestorSnapshots[ancestorSnapshots.length - 1])
4247
- : {};
4248
- let rootMessage = safeObject(
4249
- ancestorSnapshots.find((item) => String(item?.speaker_type || "").trim().toLowerCase() === "human"),
4250
- );
4251
- let latestPriorBotReply = safeObject(
4252
- [...ancestorSnapshots].reverse().find((item) => String(item?.speaker_type || "").trim().toLowerCase() === "bot"),
4253
- );
4254
-
4255
- const requestRootMessage = buildRunnerReplyChainSnapshotFromRequestSource(referencedRequest);
4256
- const requestLatestBotReply = buildRunnerReplyChainSnapshotFromRequestReply(referencedRequest);
4257
-
4258
- if (!Object.keys(parentMessage).length && parentMessageID > 0) {
4259
- parentMessage = safeObject(await loadArchiveSnapshot(parentMessageID));
4260
- }
4261
- if (!Object.keys(rootMessage).length) {
4262
- rootMessage = safeObject(requestRootMessage);
4263
- }
4264
- if (
4265
- Object.keys(requestLatestBotReply || {}).length
4266
- && (
4267
- !Object.keys(latestPriorBotReply).length
4268
- || intFromRawAllowZero(requestLatestBotReply.message_id, 0) > intFromRawAllowZero(latestPriorBotReply.message_id, 0)
4269
- )
4270
- ) {
4271
- latestPriorBotReply = safeObject(requestLatestBotReply);
4272
- }
4273
- if (!Object.keys(latestPriorBotReply).length) {
4274
- latestPriorBotReply = safeObject(requestLatestBotReply);
4275
- }
4276
- if (!Object.keys(rootMessage).length && !parentMessageID && currentMessageID > 0) {
4277
- rootMessage = safeObject(buildRunnerReplyChainSnapshotFromParsedArchive(parsed));
4278
- }
4279
-
4280
- const recentChainMessages = uniqueRunnerReplyChainSnapshots(
4281
- [
4282
- ...ancestorSnapshots,
4283
- requestLatestBotReply,
4284
- parentMessage,
4285
- latestPriorBotReply,
4286
- ].filter(Boolean),
4287
- 4,
4288
- );
4289
- const currentTurnRelation = Object.keys(parentMessage).length
4290
- ? "reply_to_parent_message"
4291
- : conversationID
4292
- ? "same_conversation_followup"
4293
- : "standalone_message";
4294
- return normalizeRunnerReplyChainContext({
4295
- conversation_id: conversationID,
4296
- current_message_id: currentMessageID,
4297
- current_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
4298
- parent_message_id: parentMessageID,
4299
- root_message_id: intFromRawAllowZero(rootMessage.message_id, 0) || undefined,
4300
- current_turn_relation: currentTurnRelation,
4301
- root_message: rootMessage,
4302
- parent_message: parentMessage,
4303
- latest_prior_bot_reply: latestPriorBotReply,
4304
- recent_chain_messages: recentChainMessages,
4305
- });
4306
- }
4307
-
4308
- function resolveRunnerReplyChainConversationContext(state, normalizedRoute, selectedRecord) {
4309
- const parsed = safeObject(selectedRecord?.parsedArchive);
4066
+ return {
4067
+ cache: nextCache,
4068
+ record: safeObject(nextCache.messageIndex.get(intFromRawAllowZero(messageID, 0))),
4069
+ };
4070
+ }
4071
+
4072
+ function buildRunnerReplyChainSnapshotFromParsedArchive(parsedArchiveRaw, overrides = {}) {
4073
+ const parsedArchive = safeObject(parsedArchiveRaw);
4074
+ return buildRunnerReplyChainSnapshotFromMessageEnvelope(
4075
+ buildRunnerTelegramMessageEnvelopeFromParsedArchive(parsedArchive, overrides),
4076
+ {
4077
+ speaker_type: firstNonEmptyString([
4078
+ overrides.speaker_type,
4079
+ buildRunnerReplyChainSpeakerType(parsedArchive),
4080
+ ]),
4081
+ speaker_label: firstNonEmptyString([
4082
+ overrides.speaker_label,
4083
+ buildRunnerReplyChainSpeakerLabel(parsedArchive),
4084
+ ]),
4085
+ body: truncateRunnerReplyChainText(
4086
+ firstNonEmptyString([overrides.body, parsedArchive.body]),
4087
+ ),
4088
+ },
4089
+ );
4090
+ }
4091
+
4092
+ function buildRunnerReplyChainSnapshotFromArchiveRecord(recordRaw) {
4093
+ const record = safeObject(recordRaw);
4094
+ const parsedArchive = safeObject(record.parsedArchive || parseArchivedChatComment(record.body));
4095
+ return buildRunnerReplyChainSnapshotFromParsedArchive(parsedArchive);
4096
+ }
4097
+
4098
+ function buildRunnerReplyChainSnapshotFromRequestSource(requestRaw) {
4099
+ const request = safeObject(requestRaw);
4100
+ const explicitEnvelope = normalizeRunnerTelegramMessageEnvelope(
4101
+ request.source_message_envelope || request.sourceMessageEnvelope,
4102
+ );
4103
+ const fallbackEnvelope = intFromRawAllowZero(request.source_message_id, 0) > 0
4104
+ ? {
4105
+ chat_id: request.chat_id,
4106
+ message_id: request.source_message_id,
4107
+ message_thread_id: request.source_message_thread_id,
4108
+ body: request.source_message_body,
4109
+ sender: "human",
4110
+ sender_is_bot: false,
4111
+ }
4112
+ : null;
4113
+ return buildRunnerReplyChainSnapshotFromMessageEnvelope(
4114
+ explicitEnvelope && Object.keys(explicitEnvelope).length
4115
+ ? explicitEnvelope
4116
+ : fallbackEnvelope,
4117
+ {
4118
+ speaker_type: "human",
4119
+ speaker_label: "human",
4120
+ },
4121
+ );
4122
+ }
4123
+
4124
+ function buildRunnerReplyChainSnapshotFromRequestReply(requestRaw) {
4125
+ const request = safeObject(requestRaw);
4126
+ const preview = runnerRequestPreferredAIReplyPreview(request);
4127
+ if (!preview) {
4128
+ return null;
4129
+ }
4130
+ const explicitEnvelope = normalizeRunnerTelegramMessageEnvelope(
4131
+ request.last_reply_message_envelope || request.lastReplyMessageEnvelope,
4132
+ );
4133
+ return buildRunnerReplyChainSnapshotFromMessageEnvelope(
4134
+ explicitEnvelope && intFromRawAllowZero(explicitEnvelope.message_id, 0) > 0
4135
+ ? explicitEnvelope
4136
+ : null,
4137
+ {
4138
+ speaker_type: "bot",
4139
+ speaker_label: firstNonEmptyString([
4140
+ request.conversation_summary_bot ? `@${String(request.conversation_summary_bot || "").trim().replace(/^@+/, "")}` : "",
4141
+ ensureArray(request.selected_bot_usernames)[0]
4142
+ ? `@${String(ensureArray(request.selected_bot_usernames)[0] || "").trim().replace(/^@+/, "")}`
4143
+ : "",
4144
+ "bot",
4145
+ ]),
4146
+ body: preview,
4147
+ },
4148
+ );
4149
+ }
4150
+
4151
+ function uniqueRunnerReplyChainSnapshots(snapshots, limit = 4) {
4152
+ const normalized = [];
4153
+ const seen = new Set();
4154
+ for (const snapshotRaw of ensureArray(snapshots)) {
4155
+ const snapshot = normalizeRunnerReplyChainSnapshot(snapshotRaw);
4156
+ if (!snapshot) continue;
4157
+ const key = snapshot.message_id
4158
+ ? `message:${snapshot.message_id}`
4159
+ : `body:${snapshot.speaker_type}:${String(snapshot.body || "").trim()}`;
4160
+ if (seen.has(key)) continue;
4161
+ seen.add(key);
4162
+ normalized.push(snapshot);
4163
+ }
4164
+ return normalized.slice(-(Math.max(1, intFromRawAllowZero(limit, 4) || 4)));
4165
+ }
4166
+
4167
+ async function buildRunnerAuthorityReplyChainContext({
4168
+ selectedRecord,
4169
+ replyChainContext = null,
4170
+ authoritySource = null,
4171
+ runtime,
4172
+ archiveThreadID = "",
4173
+ }) {
4174
+ const parsed = safeObject(selectedRecord?.parsedArchive);
4175
+ const normalizedReplyChainContext = safeObject(replyChainContext);
4176
+ const referencedRequest = safeObject(
4177
+ authoritySource?.request_key
4178
+ ? authoritySource
4179
+ : normalizedReplyChainContext.referencedRequest,
4180
+ );
4181
+ const normalizedArchiveThreadID = firstNonEmptyString([
4182
+ archiveThreadID,
4183
+ selectedRecord?.thread_id,
4184
+ selectedRecord?.threadID,
4185
+ selectedRecord?.threadId,
4186
+ ]);
4187
+ const conversationID = firstNonEmptyString([
4188
+ normalizedReplyChainContext.conversationID,
4189
+ parsed.conversationID,
4190
+ referencedRequest.conversation_id,
4191
+ ]);
4192
+ const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
4193
+ const parentMessageID = intFromRawAllowZero(
4194
+ parsed.replyToMessageID || normalizedReplyChainContext.replyToMessageID,
4195
+ 0,
4196
+ );
4197
+ let archiveThreadCache = null;
4198
+ const loadArchiveSnapshot = async (messageID) => {
4199
+ if (!(messageID > 0) || !normalizedArchiveThreadID || !runtime?.baseURL || !runtime?.token) {
4200
+ return null;
4201
+ }
4202
+ const lookup = await findRunnerArchiveThreadMessageByID({
4203
+ runtime,
4204
+ threadID: normalizedArchiveThreadID,
4205
+ messageID,
4206
+ cache: archiveThreadCache,
4207
+ });
4208
+ archiveThreadCache = lookup.cache;
4209
+ return buildRunnerReplyChainSnapshotFromArchiveRecord(lookup.record);
4210
+ };
4211
+
4212
+ const ancestorSnapshots = [];
4213
+ let nextReplyToMessageID = parentMessageID;
4214
+ const visitedMessageIDs = new Set();
4215
+ for (let hop = 0; hop < 8 && nextReplyToMessageID > 0; hop += 1) {
4216
+ if (visitedMessageIDs.has(nextReplyToMessageID)) {
4217
+ break;
4218
+ }
4219
+ visitedMessageIDs.add(nextReplyToMessageID);
4220
+ const archivedLookup = await findRunnerArchiveThreadMessageByID({
4221
+ runtime,
4222
+ threadID: normalizedArchiveThreadID,
4223
+ messageID: nextReplyToMessageID,
4224
+ cache: archiveThreadCache,
4225
+ });
4226
+ archiveThreadCache = archivedLookup.cache;
4227
+ const archivedRecord = safeObject(archivedLookup.record);
4228
+ if (!Object.keys(archivedRecord).length) {
4229
+ break;
4230
+ }
4231
+ const snapshot = buildRunnerReplyChainSnapshotFromArchiveRecord(archivedRecord);
4232
+ if (snapshot) {
4233
+ ancestorSnapshots.unshift(snapshot);
4234
+ }
4235
+ const archivedParsed = safeObject(archivedRecord.parsedArchive || parseArchivedChatComment(archivedRecord.body));
4236
+ nextReplyToMessageID = intFromRawAllowZero(archivedParsed.replyToMessageID, 0);
4237
+ }
4238
+
4239
+ let parentMessage = ancestorSnapshots.length
4240
+ ? safeObject(ancestorSnapshots[ancestorSnapshots.length - 1])
4241
+ : {};
4242
+ let rootMessage = safeObject(
4243
+ ancestorSnapshots.find((item) => String(item?.speaker_type || "").trim().toLowerCase() === "human"),
4244
+ );
4245
+ let latestPriorBotReply = safeObject(
4246
+ [...ancestorSnapshots].reverse().find((item) => String(item?.speaker_type || "").trim().toLowerCase() === "bot"),
4247
+ );
4248
+
4249
+ const requestRootMessage = buildRunnerReplyChainSnapshotFromRequestSource(referencedRequest);
4250
+ const requestLatestBotReply = buildRunnerReplyChainSnapshotFromRequestReply(referencedRequest);
4251
+
4252
+ if (!Object.keys(parentMessage).length && parentMessageID > 0) {
4253
+ parentMessage = safeObject(await loadArchiveSnapshot(parentMessageID));
4254
+ }
4255
+ if (!Object.keys(rootMessage).length) {
4256
+ rootMessage = safeObject(requestRootMessage);
4257
+ }
4258
+ if (
4259
+ Object.keys(requestLatestBotReply || {}).length
4260
+ && (
4261
+ !Object.keys(latestPriorBotReply).length
4262
+ || intFromRawAllowZero(requestLatestBotReply.message_id, 0) > intFromRawAllowZero(latestPriorBotReply.message_id, 0)
4263
+ )
4264
+ ) {
4265
+ latestPriorBotReply = safeObject(requestLatestBotReply);
4266
+ }
4267
+ if (!Object.keys(latestPriorBotReply).length) {
4268
+ latestPriorBotReply = safeObject(requestLatestBotReply);
4269
+ }
4270
+ if (!Object.keys(rootMessage).length && !parentMessageID && currentMessageID > 0) {
4271
+ rootMessage = safeObject(buildRunnerReplyChainSnapshotFromParsedArchive(parsed));
4272
+ }
4273
+
4274
+ const recentChainMessages = uniqueRunnerReplyChainSnapshots(
4275
+ [
4276
+ ...ancestorSnapshots,
4277
+ requestLatestBotReply,
4278
+ parentMessage,
4279
+ latestPriorBotReply,
4280
+ ].filter(Boolean),
4281
+ 4,
4282
+ );
4283
+ const currentTurnRelation = Object.keys(parentMessage).length
4284
+ ? "reply_to_parent_message"
4285
+ : conversationID
4286
+ ? "same_conversation_followup"
4287
+ : "standalone_message";
4288
+ return normalizeRunnerReplyChainContext({
4289
+ conversation_id: conversationID,
4290
+ current_message_id: currentMessageID,
4291
+ current_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
4292
+ parent_message_id: parentMessageID,
4293
+ root_message_id: intFromRawAllowZero(rootMessage.message_id, 0) || undefined,
4294
+ current_turn_relation: currentTurnRelation,
4295
+ root_message: rootMessage,
4296
+ parent_message: parentMessage,
4297
+ latest_prior_bot_reply: latestPriorBotReply,
4298
+ recent_chain_messages: recentChainMessages,
4299
+ });
4300
+ }
4301
+
4302
+ function resolveRunnerReplyChainConversationContext(state, normalizedRoute, selectedRecord) {
4303
+ const parsed = safeObject(selectedRecord?.parsedArchive);
4310
4304
  const explicitConversationID = String(parsed.conversationID || "").trim();
4311
4305
  if (explicitConversationID) {
4312
4306
  return {
@@ -4534,9 +4528,9 @@ function upsertRunnerRequest(state, requestKey, patch = {}) {
4534
4528
  };
4535
4529
  }
4536
4530
 
4537
- function upsertRunnerConsumedComment(state, commentIDRaw, patch = {}) {
4538
- const commentID = String(commentIDRaw || "").trim();
4539
- const currentState = safeObject(state);
4531
+ function upsertRunnerConsumedComment(state, commentIDRaw, patch = {}) {
4532
+ const commentID = String(commentIDRaw || "").trim();
4533
+ const currentState = safeObject(state);
4540
4534
  const consumedComments = normalizeBotRunnerConsumedComments(currentState.consumedComments || currentState.consumed_comments);
4541
4535
  const ledgerKey = buildRunnerConsumedCommentLedgerKey(commentID, patch.route_key, patch.comment_kind);
4542
4536
  const existing = safeObject(consumedComments[ledgerKey]);
@@ -4550,85 +4544,85 @@ function upsertRunnerConsumedComment(state, commentIDRaw, patch = {}) {
4550
4544
  request_status: normalizeRunnerRequestStatus(patch.request_status || existing.request_status),
4551
4545
  };
4552
4546
  consumedComments[ledgerKey] = nextEntry;
4553
- return {
4554
- consumedComments,
4555
- consumedComment: nextEntry,
4556
- };
4557
- }
4558
-
4559
- function resolveRunnerHumanCommentAuthorityContext({
4560
- normalizedRoute,
4561
- selectedRecord,
4562
- replyChainContext = null,
4563
- referencedRequest = null,
4564
- sharedConversationSource = null,
4565
- selectedBotUsernames = [],
4566
- normalizedSharedHumanIntent = null,
4567
- resolvedNormalizedIntent = "",
4568
- }) {
4569
- const parsed = safeObject(selectedRecord?.parsedArchive);
4570
- const explicitCurrentSelectedBotUsernames = extractRunnerExplicitSelectedBotUsernamesFromParsed(parsed);
4571
- const replyChain = safeObject(replyChainContext);
4572
- const referenced = safeObject(referencedRequest);
4573
- const sharedSource = safeObject(sharedConversationSource);
4574
- const sharedIntent = safeObject(normalizedSharedHumanIntent);
4575
- const authoritySource = Object.keys(referenced).length
4576
- ? referenced
4577
- : Object.keys(sharedSource).length
4578
- ? sharedSource
4579
- : {};
4580
- const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
4581
- const resolvedConversationID = firstNonEmptyString([
4582
- parsed.conversationID,
4583
- replyChain.conversationID,
4584
- authoritySource.conversation_id,
4585
- Object.keys(sharedIntent).length > 0
4586
- ? buildSyntheticHumanOpeningConversationID(
4587
- normalizedRoute,
4588
- String(parsed.chatID || parsed.chatId || "").trim(),
4589
- currentMessageID,
4590
- )
4591
- : "",
4592
- ]);
4593
- const explicitSelectedBotUsernames = uniqueOrderedStrings(
4594
- selectedBotUsernames,
4595
- normalizeTelegramMentionUsername,
4596
- );
4597
- const authoritySelectedBotUsernames = uniqueOrderedStrings(
4598
- explicitCurrentSelectedBotUsernames.length > 0
4599
- ? explicitCurrentSelectedBotUsernames
4600
- : explicitSelectedBotUsernames.length > 0
4601
- ? explicitSelectedBotUsernames
4602
- : resolvedConversationID && runnerRequestPreferredAuthoritySelectedBotUsernames(authoritySource).length
4603
- ? runnerRequestPreferredAuthoritySelectedBotUsernames(authoritySource)
4604
- : [],
4605
- normalizeTelegramMentionUsername,
4606
- );
4607
- const preferredNormalizedIntent = String(
4608
- sharedIntent.intentType
4609
- || resolvedNormalizedIntent
4610
- || authoritySource.normalized_intent
4611
- || "",
4612
- ).trim().toLowerCase();
4613
- return {
4614
- authoritySource,
4615
- conversationID: String(resolvedConversationID || "").trim(),
4616
- selectedBotUsernames: authoritySelectedBotUsernames,
4617
- normalizedIntent: preferredNormalizedIntent,
4618
- };
4619
- }
4620
-
4621
- async function claimRunnerRequestForHumanComment({
4622
- normalizedRoute,
4623
- routeKey,
4624
- selectedRecord,
4625
- selectedBotUsernames = [],
4626
- normalizedIntent = "",
4627
- sharedHumanIntent = null,
4628
- runtime = null,
4629
- archiveThreadID = "",
4630
- authoritativeSourceMessageEnvelope = {},
4631
- }) {
4547
+ return {
4548
+ consumedComments,
4549
+ consumedComment: nextEntry,
4550
+ };
4551
+ }
4552
+
4553
+ function resolveRunnerHumanCommentAuthorityContext({
4554
+ normalizedRoute,
4555
+ selectedRecord,
4556
+ replyChainContext = null,
4557
+ referencedRequest = null,
4558
+ sharedConversationSource = null,
4559
+ selectedBotUsernames = [],
4560
+ normalizedSharedHumanIntent = null,
4561
+ resolvedNormalizedIntent = "",
4562
+ }) {
4563
+ const parsed = safeObject(selectedRecord?.parsedArchive);
4564
+ const explicitCurrentSelectedBotUsernames = extractRunnerExplicitSelectedBotUsernamesFromParsed(parsed);
4565
+ const replyChain = safeObject(replyChainContext);
4566
+ const referenced = safeObject(referencedRequest);
4567
+ const sharedSource = safeObject(sharedConversationSource);
4568
+ const sharedIntent = safeObject(normalizedSharedHumanIntent);
4569
+ const authoritySource = Object.keys(referenced).length
4570
+ ? referenced
4571
+ : Object.keys(sharedSource).length
4572
+ ? sharedSource
4573
+ : {};
4574
+ const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
4575
+ const resolvedConversationID = firstNonEmptyString([
4576
+ parsed.conversationID,
4577
+ replyChain.conversationID,
4578
+ authoritySource.conversation_id,
4579
+ Object.keys(sharedIntent).length > 0
4580
+ ? buildSyntheticHumanOpeningConversationID(
4581
+ normalizedRoute,
4582
+ String(parsed.chatID || parsed.chatId || "").trim(),
4583
+ currentMessageID,
4584
+ )
4585
+ : "",
4586
+ ]);
4587
+ const explicitSelectedBotUsernames = uniqueOrderedStrings(
4588
+ selectedBotUsernames,
4589
+ normalizeTelegramMentionUsername,
4590
+ );
4591
+ const authoritySelectedBotUsernames = uniqueOrderedStrings(
4592
+ explicitCurrentSelectedBotUsernames.length > 0
4593
+ ? explicitCurrentSelectedBotUsernames
4594
+ : explicitSelectedBotUsernames.length > 0
4595
+ ? explicitSelectedBotUsernames
4596
+ : resolvedConversationID && runnerRequestPreferredAuthoritySelectedBotUsernames(authoritySource).length
4597
+ ? runnerRequestPreferredAuthoritySelectedBotUsernames(authoritySource)
4598
+ : [],
4599
+ normalizeTelegramMentionUsername,
4600
+ );
4601
+ const preferredNormalizedIntent = String(
4602
+ sharedIntent.intentType
4603
+ || resolvedNormalizedIntent
4604
+ || authoritySource.normalized_intent
4605
+ || "",
4606
+ ).trim().toLowerCase();
4607
+ return {
4608
+ authoritySource,
4609
+ conversationID: String(resolvedConversationID || "").trim(),
4610
+ selectedBotUsernames: authoritySelectedBotUsernames,
4611
+ normalizedIntent: preferredNormalizedIntent,
4612
+ };
4613
+ }
4614
+
4615
+ async function claimRunnerRequestForHumanComment({
4616
+ normalizedRoute,
4617
+ routeKey,
4618
+ selectedRecord,
4619
+ selectedBotUsernames = [],
4620
+ normalizedIntent = "",
4621
+ sharedHumanIntent = null,
4622
+ runtime = null,
4623
+ archiveThreadID = "",
4624
+ authoritativeSourceMessageEnvelope = {},
4625
+ }) {
4632
4626
  const parsed = safeObject(selectedRecord?.parsedArchive);
4633
4627
  const commentKind = String(parsed.kind || "").trim().toLowerCase();
4634
4628
  if (!isInboundArchiveKind(commentKind)) {
@@ -4637,45 +4631,45 @@ async function claimRunnerRequestForHumanComment({
4637
4631
  reason: "non_human_comment_cannot_create_request",
4638
4632
  };
4639
4633
  }
4640
- const currentState = loadBotRunnerState();
4641
- const currentRouteState = safeObject(safeObject(currentState.routes)[String(routeKey || "").trim()]);
4642
- const replyChainResolution = await resolveRunnerReplyChainConversationContextWithServerFallback({
4643
- state: currentState,
4634
+ const currentState = loadBotRunnerState();
4635
+ const currentRouteState = safeObject(safeObject(currentState.routes)[String(routeKey || "").trim()]);
4636
+ const replyChainResolution = await resolveRunnerReplyChainConversationContextWithServerFallback({
4637
+ state: currentState,
4644
4638
  normalizedRoute,
4645
4639
  selectedRecord,
4646
4640
  runtime,
4647
4641
  archiveThreadID,
4648
4642
  });
4649
4643
  const replyChainContext = safeObject(replyChainResolution.replyChainContext);
4650
- const referencedRequest = safeObject(replyChainContext.referencedRequest);
4651
- const resolvedNormalizedIntent = resolveRunnerRequestClaimIntent({
4652
- normalizedIntent,
4653
- selectedRecord,
4654
- });
4655
- let stateForClaim = safeObject(replyChainResolution.state);
4656
- const normalizedSharedHumanIntent = safeObject(sharedHumanIntent);
4657
- const provisionalConversationID = String(
4658
- parsed.conversationID
4659
- || replyChainContext.conversationID
4660
- || "",
4661
- ).trim();
4662
- const provisionalNormalizedIntent = String(
4663
- normalizedSharedHumanIntent.intentType
4664
- || resolvedNormalizedIntent
4665
- || referencedRequest.normalized_intent
4666
- || "",
4667
- ).trim().toLowerCase();
4668
- const provisionalRequestKey = buildRunnerRequestKey({
4669
- normalizedRoute,
4670
- selectedRecord,
4671
- selectedBotUsernames,
4672
- normalizedIntent: provisionalNormalizedIntent,
4673
- conversationID: provisionalConversationID,
4674
- });
4675
- const baseConversationID = provisionalConversationID;
4676
- if (
4677
- Object.keys(referencedRequest).length > 0
4678
- && baseConversationID
4644
+ const referencedRequest = safeObject(replyChainContext.referencedRequest);
4645
+ const resolvedNormalizedIntent = resolveRunnerRequestClaimIntent({
4646
+ normalizedIntent,
4647
+ selectedRecord,
4648
+ });
4649
+ let stateForClaim = safeObject(replyChainResolution.state);
4650
+ const normalizedSharedHumanIntent = safeObject(sharedHumanIntent);
4651
+ const provisionalConversationID = String(
4652
+ parsed.conversationID
4653
+ || replyChainContext.conversationID
4654
+ || "",
4655
+ ).trim();
4656
+ const provisionalNormalizedIntent = String(
4657
+ normalizedSharedHumanIntent.intentType
4658
+ || resolvedNormalizedIntent
4659
+ || referencedRequest.normalized_intent
4660
+ || "",
4661
+ ).trim().toLowerCase();
4662
+ const provisionalRequestKey = buildRunnerRequestKey({
4663
+ normalizedRoute,
4664
+ selectedRecord,
4665
+ selectedBotUsernames,
4666
+ normalizedIntent: provisionalNormalizedIntent,
4667
+ conversationID: provisionalConversationID,
4668
+ });
4669
+ const baseConversationID = provisionalConversationID;
4670
+ if (
4671
+ Object.keys(referencedRequest).length > 0
4672
+ && baseConversationID
4679
4673
  && !String(referencedRequest.conversation_id || "").trim()
4680
4674
  && String(referencedRequest.request_key || "").trim()
4681
4675
  ) {
@@ -4703,44 +4697,44 @@ async function claimRunnerRequestForHumanComment({
4703
4697
  && runtime?.baseURL
4704
4698
  && runtime?.token
4705
4699
  ) {
4706
- sharedConversationSource = safeObject(await findServerRunnerConversationSourceRequestForMessageID({
4707
- normalizedRoute,
4708
- runtime,
4709
- chatID: String(parsed.chatID || parsed.chatId || "").trim(),
4710
- messageID: currentMessageID,
4711
- excludeRequestKey: provisionalRequestKey,
4712
- }));
4713
- }
4714
- const authorityContext = resolveRunnerHumanCommentAuthorityContext({
4715
- normalizedRoute,
4716
- selectedRecord,
4717
- replyChainContext,
4718
- referencedRequest,
4719
- sharedConversationSource,
4720
- selectedBotUsernames,
4721
- normalizedSharedHumanIntent,
4722
- resolvedNormalizedIntent,
4723
- });
4724
- const authoritySource = safeObject(authorityContext.authoritySource);
4725
- const authorityReplyChainContext = await buildRunnerAuthorityReplyChainContext({
4726
- selectedRecord,
4727
- replyChainContext,
4728
- authoritySource,
4729
- runtime,
4730
- archiveThreadID,
4731
- });
4732
- const resolvedConversationID = String(authorityContext.conversationID || "").trim();
4733
- const preferredNormalizedIntent = String(authorityContext.normalizedIntent || "").trim().toLowerCase();
4734
- const requestSelectedBotUsernames = ensureArray(authorityContext.selectedBotUsernames);
4735
- const requestKey = buildRunnerRequestKey({
4736
- normalizedRoute,
4737
- selectedRecord,
4738
- selectedBotUsernames: requestSelectedBotUsernames,
4739
- normalizedIntent: preferredNormalizedIntent,
4740
- conversationID: resolvedConversationID,
4741
- });
4742
- const requests = normalizeBotRunnerRequests(stateForClaim.requests);
4743
- const existing = safeObject(requests[requestKey]);
4700
+ sharedConversationSource = safeObject(await findServerRunnerConversationSourceRequestForMessageID({
4701
+ normalizedRoute,
4702
+ runtime,
4703
+ chatID: String(parsed.chatID || parsed.chatId || "").trim(),
4704
+ messageID: currentMessageID,
4705
+ excludeRequestKey: provisionalRequestKey,
4706
+ }));
4707
+ }
4708
+ const authorityContext = resolveRunnerHumanCommentAuthorityContext({
4709
+ normalizedRoute,
4710
+ selectedRecord,
4711
+ replyChainContext,
4712
+ referencedRequest,
4713
+ sharedConversationSource,
4714
+ selectedBotUsernames,
4715
+ normalizedSharedHumanIntent,
4716
+ resolvedNormalizedIntent,
4717
+ });
4718
+ const authoritySource = safeObject(authorityContext.authoritySource);
4719
+ const authorityReplyChainContext = await buildRunnerAuthorityReplyChainContext({
4720
+ selectedRecord,
4721
+ replyChainContext,
4722
+ authoritySource,
4723
+ runtime,
4724
+ archiveThreadID,
4725
+ });
4726
+ const resolvedConversationID = String(authorityContext.conversationID || "").trim();
4727
+ const preferredNormalizedIntent = String(authorityContext.normalizedIntent || "").trim().toLowerCase();
4728
+ const requestSelectedBotUsernames = ensureArray(authorityContext.selectedBotUsernames);
4729
+ const requestKey = buildRunnerRequestKey({
4730
+ normalizedRoute,
4731
+ selectedRecord,
4732
+ selectedBotUsernames: requestSelectedBotUsernames,
4733
+ normalizedIntent: preferredNormalizedIntent,
4734
+ conversationID: resolvedConversationID,
4735
+ });
4736
+ const requests = normalizeBotRunnerRequests(stateForClaim.requests);
4737
+ const existing = safeObject(requests[requestKey]);
4744
4738
  if (isFinalRunnerRequestStatus(existing.status)) {
4745
4739
  return {
4746
4740
  ok: false,
@@ -4758,126 +4752,126 @@ async function claimRunnerRequestForHumanComment({
4758
4752
  reason: "request_already_claimed",
4759
4753
  requestKey,
4760
4754
  };
4761
- }
4762
- const nowISO = new Date().toISOString();
4763
- const sourceMessageEnvelope = selectAuthoritativeRunnerSourceMessageEnvelope(
4764
- authoritativeSourceMessageEnvelope,
4765
- buildRunnerSourceMessageEnvelope({
4766
- routeState: currentRouteState,
4767
- routeKey,
4768
- normalizedRoute,
4769
- parsedArchive: parsed,
4770
- }),
4771
- );
4772
- const { requests: nextRequests, request } = upsertRunnerRequest(stateForClaim, requestKey, {
4773
- project_id: String(normalizedRoute?.projectID || "").trim(),
4774
- provider: String(normalizedRoute?.provider || "").trim(),
4775
- chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
4776
- source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
4777
- source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
4778
- source_message_body: String(parsed.body || "").trim(),
4779
- source_message_origin: String(sourceMessageEnvelope.source_origin || "").trim().toLowerCase(),
4780
- source_message_route_key: String(sourceMessageEnvelope.source_route_key || "").trim(),
4781
- source_message_bot_username: normalizeTelegramMentionUsername(sourceMessageEnvelope.source_bot_username),
4782
- source_message_envelope: sourceMessageEnvelope,
4783
- root_comment_id: String(selectedRecord?.id || "").trim(),
4784
- root_comment_kind: commentKind,
4785
- conversation_id: resolvedConversationID,
4786
- reply_chain_context: authorityReplyChainContext,
4787
- selected_bot_usernames: uniqueOrderedStrings(requestSelectedBotUsernames, normalizeTelegramMentionUsername),
4788
- conversation_intent_mode: String(
4789
- normalizedSharedHumanIntent.intentMode
4790
- || existing.conversation_intent_mode
4791
- || authoritySource.conversation_intent_mode
4792
- || "",
4793
- ).trim().toLowerCase(),
4794
- conversation_lead_bot: normalizeTelegramMentionUsername(
4795
- normalizedSharedHumanIntent.leadBotSelector
4796
- || existing.conversation_lead_bot
4797
- || authoritySource.conversation_lead_bot,
4798
- ),
4799
- conversation_summary_bot: normalizeTelegramMentionUsername(
4800
- normalizedSharedHumanIntent.summaryBotSelector
4801
- || existing.conversation_summary_bot
4802
- || authoritySource.conversation_summary_bot,
4803
- ),
4804
- conversation_participants: uniqueOrderedStrings(
4805
- ensureArray(normalizedSharedHumanIntent.participantSelectors).length
4806
- ? normalizedSharedHumanIntent.participantSelectors
4807
- : ensureArray(existing.conversation_participants).length
4808
- ? existing.conversation_participants
4809
- : ensureArray(authoritySource.conversation_participants).length
4810
- ? authoritySource.conversation_participants
4811
- : [],
4812
- normalizeTelegramMentionUsername,
4813
- ),
4814
- conversation_initial_responders: uniqueOrderedStrings(
4815
- ensureArray(normalizedSharedHumanIntent.initialResponderSelectors).length
4816
- ? normalizedSharedHumanIntent.initialResponderSelectors
4817
- : ensureArray(existing.conversation_initial_responders).length
4818
- ? existing.conversation_initial_responders
4819
- : ensureArray(authoritySource.conversation_initial_responders).length
4820
- ? authoritySource.conversation_initial_responders
4821
- : [],
4822
- normalizeTelegramMentionUsername,
4823
- ),
4824
- conversation_allowed_responders: uniqueOrderedStrings(
4825
- ensureArray(normalizedSharedHumanIntent.allowedResponderSelectors).length
4826
- ? normalizedSharedHumanIntent.allowedResponderSelectors
4827
- : ensureArray(existing.conversation_allowed_responders).length
4828
- ? existing.conversation_allowed_responders
4829
- : ensureArray(authoritySource.conversation_allowed_responders).length
4830
- ? authoritySource.conversation_allowed_responders
4831
- : [],
4832
- normalizeTelegramMentionUsername,
4833
- ),
4834
- conversation_allow_bot_to_bot: normalizedSharedHumanIntent.allowBotToBot === true
4835
- || existing.conversation_allow_bot_to_bot === true
4836
- || authoritySource.conversation_allow_bot_to_bot === true,
4837
- conversation_reply_expectation: String(
4838
- normalizedSharedHumanIntent.replyExpectation
4839
- || existing.conversation_reply_expectation
4840
- || authoritySource.conversation_reply_expectation
4841
- || "",
4842
- ).trim().toLowerCase(),
4843
- execution_contract_type: runnerRequestPreferredExecutionContractType(existing)
4844
- || runnerRequestPreferredExecutionContractType(authoritySource),
4845
- execution_contract_actionable: runnerRequestPreferredExecutionContractActionable(existing)
4846
- || runnerRequestPreferredExecutionContractActionable(authoritySource),
4847
- execution_contract_targets: uniqueOrderedStrings(
4848
- runnerRequestPreferredExecutionContractTargets(existing).length
4849
- ? runnerRequestPreferredExecutionContractTargets(existing)
4850
- : runnerRequestPreferredExecutionContractTargets(authoritySource),
4851
- normalizeTelegramMentionUsername,
4852
- ),
4853
- next_expected_responders: uniqueOrderedStrings(
4854
- runnerRequestPreferredNextExpectedResponders(existing).length
4855
- ? runnerRequestPreferredNextExpectedResponders(existing)
4856
- : runnerRequestPreferredNextExpectedResponders(authoritySource),
4857
- normalizeTelegramMentionUsername,
4858
- ),
4859
- normalized_intent: String(preferredNormalizedIntent || existing.normalized_intent || "").trim().toLowerCase(),
4860
- status: "claimed",
4861
- claimed_by_route: String(routeKey || "").trim(),
4862
- claimed_at: firstNonEmptyString([existing.claimed_at, nowISO]) || nowISO,
4863
- root_work_item_id: String(existing.root_work_item_id || authoritySource.root_work_item_id || "").trim(),
4864
- root_work_item_title: String(existing.root_work_item_title || authoritySource.root_work_item_title || "").trim(),
4865
- root_work_item_status: normalizeRunnerWorkItemStatus(
4866
- existing.root_work_item_status || authoritySource.root_work_item_status,
4867
- ),
4868
- root_thread_id: String(existing.root_thread_id || authoritySource.root_thread_id || "").trim(),
4869
- root_work_item_created_at: firstNonEmptyString([
4870
- existing.root_work_item_created_at,
4871
- authoritySource.root_work_item_created_at,
4872
- ]),
4873
- root_work_item_last_error: String(
4874
- existing.root_work_item_last_error || authoritySource.root_work_item_last_error || "",
4875
- ).trim(),
4876
- last_comment_id: String(selectedRecord?.id || "").trim(),
4877
- last_comment_kind: commentKind,
4878
- last_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
4879
- last_source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
4880
- });
4755
+ }
4756
+ const nowISO = new Date().toISOString();
4757
+ const sourceMessageEnvelope = selectAuthoritativeRunnerSourceMessageEnvelope(
4758
+ authoritativeSourceMessageEnvelope,
4759
+ buildRunnerSourceMessageEnvelope({
4760
+ routeState: currentRouteState,
4761
+ routeKey,
4762
+ normalizedRoute,
4763
+ parsedArchive: parsed,
4764
+ }),
4765
+ );
4766
+ const { requests: nextRequests, request } = upsertRunnerRequest(stateForClaim, requestKey, {
4767
+ project_id: String(normalizedRoute?.projectID || "").trim(),
4768
+ provider: String(normalizedRoute?.provider || "").trim(),
4769
+ chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
4770
+ source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
4771
+ source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
4772
+ source_message_body: String(parsed.body || "").trim(),
4773
+ source_message_origin: String(sourceMessageEnvelope.source_origin || "").trim().toLowerCase(),
4774
+ source_message_route_key: String(sourceMessageEnvelope.source_route_key || "").trim(),
4775
+ source_message_bot_username: normalizeTelegramMentionUsername(sourceMessageEnvelope.source_bot_username),
4776
+ source_message_envelope: sourceMessageEnvelope,
4777
+ root_comment_id: String(selectedRecord?.id || "").trim(),
4778
+ root_comment_kind: commentKind,
4779
+ conversation_id: resolvedConversationID,
4780
+ reply_chain_context: authorityReplyChainContext,
4781
+ selected_bot_usernames: uniqueOrderedStrings(requestSelectedBotUsernames, normalizeTelegramMentionUsername),
4782
+ conversation_intent_mode: String(
4783
+ normalizedSharedHumanIntent.intentMode
4784
+ || existing.conversation_intent_mode
4785
+ || authoritySource.conversation_intent_mode
4786
+ || "",
4787
+ ).trim().toLowerCase(),
4788
+ conversation_lead_bot: normalizeTelegramMentionUsername(
4789
+ normalizedSharedHumanIntent.leadBotSelector
4790
+ || existing.conversation_lead_bot
4791
+ || authoritySource.conversation_lead_bot,
4792
+ ),
4793
+ conversation_summary_bot: normalizeTelegramMentionUsername(
4794
+ normalizedSharedHumanIntent.summaryBotSelector
4795
+ || existing.conversation_summary_bot
4796
+ || authoritySource.conversation_summary_bot,
4797
+ ),
4798
+ conversation_participants: uniqueOrderedStrings(
4799
+ ensureArray(normalizedSharedHumanIntent.participantSelectors).length
4800
+ ? normalizedSharedHumanIntent.participantSelectors
4801
+ : ensureArray(existing.conversation_participants).length
4802
+ ? existing.conversation_participants
4803
+ : ensureArray(authoritySource.conversation_participants).length
4804
+ ? authoritySource.conversation_participants
4805
+ : [],
4806
+ normalizeTelegramMentionUsername,
4807
+ ),
4808
+ conversation_initial_responders: uniqueOrderedStrings(
4809
+ ensureArray(normalizedSharedHumanIntent.initialResponderSelectors).length
4810
+ ? normalizedSharedHumanIntent.initialResponderSelectors
4811
+ : ensureArray(existing.conversation_initial_responders).length
4812
+ ? existing.conversation_initial_responders
4813
+ : ensureArray(authoritySource.conversation_initial_responders).length
4814
+ ? authoritySource.conversation_initial_responders
4815
+ : [],
4816
+ normalizeTelegramMentionUsername,
4817
+ ),
4818
+ conversation_allowed_responders: uniqueOrderedStrings(
4819
+ ensureArray(normalizedSharedHumanIntent.allowedResponderSelectors).length
4820
+ ? normalizedSharedHumanIntent.allowedResponderSelectors
4821
+ : ensureArray(existing.conversation_allowed_responders).length
4822
+ ? existing.conversation_allowed_responders
4823
+ : ensureArray(authoritySource.conversation_allowed_responders).length
4824
+ ? authoritySource.conversation_allowed_responders
4825
+ : [],
4826
+ normalizeTelegramMentionUsername,
4827
+ ),
4828
+ conversation_allow_bot_to_bot: normalizedSharedHumanIntent.allowBotToBot === true
4829
+ || existing.conversation_allow_bot_to_bot === true
4830
+ || authoritySource.conversation_allow_bot_to_bot === true,
4831
+ conversation_reply_expectation: String(
4832
+ normalizedSharedHumanIntent.replyExpectation
4833
+ || existing.conversation_reply_expectation
4834
+ || authoritySource.conversation_reply_expectation
4835
+ || "",
4836
+ ).trim().toLowerCase(),
4837
+ execution_contract_type: runnerRequestPreferredExecutionContractType(existing)
4838
+ || runnerRequestPreferredExecutionContractType(authoritySource),
4839
+ execution_contract_actionable: runnerRequestPreferredExecutionContractActionable(existing)
4840
+ || runnerRequestPreferredExecutionContractActionable(authoritySource),
4841
+ execution_contract_targets: uniqueOrderedStrings(
4842
+ runnerRequestPreferredExecutionContractTargets(existing).length
4843
+ ? runnerRequestPreferredExecutionContractTargets(existing)
4844
+ : runnerRequestPreferredExecutionContractTargets(authoritySource),
4845
+ normalizeTelegramMentionUsername,
4846
+ ),
4847
+ next_expected_responders: uniqueOrderedStrings(
4848
+ runnerRequestPreferredNextExpectedResponders(existing).length
4849
+ ? runnerRequestPreferredNextExpectedResponders(existing)
4850
+ : runnerRequestPreferredNextExpectedResponders(authoritySource),
4851
+ normalizeTelegramMentionUsername,
4852
+ ),
4853
+ normalized_intent: String(preferredNormalizedIntent || existing.normalized_intent || "").trim().toLowerCase(),
4854
+ status: "claimed",
4855
+ claimed_by_route: String(routeKey || "").trim(),
4856
+ claimed_at: firstNonEmptyString([existing.claimed_at, nowISO]) || nowISO,
4857
+ root_work_item_id: String(existing.root_work_item_id || authoritySource.root_work_item_id || "").trim(),
4858
+ root_work_item_title: String(existing.root_work_item_title || authoritySource.root_work_item_title || "").trim(),
4859
+ root_work_item_status: normalizeRunnerWorkItemStatus(
4860
+ existing.root_work_item_status || authoritySource.root_work_item_status,
4861
+ ),
4862
+ root_thread_id: String(existing.root_thread_id || authoritySource.root_thread_id || "").trim(),
4863
+ root_work_item_created_at: firstNonEmptyString([
4864
+ existing.root_work_item_created_at,
4865
+ authoritySource.root_work_item_created_at,
4866
+ ]),
4867
+ root_work_item_last_error: String(
4868
+ existing.root_work_item_last_error || authoritySource.root_work_item_last_error || "",
4869
+ ).trim(),
4870
+ last_comment_id: String(selectedRecord?.id || "").trim(),
4871
+ last_comment_kind: commentKind,
4872
+ last_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
4873
+ last_source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
4874
+ });
4881
4875
  const { consumedComments: nextConsumedComments } = upsertRunnerConsumedComment(stateForClaim, selectedRecord?.id, {
4882
4876
  project_id: String(normalizedRoute?.projectID || "").trim(),
4883
4877
  provider: String(normalizedRoute?.provider || "").trim(),
@@ -5390,7 +5384,7 @@ function buildRunnerRootWorkItemTransitionPath(currentStatusRaw, targetStatusRaw
5390
5384
  return [];
5391
5385
  }
5392
5386
 
5393
- function buildRunnerRouteFollowupSnapshotPatch(selectedRecordRaw, resultRaw = {}) {
5387
+ function buildRunnerRouteFollowupSnapshotPatch(selectedRecordRaw, resultRaw = {}) {
5394
5388
  const parsed = safeObject(safeObject(selectedRecordRaw).parsedArchive);
5395
5389
  const commentKind = String(parsed.kind || "").trim().toLowerCase();
5396
5390
  if (["telegram_message", "telegram_edited_message"].includes(commentKind)) {
@@ -5429,26 +5423,26 @@ function buildRunnerRouteFollowupSnapshotPatch(selectedRecordRaw, resultRaw = {}
5429
5423
  ensureArray(result.assignment_validation_modes),
5430
5424
  (value) => String(value || "").trim().toLowerCase(),
5431
5425
  ),
5432
- last_followup_delivery_status: String(result.delivery_status || "").trim().toLowerCase(),
5433
- last_followup_archive_status: String(result.archive_status || "").trim().toLowerCase(),
5434
- last_followup_transport_error: String(result.transport_error || "").trim(),
5435
- last_followup_archive_error: String(result.archive_error || "").trim(),
5436
- last_followup_source_message_envelope: normalizeRunnerTelegramMessageEnvelope(result.source_message_envelope),
5437
- last_followup_last_reply_message_envelope: normalizeRunnerTelegramMessageEnvelope(result.last_reply_message_envelope),
5438
- last_followup_attempted_delivery_envelope: normalizeRunnerTelegramMessageEnvelope(result.attempted_delivery_envelope),
5439
- });
5440
- }
5441
-
5442
- function buildRunnerRequestRecoveryPatchFromRouteState(currentStateRaw, requestRaw, routeKeyHint = "") {
5443
- const currentState = safeObject(currentStateRaw);
5444
- const request = safeObject(requestRaw);
5445
- const requestKey = String(request.request_key || "").trim();
5446
- if (!requestKey) {
5447
- return {};
5448
- }
5449
- const requestSourceMessageID = intFromRawAllowZero(request.last_source_message_id, 0)
5450
- || intFromRawAllowZero(request.source_message_id, 0)
5451
- || intFromRawAllowZero(safeObject(request.source_message_envelope).message_id, 0);
5426
+ last_followup_delivery_status: String(result.delivery_status || "").trim().toLowerCase(),
5427
+ last_followup_archive_status: String(result.archive_status || "").trim().toLowerCase(),
5428
+ last_followup_transport_error: String(result.transport_error || "").trim(),
5429
+ last_followup_archive_error: String(result.archive_error || "").trim(),
5430
+ last_followup_source_message_envelope: normalizeRunnerTelegramMessageEnvelope(result.source_message_envelope),
5431
+ last_followup_last_reply_message_envelope: normalizeRunnerTelegramMessageEnvelope(result.last_reply_message_envelope),
5432
+ last_followup_attempted_delivery_envelope: normalizeRunnerTelegramMessageEnvelope(result.attempted_delivery_envelope),
5433
+ });
5434
+ }
5435
+
5436
+ function buildRunnerRequestRecoveryPatchFromRouteState(currentStateRaw, requestRaw, routeKeyHint = "") {
5437
+ const currentState = safeObject(currentStateRaw);
5438
+ const request = safeObject(requestRaw);
5439
+ const requestKey = String(request.request_key || "").trim();
5440
+ if (!requestKey) {
5441
+ return {};
5442
+ }
5443
+ const requestSourceMessageID = intFromRawAllowZero(request.last_source_message_id, 0)
5444
+ || intFromRawAllowZero(request.source_message_id, 0)
5445
+ || intFromRawAllowZero(safeObject(request.source_message_envelope).message_id, 0);
5452
5446
  const routeKeys = uniqueOrderedStrings(
5453
5447
  [
5454
5448
  routeKeyHint,
@@ -5456,14 +5450,14 @@ function buildRunnerRequestRecoveryPatchFromRouteState(currentStateRaw, requestR
5456
5450
  ],
5457
5451
  (value) => String(value || "").trim(),
5458
5452
  ).filter(Boolean);
5459
- for (const routeKey of routeKeys) {
5460
- const routeState = safeObject(safeObject(currentState.routes)[routeKey]);
5453
+ for (const routeKey of routeKeys) {
5454
+ const routeState = safeObject(safeObject(currentState.routes)[routeKey]);
5461
5455
  if (!Object.keys(routeState).length) {
5462
5456
  continue;
5463
5457
  }
5464
- const routeSourceMessageID = intFromRawAllowZero(routeState.last_source_message_id, 0)
5465
- || intFromRawAllowZero(routeState.source_message_id, 0)
5466
- || intFromRawAllowZero(safeObject(routeState.last_followup_source_message_envelope).message_id, 0);
5458
+ const routeSourceMessageID = intFromRawAllowZero(routeState.last_source_message_id, 0)
5459
+ || intFromRawAllowZero(routeState.source_message_id, 0)
5460
+ || intFromRawAllowZero(safeObject(routeState.last_followup_source_message_envelope).message_id, 0);
5467
5461
  if (requestSourceMessageID && routeSourceMessageID && requestSourceMessageID !== routeSourceMessageID) {
5468
5462
  continue;
5469
5463
  }
@@ -5519,69 +5513,69 @@ function buildRunnerRequestRecoveryPatchFromRouteState(currentStateRaw, requestR
5519
5513
  setFollowupStringPatch("followup_assignment_validation_status", ["last_followup_assignment_validation_status", "last_assignment_validation_status"]);
5520
5514
  setFollowupStringPatch("followup_assignment_validation_reason", ["last_followup_assignment_validation_reason", "last_assignment_validation_reason"]);
5521
5515
  setFollowupArrayPatch("followup_assignment_validation_modes", ["last_followup_assignment_validation_modes"], (value) => String(value || "").trim().toLowerCase());
5522
- setFollowupStringPatch("followup_delivery_status", ["last_followup_delivery_status"]);
5523
- setFollowupStringPatch("followup_archive_status", ["last_followup_archive_status"]);
5524
- setFollowupStringPatch("followup_transport_error", ["last_followup_transport_error"]);
5525
- setFollowupStringPatch("followup_archive_error", ["last_followup_archive_error"]);
5526
- const routeFollowupDeliveryStatus = String(routeState.last_followup_delivery_status || "").trim().toLowerCase();
5527
- const routeFollowupDelivered = ["delivered", "dry_run"].includes(routeFollowupDeliveryStatus);
5528
- const routeLocalRecoveredSourceEnvelope = normalizeRunnerTelegramMessageEnvelope(
5529
- findRunnerRouteLocalInboundEnvelopeByMessage(routeState, {
5530
- chatID: String(request.chat_id || request.chatID || "").trim(),
5531
- messageID: requestSourceMessageID || routeSourceMessageID,
5532
- }),
5533
- );
5534
- const recoveredSourceEnvelope = mergeRunnerSourceMessageEnvelopes(
5535
- routeState.last_followup_source_message_envelope,
5536
- routeLocalRecoveredSourceEnvelope,
5537
- );
5538
- if (Object.keys(recoveredSourceEnvelope).length) {
5539
- if (!Object.keys(safeObject(request.source_message_envelope)).length) {
5540
- patch.source_message_envelope = recoveredSourceEnvelope;
5541
- }
5542
- if (!(intFromRawAllowZero(request.source_message_id, 0) > 0)) {
5543
- const recoveredMessageID = intFromRawAllowZero(recoveredSourceEnvelope.message_id, 0);
5544
- if (recoveredMessageID > 0) {
5545
- patch.source_message_id = recoveredMessageID;
5546
- }
5547
- }
5548
- if (!(intFromRawAllowZero(request.source_message_thread_id, 0) > 0)) {
5549
- const recoveredThreadID = intFromRawAllowZero(recoveredSourceEnvelope.message_thread_id, 0);
5550
- if (recoveredThreadID > 0) {
5551
- patch.source_message_thread_id = recoveredThreadID;
5552
- }
5553
- }
5554
- if (!String(request.source_message_body || "").trim() && String(recoveredSourceEnvelope.body || "").trim()) {
5555
- patch.source_message_body = String(recoveredSourceEnvelope.body || "").trim();
5556
- }
5557
- if (!String(request.source_message_origin || "").trim() && String(recoveredSourceEnvelope.source_origin || "").trim()) {
5558
- patch.source_message_origin = String(recoveredSourceEnvelope.source_origin || "").trim().toLowerCase();
5559
- }
5560
- if (!String(request.source_message_route_key || "").trim() && String(recoveredSourceEnvelope.source_route_key || "").trim()) {
5561
- patch.source_message_route_key = String(recoveredSourceEnvelope.source_route_key || "").trim();
5562
- }
5563
- if (
5564
- !normalizeTelegramMentionUsername(request.source_message_bot_username)
5565
- && normalizeTelegramMentionUsername(recoveredSourceEnvelope.source_bot_username)
5566
- ) {
5567
- patch.source_message_bot_username = normalizeTelegramMentionUsername(
5568
- recoveredSourceEnvelope.source_bot_username,
5569
- );
5570
- }
5571
- }
5572
- if (!Object.keys(safeObject(request.last_reply_message_envelope)).length && routeFollowupDelivered) {
5573
- const recoveredReplyEnvelope = normalizeRunnerTelegramMessageEnvelope(routeState.last_followup_last_reply_message_envelope);
5574
- if (intFromRawAllowZero(recoveredReplyEnvelope.message_id, 0) > 0) {
5575
- patch.last_reply_message_envelope = recoveredReplyEnvelope;
5576
- }
5577
- }
5578
- if (!Object.keys(safeObject(request.attempted_delivery_envelope)).length) {
5579
- const recoveredAttemptEnvelope = normalizeRunnerTelegramMessageEnvelope(routeState.last_followup_attempted_delivery_envelope);
5580
- if (Object.keys(recoveredAttemptEnvelope).length) {
5581
- patch.attempted_delivery_envelope = recoveredAttemptEnvelope;
5582
- }
5583
- }
5584
- const requestStatus = normalizeRunnerRequestStatus(request.status);
5516
+ setFollowupStringPatch("followup_delivery_status", ["last_followup_delivery_status"]);
5517
+ setFollowupStringPatch("followup_archive_status", ["last_followup_archive_status"]);
5518
+ setFollowupStringPatch("followup_transport_error", ["last_followup_transport_error"]);
5519
+ setFollowupStringPatch("followup_archive_error", ["last_followup_archive_error"]);
5520
+ const routeFollowupDeliveryStatus = String(routeState.last_followup_delivery_status || "").trim().toLowerCase();
5521
+ const routeFollowupDelivered = ["delivered", "dry_run"].includes(routeFollowupDeliveryStatus);
5522
+ const routeLocalRecoveredSourceEnvelope = normalizeRunnerTelegramMessageEnvelope(
5523
+ findRunnerRouteLocalInboundEnvelopeByMessage(routeState, {
5524
+ chatID: String(request.chat_id || request.chatID || "").trim(),
5525
+ messageID: requestSourceMessageID || routeSourceMessageID,
5526
+ }),
5527
+ );
5528
+ const recoveredSourceEnvelope = mergeRunnerSourceMessageEnvelopes(
5529
+ routeState.last_followup_source_message_envelope,
5530
+ routeLocalRecoveredSourceEnvelope,
5531
+ );
5532
+ if (Object.keys(recoveredSourceEnvelope).length) {
5533
+ if (!Object.keys(safeObject(request.source_message_envelope)).length) {
5534
+ patch.source_message_envelope = recoveredSourceEnvelope;
5535
+ }
5536
+ if (!(intFromRawAllowZero(request.source_message_id, 0) > 0)) {
5537
+ const recoveredMessageID = intFromRawAllowZero(recoveredSourceEnvelope.message_id, 0);
5538
+ if (recoveredMessageID > 0) {
5539
+ patch.source_message_id = recoveredMessageID;
5540
+ }
5541
+ }
5542
+ if (!(intFromRawAllowZero(request.source_message_thread_id, 0) > 0)) {
5543
+ const recoveredThreadID = intFromRawAllowZero(recoveredSourceEnvelope.message_thread_id, 0);
5544
+ if (recoveredThreadID > 0) {
5545
+ patch.source_message_thread_id = recoveredThreadID;
5546
+ }
5547
+ }
5548
+ if (!String(request.source_message_body || "").trim() && String(recoveredSourceEnvelope.body || "").trim()) {
5549
+ patch.source_message_body = String(recoveredSourceEnvelope.body || "").trim();
5550
+ }
5551
+ if (!String(request.source_message_origin || "").trim() && String(recoveredSourceEnvelope.source_origin || "").trim()) {
5552
+ patch.source_message_origin = String(recoveredSourceEnvelope.source_origin || "").trim().toLowerCase();
5553
+ }
5554
+ if (!String(request.source_message_route_key || "").trim() && String(recoveredSourceEnvelope.source_route_key || "").trim()) {
5555
+ patch.source_message_route_key = String(recoveredSourceEnvelope.source_route_key || "").trim();
5556
+ }
5557
+ if (
5558
+ !normalizeTelegramMentionUsername(request.source_message_bot_username)
5559
+ && normalizeTelegramMentionUsername(recoveredSourceEnvelope.source_bot_username)
5560
+ ) {
5561
+ patch.source_message_bot_username = normalizeTelegramMentionUsername(
5562
+ recoveredSourceEnvelope.source_bot_username,
5563
+ );
5564
+ }
5565
+ }
5566
+ if (!Object.keys(safeObject(request.last_reply_message_envelope)).length && routeFollowupDelivered) {
5567
+ const recoveredReplyEnvelope = normalizeRunnerTelegramMessageEnvelope(routeState.last_followup_last_reply_message_envelope);
5568
+ if (intFromRawAllowZero(recoveredReplyEnvelope.message_id, 0) > 0) {
5569
+ patch.last_reply_message_envelope = recoveredReplyEnvelope;
5570
+ }
5571
+ }
5572
+ if (!Object.keys(safeObject(request.attempted_delivery_envelope)).length) {
5573
+ const recoveredAttemptEnvelope = normalizeRunnerTelegramMessageEnvelope(routeState.last_followup_attempted_delivery_envelope);
5574
+ if (Object.keys(recoveredAttemptEnvelope).length) {
5575
+ patch.attempted_delivery_envelope = recoveredAttemptEnvelope;
5576
+ }
5577
+ }
5578
+ const requestStatus = normalizeRunnerRequestStatus(request.status);
5585
5579
  if (!isFinalRunnerRequestStatus(requestStatus)) {
5586
5580
  const routeAction = String(routeState.last_action || "").trim().toLowerCase();
5587
5581
  if (routeAction === "replied") {
@@ -5741,49 +5735,49 @@ function peekRunnerContinuationRequestForBotReply({
5741
5735
  || safeObject(sessionMatch.routeState).last_request_key
5742
5736
  || "",
5743
5737
  ).trim();
5744
- if (
5745
- fallbackRequestKey
5746
- && String(session.status || "").trim().toLowerCase() === "open"
5747
- ) {
5748
- const fallbackRequest = safeObject(normalizeBotRunnerRequests(currentState.requests)[fallbackRequestKey]);
5749
- const fallbackSourceMessageID = intFromRawAllowZero(
5750
- fallbackRequest.source_message_id
5751
- || safeObject(sessionMatch.routeState).last_source_message_id
5752
- || safeObject(sessionMatch.routeState).source_message_id
5753
- || safeObject(safeObject(sessionMatch.routeState).last_followup_source_message_envelope).message_id,
5754
- 0,
5755
- );
5756
- const fallbackLocalSourceEnvelope = normalizeRunnerTelegramMessageEnvelope(
5757
- findRunnerRouteLocalInboundEnvelopeByMessage(sessionMatch.routeState, {
5758
- chatID: String(parsed.chatID || parsed.chatId || "").trim(),
5759
- messageID: fallbackSourceMessageID,
5760
- }),
5761
- );
5762
- const fallbackSourceEnvelope = mergeRunnerSourceMessageEnvelopes(
5763
- safeObject(sessionMatch.routeState).last_followup_source_message_envelope,
5764
- fallbackLocalSourceEnvelope,
5765
- );
5766
- const nowISO = new Date().toISOString();
5767
- const seedRequest = Object.keys(fallbackRequest).length
5768
- ? fallbackRequest
5769
- : {
5770
- request_key: fallbackRequestKey,
5771
- project_id: String(normalizedRoute?.projectID || "").trim(),
5772
- provider: String(normalizedRoute?.provider || "").trim(),
5773
- chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
5774
- source_message_id: intFromRawAllowZero(fallbackSourceEnvelope.message_id, 0) || undefined,
5775
- source_message_thread_id: intFromRawAllowZero(fallbackSourceEnvelope.message_thread_id, 0) || undefined,
5776
- source_message_body: String(fallbackSourceEnvelope.body || "").trim(),
5777
- source_message_origin: String(fallbackSourceEnvelope.source_origin || "").trim().toLowerCase(),
5778
- source_message_route_key: String(fallbackSourceEnvelope.source_route_key || "").trim(),
5779
- source_message_bot_username: normalizeTelegramMentionUsername(
5780
- fallbackSourceEnvelope.source_bot_username,
5781
- ),
5782
- source_message_envelope: fallbackSourceEnvelope,
5783
- conversation_id: conversationID,
5784
- selected_bot_usernames: ensureArray(session.participants),
5785
- conversation_allowed_responders: ensureArray(session.allowed_responders),
5786
- conversation_intent_mode: String(session.intent_mode || "").trim().toLowerCase(),
5738
+ if (
5739
+ fallbackRequestKey
5740
+ && String(session.status || "").trim().toLowerCase() === "open"
5741
+ ) {
5742
+ const fallbackRequest = safeObject(normalizeBotRunnerRequests(currentState.requests)[fallbackRequestKey]);
5743
+ const fallbackSourceMessageID = intFromRawAllowZero(
5744
+ fallbackRequest.source_message_id
5745
+ || safeObject(sessionMatch.routeState).last_source_message_id
5746
+ || safeObject(sessionMatch.routeState).source_message_id
5747
+ || safeObject(safeObject(sessionMatch.routeState).last_followup_source_message_envelope).message_id,
5748
+ 0,
5749
+ );
5750
+ const fallbackLocalSourceEnvelope = normalizeRunnerTelegramMessageEnvelope(
5751
+ findRunnerRouteLocalInboundEnvelopeByMessage(sessionMatch.routeState, {
5752
+ chatID: String(parsed.chatID || parsed.chatId || "").trim(),
5753
+ messageID: fallbackSourceMessageID,
5754
+ }),
5755
+ );
5756
+ const fallbackSourceEnvelope = mergeRunnerSourceMessageEnvelopes(
5757
+ safeObject(sessionMatch.routeState).last_followup_source_message_envelope,
5758
+ fallbackLocalSourceEnvelope,
5759
+ );
5760
+ const nowISO = new Date().toISOString();
5761
+ const seedRequest = Object.keys(fallbackRequest).length
5762
+ ? fallbackRequest
5763
+ : {
5764
+ request_key: fallbackRequestKey,
5765
+ project_id: String(normalizedRoute?.projectID || "").trim(),
5766
+ provider: String(normalizedRoute?.provider || "").trim(),
5767
+ chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
5768
+ source_message_id: intFromRawAllowZero(fallbackSourceEnvelope.message_id, 0) || undefined,
5769
+ source_message_thread_id: intFromRawAllowZero(fallbackSourceEnvelope.message_thread_id, 0) || undefined,
5770
+ source_message_body: String(fallbackSourceEnvelope.body || "").trim(),
5771
+ source_message_origin: String(fallbackSourceEnvelope.source_origin || "").trim().toLowerCase(),
5772
+ source_message_route_key: String(fallbackSourceEnvelope.source_route_key || "").trim(),
5773
+ source_message_bot_username: normalizeTelegramMentionUsername(
5774
+ fallbackSourceEnvelope.source_bot_username,
5775
+ ),
5776
+ source_message_envelope: fallbackSourceEnvelope,
5777
+ conversation_id: conversationID,
5778
+ selected_bot_usernames: ensureArray(session.participants),
5779
+ conversation_allowed_responders: ensureArray(session.allowed_responders),
5780
+ conversation_intent_mode: String(session.intent_mode || "").trim().toLowerCase(),
5787
5781
  conversation_lead_bot: normalizeTelegramMentionUsername(session.lead_bot_username),
5788
5782
  conversation_summary_bot: normalizeTelegramMentionUsername(session.summary_bot_username),
5789
5783
  conversation_participants: ensureArray(session.participants),
@@ -5845,16 +5839,16 @@ function peekRunnerContinuationRequestForBotReply({
5845
5839
  };
5846
5840
  }
5847
5841
 
5848
- function resolveRunnerContinuationRequestForBotReply({
5849
- normalizedRoute,
5850
- routeKey,
5851
- selectedRecord,
5852
- }) {
5853
- const parsed = safeObject(selectedRecord?.parsedArchive);
5854
- const continuation = peekRunnerContinuationRequestForBotReply({
5855
- normalizedRoute,
5856
- selectedRecord,
5857
- });
5842
+ function resolveRunnerContinuationRequestForBotReply({
5843
+ normalizedRoute,
5844
+ routeKey,
5845
+ selectedRecord,
5846
+ }) {
5847
+ const parsed = safeObject(selectedRecord?.parsedArchive);
5848
+ const continuation = peekRunnerContinuationRequestForBotReply({
5849
+ normalizedRoute,
5850
+ selectedRecord,
5851
+ });
5858
5852
  if (!continuation.ok) {
5859
5853
  return continuation;
5860
5854
  }
@@ -5874,12 +5868,12 @@ function resolveRunnerContinuationRequestForBotReply({
5874
5868
  comment_kind: commentKind,
5875
5869
  request_status: request.status,
5876
5870
  });
5877
- const { requests: nextRequests, request: nextRequest } = upsertRunnerRequest(currentState, request.request_key, {
5878
- last_comment_id: String(selectedRecord?.id || "").trim(),
5879
- last_comment_kind: commentKind,
5880
- last_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
5881
- last_source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
5882
- });
5871
+ const { requests: nextRequests, request: nextRequest } = upsertRunnerRequest(currentState, request.request_key, {
5872
+ last_comment_id: String(selectedRecord?.id || "").trim(),
5873
+ last_comment_kind: commentKind,
5874
+ last_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
5875
+ last_source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
5876
+ });
5883
5877
  saveBotRunnerState({
5884
5878
  routes: currentState.routes,
5885
5879
  sharedInboxes: currentState.sharedInboxes || currentState.shared_inboxes,
@@ -5894,11 +5888,11 @@ function resolveRunnerContinuationRequestForBotReply({
5894
5888
  };
5895
5889
  }
5896
5890
 
5897
- function markRunnerRequestLifecycle({
5898
- normalizedRoute,
5899
- requestKey,
5900
- selectedRecord,
5901
- routeKey,
5891
+ function markRunnerRequestLifecycle({
5892
+ normalizedRoute,
5893
+ requestKey,
5894
+ selectedRecord,
5895
+ routeKey,
5902
5896
  outcome,
5903
5897
  conversationIDRaw = "",
5904
5898
  allowedResponders = [],
@@ -5928,77 +5922,77 @@ function markRunnerRequestLifecycle({
5928
5922
  assignmentValidationStatus = "",
5929
5923
  assignmentValidationReason = "",
5930
5924
  assignmentValidationModes = [],
5931
- deliveryStatus = "",
5932
- archiveStatus = "",
5933
- transportError = "",
5934
- archiveError = "",
5935
- lastReplyMessageID = 0,
5936
- lastReplyMessageThreadID = 0,
5937
- replyToMessageID = 0,
5938
- replyFallbackUsed = false,
5939
- authoritativeSourceMessageEnvelope = {},
5940
- }) {
5925
+ deliveryStatus = "",
5926
+ archiveStatus = "",
5927
+ transportError = "",
5928
+ archiveError = "",
5929
+ lastReplyMessageID = 0,
5930
+ lastReplyMessageThreadID = 0,
5931
+ replyToMessageID = 0,
5932
+ replyFallbackUsed = false,
5933
+ authoritativeSourceMessageEnvelope = {},
5934
+ }) {
5941
5935
  const key = String(requestKey || "").trim();
5942
5936
  if (!key) return null;
5943
- const currentState = loadBotRunnerState();
5944
- const existing = safeObject(normalizeBotRunnerRequests(currentState.requests)[key]);
5945
- if (!Object.keys(existing).length) return null;
5946
- const parsed = safeObject(selectedRecord?.parsedArchive);
5947
- const parsedKind = String(parsed.kind || "").trim().toLowerCase();
5948
- const conversationID = String(conversationIDRaw || existing.conversation_id || parsed.conversationID || "").trim();
5949
- const normalizedCurrentBotSelector = normalizeTelegramMentionUsername(
5950
- currentBotSelector
5951
- || safeObject(currentState.routes)[String(routeKey || "").trim()]?.last_speaker_bot_username
5952
- || "",
5953
- );
5954
- const sourceMessageEnvelope = selectAuthoritativeRunnerSourceMessageEnvelope(
5955
- selectAuthoritativeRunnerSourceMessageEnvelope(
5956
- authoritativeSourceMessageEnvelope,
5957
- safeObject(existing.source_message_envelope),
5958
- ),
5959
- buildRunnerSourceMessageEnvelope({
5960
- routeState: safeObject(safeObject(currentState.routes)[String(routeKey || "").trim()]),
5961
- routeKey,
5962
- normalizedRoute,
5963
- parsedArchive: parsed,
5964
- }),
5965
- );
5966
- const effectiveReplyToMessageID = intFromRawAllowZero(
5967
- replyToMessageID,
5968
- intFromRawAllowZero(existing.last_reply_to_message_id, 0),
5969
- );
5970
- const lastReplyMessageEnvelope = buildTelegramBotReplyEnvelope({
5971
- sourceEnvelope: sourceMessageEnvelope,
5972
- chatID: existing.chat_id,
5973
- messageID: lastReplyMessageID,
5974
- messageThreadID: lastReplyMessageThreadID,
5975
- replyToMessageID: effectiveReplyToMessageID,
5976
- senderUsername: normalizedCurrentBotSelector,
5977
- body: aiReplyPreview,
5978
- });
5979
- const normalizedDeliveryStatus = String(deliveryStatus || "").trim().toLowerCase();
5980
- const persistSuccessfulReplyEnvelope = ["delivered", "dry_run"].includes(normalizedDeliveryStatus)
5981
- && intFromRawAllowZero(lastReplyMessageEnvelope.message_id, 0) > 0;
5982
- const attemptedDeliveryEnvelope = buildTelegramBotReplyEnvelope({
5983
- sourceEnvelope: sourceMessageEnvelope,
5984
- chatID: existing.chat_id,
5985
- messageThreadID: lastReplyMessageThreadID,
5986
- replyToMessageID: effectiveReplyToMessageID,
5987
- senderUsername: normalizedCurrentBotSelector,
5988
- body: aiReplyPreview,
5989
- });
5990
- const shouldRefreshAttemptedDeliveryEnvelope = (
5991
- aiReplyGenerated === true
5992
- || String(aiReplyPreview || "").trim().length > 0
5993
- || String(deliveryStatus || "").trim().length > 0
5994
- || String(transportError || "").trim().length > 0
5995
- || intFromRawAllowZero(replyToMessageID, 0) > 0
5996
- || intFromRawAllowZero(lastReplyMessageThreadID, 0) > 0
5997
- );
5998
- const rootEffectiveExecutionContractTargets = uniqueOrderedStrings(
5999
- [
6000
- ...ensureArray(executionContractTargets),
6001
- ...ensureArray(normalizedExecutionContractTargets),
5937
+ const currentState = loadBotRunnerState();
5938
+ const existing = safeObject(normalizeBotRunnerRequests(currentState.requests)[key]);
5939
+ if (!Object.keys(existing).length) return null;
5940
+ const parsed = safeObject(selectedRecord?.parsedArchive);
5941
+ const parsedKind = String(parsed.kind || "").trim().toLowerCase();
5942
+ const conversationID = String(conversationIDRaw || existing.conversation_id || parsed.conversationID || "").trim();
5943
+ const normalizedCurrentBotSelector = normalizeTelegramMentionUsername(
5944
+ currentBotSelector
5945
+ || safeObject(currentState.routes)[String(routeKey || "").trim()]?.last_speaker_bot_username
5946
+ || "",
5947
+ );
5948
+ const sourceMessageEnvelope = selectAuthoritativeRunnerSourceMessageEnvelope(
5949
+ selectAuthoritativeRunnerSourceMessageEnvelope(
5950
+ authoritativeSourceMessageEnvelope,
5951
+ safeObject(existing.source_message_envelope),
5952
+ ),
5953
+ buildRunnerSourceMessageEnvelope({
5954
+ routeState: safeObject(safeObject(currentState.routes)[String(routeKey || "").trim()]),
5955
+ routeKey,
5956
+ normalizedRoute,
5957
+ parsedArchive: parsed,
5958
+ }),
5959
+ );
5960
+ const effectiveReplyToMessageID = intFromRawAllowZero(
5961
+ replyToMessageID,
5962
+ intFromRawAllowZero(existing.last_reply_to_message_id, 0),
5963
+ );
5964
+ const lastReplyMessageEnvelope = buildTelegramBotReplyEnvelope({
5965
+ sourceEnvelope: sourceMessageEnvelope,
5966
+ chatID: existing.chat_id,
5967
+ messageID: lastReplyMessageID,
5968
+ messageThreadID: lastReplyMessageThreadID,
5969
+ replyToMessageID: effectiveReplyToMessageID,
5970
+ senderUsername: normalizedCurrentBotSelector,
5971
+ body: aiReplyPreview,
5972
+ });
5973
+ const normalizedDeliveryStatus = String(deliveryStatus || "").trim().toLowerCase();
5974
+ const persistSuccessfulReplyEnvelope = ["delivered", "dry_run"].includes(normalizedDeliveryStatus)
5975
+ && intFromRawAllowZero(lastReplyMessageEnvelope.message_id, 0) > 0;
5976
+ const attemptedDeliveryEnvelope = buildTelegramBotReplyEnvelope({
5977
+ sourceEnvelope: sourceMessageEnvelope,
5978
+ chatID: existing.chat_id,
5979
+ messageThreadID: lastReplyMessageThreadID,
5980
+ replyToMessageID: effectiveReplyToMessageID,
5981
+ senderUsername: normalizedCurrentBotSelector,
5982
+ body: aiReplyPreview,
5983
+ });
5984
+ const shouldRefreshAttemptedDeliveryEnvelope = (
5985
+ aiReplyGenerated === true
5986
+ || String(aiReplyPreview || "").trim().length > 0
5987
+ || String(deliveryStatus || "").trim().length > 0
5988
+ || String(transportError || "").trim().length > 0
5989
+ || intFromRawAllowZero(replyToMessageID, 0) > 0
5990
+ || intFromRawAllowZero(lastReplyMessageThreadID, 0) > 0
5991
+ );
5992
+ const rootEffectiveExecutionContractTargets = uniqueOrderedStrings(
5993
+ [
5994
+ ...ensureArray(executionContractTargets),
5995
+ ...ensureArray(normalizedExecutionContractTargets),
6002
5996
  ...ensureArray(responseContractValidationTargets),
6003
5997
  ],
6004
5998
  normalizeTelegramMentionUsername,
@@ -6321,11 +6315,11 @@ function markRunnerRequestLifecycle({
6321
6315
  ? transportError || existing.followup_transport_error || ""
6322
6316
  : existing.followup_transport_error || "",
6323
6317
  ).trim(),
6324
- followup_archive_error: String(
6325
- isFollowupComment
6326
- ? archiveError || existing.followup_archive_error || ""
6327
- : existing.followup_archive_error || "",
6328
- ).trim(),
6318
+ followup_archive_error: String(
6319
+ isFollowupComment
6320
+ ? archiveError || existing.followup_archive_error || ""
6321
+ : existing.followup_archive_error || "",
6322
+ ).trim(),
6329
6323
  normalized_intent: nextNormalizedIntent,
6330
6324
  status: nextStatus,
6331
6325
  started_at: firstNonEmptyString([existing.started_at, nowISO]),
@@ -6333,45 +6327,45 @@ function markRunnerRequestLifecycle({
6333
6327
  closed_at: (nextStatus === "closed" || nextStatus === "expired" || nextStatus === "loop_closed")
6334
6328
  ? nowISO
6335
6329
  : String(existing.closed_at || "").trim(),
6336
- closed_reason: (nextStatus === "closed" || nextStatus === "expired" || nextStatus === "loop_closed")
6337
- ? String(closedReason || existing.closed_reason || normalizedOutcome).trim()
6338
- : String(existing.closed_reason || "").trim(),
6339
- source_message_body: parsedKind === "bot_reply"
6340
- ? String(existing.source_message_body || "").trim()
6341
- : String(parsed.body || existing.source_message_body || "").trim(),
6342
- source_message_origin: parsedKind === "bot_reply"
6343
- ? String(existing.source_message_origin || "").trim().toLowerCase()
6344
- : String(sourceMessageEnvelope.source_origin || existing.source_message_origin || "").trim().toLowerCase(),
6345
- source_message_route_key: parsedKind === "bot_reply"
6346
- ? String(existing.source_message_route_key || "").trim()
6347
- : String(sourceMessageEnvelope.source_route_key || existing.source_message_route_key || "").trim(),
6348
- source_message_bot_username: parsedKind === "bot_reply"
6349
- ? normalizeTelegramMentionUsername(existing.source_message_bot_username)
6350
- : normalizeTelegramMentionUsername(
6351
- sourceMessageEnvelope.source_bot_username || existing.source_message_bot_username,
6352
- ),
6353
- source_message_envelope: parsedKind === "bot_reply"
6354
- ? safeObject(existing.source_message_envelope)
6355
- : Object.keys(sourceMessageEnvelope).length
6356
- ? sourceMessageEnvelope
6357
- : safeObject(existing.source_message_envelope),
6358
- last_comment_id: String(selectedRecord?.id || existing.last_comment_id || "").trim(),
6359
- last_comment_kind: String(parsed.kind || existing.last_comment_kind || "").trim().toLowerCase(),
6360
- last_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || existing.last_source_message_id,
6361
- last_source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || existing.last_source_message_thread_id,
6362
- last_reply_message_id: intFromRawAllowZero(lastReplyMessageID, 0) || existing.last_reply_message_id,
6363
- last_reply_message_thread_id: intFromRawAllowZero(lastReplyMessageThreadID, 0) || existing.last_reply_message_thread_id,
6364
- last_reply_to_message_id: intFromRawAllowZero(replyToMessageID, 0) || existing.last_reply_to_message_id,
6365
- last_reply_message_envelope: persistSuccessfulReplyEnvelope
6366
- ? lastReplyMessageEnvelope
6367
- : safeObject(existing.last_reply_message_envelope),
6368
- attempted_delivery_envelope: shouldRefreshAttemptedDeliveryEnvelope
6369
- ? attemptedDeliveryEnvelope
6370
- : safeObject(existing.attempted_delivery_envelope),
6371
- last_reply_fallback_used: replyFallbackUsed === true
6372
- ? true
6373
- : existing.last_reply_fallback_used === true,
6374
- };
6330
+ closed_reason: (nextStatus === "closed" || nextStatus === "expired" || nextStatus === "loop_closed")
6331
+ ? String(closedReason || existing.closed_reason || normalizedOutcome).trim()
6332
+ : String(existing.closed_reason || "").trim(),
6333
+ source_message_body: parsedKind === "bot_reply"
6334
+ ? String(existing.source_message_body || "").trim()
6335
+ : String(parsed.body || existing.source_message_body || "").trim(),
6336
+ source_message_origin: parsedKind === "bot_reply"
6337
+ ? String(existing.source_message_origin || "").trim().toLowerCase()
6338
+ : String(sourceMessageEnvelope.source_origin || existing.source_message_origin || "").trim().toLowerCase(),
6339
+ source_message_route_key: parsedKind === "bot_reply"
6340
+ ? String(existing.source_message_route_key || "").trim()
6341
+ : String(sourceMessageEnvelope.source_route_key || existing.source_message_route_key || "").trim(),
6342
+ source_message_bot_username: parsedKind === "bot_reply"
6343
+ ? normalizeTelegramMentionUsername(existing.source_message_bot_username)
6344
+ : normalizeTelegramMentionUsername(
6345
+ sourceMessageEnvelope.source_bot_username || existing.source_message_bot_username,
6346
+ ),
6347
+ source_message_envelope: parsedKind === "bot_reply"
6348
+ ? safeObject(existing.source_message_envelope)
6349
+ : Object.keys(sourceMessageEnvelope).length
6350
+ ? sourceMessageEnvelope
6351
+ : safeObject(existing.source_message_envelope),
6352
+ last_comment_id: String(selectedRecord?.id || existing.last_comment_id || "").trim(),
6353
+ last_comment_kind: String(parsed.kind || existing.last_comment_kind || "").trim().toLowerCase(),
6354
+ last_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || existing.last_source_message_id,
6355
+ last_source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || existing.last_source_message_thread_id,
6356
+ last_reply_message_id: intFromRawAllowZero(lastReplyMessageID, 0) || existing.last_reply_message_id,
6357
+ last_reply_message_thread_id: intFromRawAllowZero(lastReplyMessageThreadID, 0) || existing.last_reply_message_thread_id,
6358
+ last_reply_to_message_id: intFromRawAllowZero(replyToMessageID, 0) || existing.last_reply_to_message_id,
6359
+ last_reply_message_envelope: persistSuccessfulReplyEnvelope
6360
+ ? lastReplyMessageEnvelope
6361
+ : safeObject(existing.last_reply_message_envelope),
6362
+ attempted_delivery_envelope: shouldRefreshAttemptedDeliveryEnvelope
6363
+ ? attemptedDeliveryEnvelope
6364
+ : safeObject(existing.attempted_delivery_envelope),
6365
+ last_reply_fallback_used: replyFallbackUsed === true
6366
+ ? true
6367
+ : existing.last_reply_fallback_used === true,
6368
+ };
6375
6369
  const { requests: nextRequests, request } = upsertRunnerRequest(currentState, key, patch);
6376
6370
  const commentID = String(selectedRecord?.id || "").trim();
6377
6371
  let nextConsumedComments = currentState.consumedComments || currentState.consumed_comments;
@@ -6547,55 +6541,55 @@ function runnerLedgerEntryMatchesProject(entryRaw, normalizedRoute, requestIndex
6547
6541
  );
6548
6542
  }
6549
6543
 
6550
- function mergeRunnerRequestForServerHydration(localEntryRaw, serverEntryRaw) {
6551
- const localEntry = safeObject(localEntryRaw);
6552
- const serverEntry = safeObject(serverEntryRaw);
6553
- const merged = {
6554
- ...localEntry,
6555
- ...serverEntry,
6556
- };
6557
- const preserveLocalNumberWhenServerMissing = (fieldName) => {
6558
- const serverValue = intFromRawAllowZero(serverEntry[fieldName], 0);
6559
- const localValue = intFromRawAllowZero(localEntry[fieldName], 0);
6560
- if (!(serverValue > 0) && localValue > 0) {
6561
- merged[fieldName] = localValue;
6562
- }
6563
- };
6564
- const preserveLocalStringWhenServerBlank = (fieldName) => {
6565
- const serverValue = String(serverEntry[fieldName] || "").trim();
6566
- const localValue = String(localEntry[fieldName] || "").trim();
6567
- if (!serverValue && localValue) {
6568
- merged[fieldName] = localValue;
6544
+ function mergeRunnerRequestForServerHydration(localEntryRaw, serverEntryRaw) {
6545
+ const localEntry = safeObject(localEntryRaw);
6546
+ const serverEntry = safeObject(serverEntryRaw);
6547
+ const merged = {
6548
+ ...localEntry,
6549
+ ...serverEntry,
6550
+ };
6551
+ const preserveLocalNumberWhenServerMissing = (fieldName) => {
6552
+ const serverValue = intFromRawAllowZero(serverEntry[fieldName], 0);
6553
+ const localValue = intFromRawAllowZero(localEntry[fieldName], 0);
6554
+ if (!(serverValue > 0) && localValue > 0) {
6555
+ merged[fieldName] = localValue;
6556
+ }
6557
+ };
6558
+ const preserveLocalStringWhenServerBlank = (fieldName) => {
6559
+ const serverValue = String(serverEntry[fieldName] || "").trim();
6560
+ const localValue = String(localEntry[fieldName] || "").trim();
6561
+ if (!serverValue && localValue) {
6562
+ merged[fieldName] = localValue;
6569
6563
  }
6570
6564
  };
6571
6565
  const preserveLocalArrayWhenServerEmpty = (fieldName) => {
6572
6566
  const serverValues = ensureArray(serverEntry[fieldName]).filter(Boolean);
6573
6567
  const localValues = ensureArray(localEntry[fieldName]).filter(Boolean);
6574
- if (serverValues.length === 0 && localValues.length > 0) {
6575
- merged[fieldName] = localValues;
6576
- }
6577
- };
6578
- const preserveLocalObjectWhenServerBlank = (fieldName) => {
6579
- const serverValue = safeObject(serverEntry[fieldName]);
6580
- const localValue = safeObject(localEntry[fieldName]);
6581
- if (!Object.keys(serverValue).length && Object.keys(localValue).length) {
6582
- merged[fieldName] = localValue;
6583
- }
6584
- };
6585
- preserveLocalNumberWhenServerMissing("source_message_id");
6586
- preserveLocalNumberWhenServerMissing("source_message_thread_id");
6587
- preserveLocalStringWhenServerBlank("source_message_body");
6588
- preserveLocalStringWhenServerBlank("source_message_origin");
6589
- preserveLocalStringWhenServerBlank("source_message_route_key");
6590
- preserveLocalStringWhenServerBlank("source_message_bot_username");
6591
- preserveLocalObjectWhenServerBlank("source_message_envelope");
6592
- preserveLocalObjectWhenServerBlank("last_reply_message_envelope");
6593
- preserveLocalObjectWhenServerBlank("attempted_delivery_envelope");
6594
- preserveLocalStringWhenServerBlank("conversation_id");
6595
- preserveLocalStringWhenServerBlank("conversation_intent_mode");
6596
- preserveLocalStringWhenServerBlank("conversation_lead_bot");
6597
- preserveLocalStringWhenServerBlank("conversation_summary_bot");
6598
- preserveLocalStringWhenServerBlank("conversation_reply_expectation");
6568
+ if (serverValues.length === 0 && localValues.length > 0) {
6569
+ merged[fieldName] = localValues;
6570
+ }
6571
+ };
6572
+ const preserveLocalObjectWhenServerBlank = (fieldName) => {
6573
+ const serverValue = safeObject(serverEntry[fieldName]);
6574
+ const localValue = safeObject(localEntry[fieldName]);
6575
+ if (!Object.keys(serverValue).length && Object.keys(localValue).length) {
6576
+ merged[fieldName] = localValue;
6577
+ }
6578
+ };
6579
+ preserveLocalNumberWhenServerMissing("source_message_id");
6580
+ preserveLocalNumberWhenServerMissing("source_message_thread_id");
6581
+ preserveLocalStringWhenServerBlank("source_message_body");
6582
+ preserveLocalStringWhenServerBlank("source_message_origin");
6583
+ preserveLocalStringWhenServerBlank("source_message_route_key");
6584
+ preserveLocalStringWhenServerBlank("source_message_bot_username");
6585
+ preserveLocalObjectWhenServerBlank("source_message_envelope");
6586
+ preserveLocalObjectWhenServerBlank("last_reply_message_envelope");
6587
+ preserveLocalObjectWhenServerBlank("attempted_delivery_envelope");
6588
+ preserveLocalStringWhenServerBlank("conversation_id");
6589
+ preserveLocalStringWhenServerBlank("conversation_intent_mode");
6590
+ preserveLocalStringWhenServerBlank("conversation_lead_bot");
6591
+ preserveLocalStringWhenServerBlank("conversation_summary_bot");
6592
+ preserveLocalStringWhenServerBlank("conversation_reply_expectation");
6599
6593
  preserveLocalStringWhenServerBlank("execution_contract_type");
6600
6594
  preserveLocalStringWhenServerBlank("normalized_execution_contract_type");
6601
6595
  preserveLocalStringWhenServerBlank("ai_reply_generated_at");
@@ -6631,16 +6625,16 @@ function mergeRunnerRequestForServerHydration(localEntryRaw, serverEntryRaw) {
6631
6625
  preserveLocalArrayWhenServerEmpty("root_execution_contract_targets");
6632
6626
  preserveLocalArrayWhenServerEmpty("root_next_expected_responders");
6633
6627
  preserveLocalArrayWhenServerEmpty("root_response_contract_validation_targets");
6634
- preserveLocalArrayWhenServerEmpty("followup_execution_contract_targets");
6635
- preserveLocalArrayWhenServerEmpty("followup_next_expected_responders");
6636
- preserveLocalArrayWhenServerEmpty("followup_normalized_execution_contract_targets");
6637
- preserveLocalArrayWhenServerEmpty("followup_normalized_execution_next_responders");
6638
- preserveLocalArrayWhenServerEmpty("followup_response_contract_validation_targets");
6639
- preserveLocalArrayWhenServerEmpty("followup_assignment_validation_modes");
6640
- preserveLocalObjectWhenServerBlank("reply_chain_context");
6641
- if (serverEntry.conversation_allow_bot_to_bot !== true && localEntry.conversation_allow_bot_to_bot === true) {
6642
- merged.conversation_allow_bot_to_bot = true;
6643
- }
6628
+ preserveLocalArrayWhenServerEmpty("followup_execution_contract_targets");
6629
+ preserveLocalArrayWhenServerEmpty("followup_next_expected_responders");
6630
+ preserveLocalArrayWhenServerEmpty("followup_normalized_execution_contract_targets");
6631
+ preserveLocalArrayWhenServerEmpty("followup_normalized_execution_next_responders");
6632
+ preserveLocalArrayWhenServerEmpty("followup_response_contract_validation_targets");
6633
+ preserveLocalArrayWhenServerEmpty("followup_assignment_validation_modes");
6634
+ preserveLocalObjectWhenServerBlank("reply_chain_context");
6635
+ if (serverEntry.conversation_allow_bot_to_bot !== true && localEntry.conversation_allow_bot_to_bot === true) {
6636
+ merged.conversation_allow_bot_to_bot = true;
6637
+ }
6644
6638
  if (serverEntry.execution_contract_actionable !== true && localEntry.execution_contract_actionable === true) {
6645
6639
  merged.execution_contract_actionable = true;
6646
6640
  }
@@ -7895,16 +7889,16 @@ function parseArchivedChatComment(rawBody) {
7895
7889
  .filter(Boolean),
7896
7890
  }
7897
7891
  : null;
7898
- return {
7899
- kind,
7900
- header,
7901
- metadata,
7902
- body,
7903
- chatID: String(metadata.chat_id || "").trim(),
7904
- chatType: String(metadata.chat_type || "").trim().toLowerCase(),
7905
- messageID: intFromRawAllowZero(metadata.message_id, 0),
7906
- messageThreadID: intFromRawAllowZero(metadata.message_thread_id, 0),
7907
- senderID: String(metadata.sender_id || "").trim(),
7892
+ return {
7893
+ kind,
7894
+ header,
7895
+ metadata,
7896
+ body,
7897
+ chatID: String(metadata.chat_id || "").trim(),
7898
+ chatType: String(metadata.chat_type || "").trim().toLowerCase(),
7899
+ messageID: intFromRawAllowZero(metadata.message_id, 0),
7900
+ messageThreadID: intFromRawAllowZero(metadata.message_thread_id, 0),
7901
+ senderID: String(metadata.sender_id || "").trim(),
7908
7902
  sender: String(metadata.sender || metadata.bot_username || metadata.bot_name || "").trim(),
7909
7903
  senderIsBot: kind === "bot_reply" ? true : boolFromRaw(metadata.sender_is_bot, false),
7910
7904
  username: String(metadata.telegram_username || metadata.bot_username || metadata.bot_name || "").trim(),
@@ -7912,11 +7906,11 @@ function parseArchivedChatComment(rawBody) {
7912
7906
  .split(",")
7913
7907
  .map((value) => normalizeTelegramMentionUsername(value))
7914
7908
  .filter(Boolean),
7915
- occurredAt: String(metadata.occurred_at || "").trim(),
7916
- sourceOrigin: String(metadata.source_origin || "").trim().toLowerCase(),
7917
- sourceRouteKey: String(metadata.source_route_key || "").trim(),
7918
- sourceBotUsername: normalizeTelegramMentionUsername(metadata.source_bot_username || ""),
7919
- replyToMessageID: intFromRawAllowZero(metadata.reply_to_message_id, 0),
7909
+ occurredAt: String(metadata.occurred_at || "").trim(),
7910
+ sourceOrigin: String(metadata.source_origin || "").trim().toLowerCase(),
7911
+ sourceRouteKey: String(metadata.source_route_key || "").trim(),
7912
+ sourceBotUsername: normalizeTelegramMentionUsername(metadata.source_bot_username || ""),
7913
+ replyToMessageID: intFromRawAllowZero(metadata.reply_to_message_id, 0),
7920
7914
  replyToSender: String(metadata.reply_to_sender || "").trim(),
7921
7915
  replyToUsername: String(metadata.reply_to_telegram_username || "").trim(),
7922
7916
  replyToSenderIsBot: boolFromRaw(metadata.reply_to_sender_is_bot, false),
@@ -8163,23 +8157,23 @@ function extractTelegramEntityText(text, entity) {
8163
8157
  return body.slice(offset, offset + length);
8164
8158
  }
8165
8159
 
8166
- function extractTelegramMentionUsernames(text, entities) {
8167
- const set = new Set();
8168
- for (const entityRaw of ensureArray(entities)) {
8169
- const entity = safeObject(entityRaw);
8160
+ function extractTelegramMentionUsernames(text, entities) {
8161
+ const set = new Set();
8162
+ for (const entityRaw of ensureArray(entities)) {
8163
+ const entity = safeObject(entityRaw);
8170
8164
  const type = String(entity.type || "").trim().toLowerCase();
8171
8165
  if (type === "mention") {
8172
8166
  const username = normalizeTelegramMentionUsername(extractTelegramEntityText(text, entity));
8173
8167
  if (username) set.add(username);
8174
8168
  continue;
8175
8169
  }
8176
- if (type === "text_mention") {
8177
- const username = normalizeTelegramMentionUsername(entity.user?.username);
8178
- if (username) set.add(username);
8179
- }
8180
- }
8181
- return Array.from(set);
8182
- }
8170
+ if (type === "text_mention") {
8171
+ const username = normalizeTelegramMentionUsername(entity.user?.username);
8172
+ if (username) set.add(username);
8173
+ }
8174
+ }
8175
+ return Array.from(set);
8176
+ }
8183
8177
 
8184
8178
  function normalizeLocalTelegramUpdate(rawUpdate) {
8185
8179
  const update = safeObject(rawUpdate);
@@ -8225,47 +8219,47 @@ function buildArchivedInboundMessageKey(chatID, messageID) {
8225
8219
  return `${String(chatID || "").trim()}:${intFromRawAllowZero(messageID, 0)}`;
8226
8220
  }
8227
8221
 
8228
- function formatTelegramInboundArchiveComment(normalized) {
8229
- const headerLines = [
8222
+ function formatTelegramInboundArchiveComment(normalized) {
8223
+ const headerLines = [
8230
8224
  `[Telegram ${normalized.eventName === "telegram.message.updated" ? "edited" : "message"}]`,
8231
8225
  `chat_id: ${normalized.chatID || "<missing>"}`,
8232
8226
  `chat_type: ${normalized.chatType || "unknown"}`,
8233
8227
  `message_id: ${normalized.messageID || "<missing>"}`,
8234
8228
  `occurred_at: ${normalized.occurredAt || new Date().toISOString()}`,
8235
8229
  `sender_id: ${normalized.fromID || "<missing>"}`,
8236
- `sender: ${normalized.fromName || normalized.fromUsername || normalized.fromID || "unknown"}`,
8237
- `sender_is_bot: ${normalized.fromIsBot ? "true" : "false"}`,
8238
- ];
8239
- if (String(normalized.sourceOrigin || "").trim()) {
8240
- headerLines.push(`source_origin: ${String(normalized.sourceOrigin || "").trim()}`);
8241
- }
8242
- if (String(normalized.sourceRouteKey || "").trim()) {
8243
- headerLines.push(`source_route_key: ${String(normalized.sourceRouteKey || "").trim()}`);
8244
- }
8245
- if (String(normalized.sourceBotUsername || "").trim()) {
8246
- headerLines.push(`source_bot_username: @${String(normalized.sourceBotUsername || "").trim().replace(/^@+/, "")}`);
8247
- }
8248
- if (normalized.fromUsername) {
8230
+ `sender: ${normalized.fromName || normalized.fromUsername || normalized.fromID || "unknown"}`,
8231
+ `sender_is_bot: ${normalized.fromIsBot ? "true" : "false"}`,
8232
+ ];
8233
+ if (String(normalized.sourceOrigin || "").trim()) {
8234
+ headerLines.push(`source_origin: ${String(normalized.sourceOrigin || "").trim()}`);
8235
+ }
8236
+ if (String(normalized.sourceRouteKey || "").trim()) {
8237
+ headerLines.push(`source_route_key: ${String(normalized.sourceRouteKey || "").trim()}`);
8238
+ }
8239
+ if (String(normalized.sourceBotUsername || "").trim()) {
8240
+ headerLines.push(`source_bot_username: @${String(normalized.sourceBotUsername || "").trim().replace(/^@+/, "")}`);
8241
+ }
8242
+ if (normalized.fromUsername) {
8249
8243
  headerLines.push(`telegram_username: @${normalized.fromUsername.replace(/^@+/, "")}`);
8250
8244
  }
8251
8245
  if (normalized.mentionUsernames.length > 0) {
8252
8246
  headerLines.push(`mention_usernames: ${normalized.mentionUsernames.map((item) => `@${item}`).join(", ")}`);
8253
8247
  }
8254
- if (normalized.replyToMessageID > 0) {
8255
- headerLines.push(`reply_to_message_id: ${normalized.replyToMessageID}`);
8256
- headerLines.push(`reply_to_sender_is_bot: ${normalized.replyToFromIsBot ? "true" : "false"}`);
8248
+ if (normalized.replyToMessageID > 0) {
8249
+ headerLines.push(`reply_to_message_id: ${normalized.replyToMessageID}`);
8250
+ headerLines.push(`reply_to_sender_is_bot: ${normalized.replyToFromIsBot ? "true" : "false"}`);
8257
8251
  if (normalized.replyToFromName) {
8258
8252
  headerLines.push(`reply_to_sender: ${normalized.replyToFromName}`);
8259
8253
  }
8260
8254
  if (normalized.replyToFromUsername) {
8261
- headerLines.push(`reply_to_telegram_username: @${normalized.replyToFromUsername.replace(/^@+/, "")}`);
8262
- }
8263
- }
8264
- if (intFromRawAllowZero(normalized.messageThreadID, 0) > 0) {
8265
- headerLines.push(`message_thread_id: ${intFromRawAllowZero(normalized.messageThreadID, 0)}`);
8266
- }
8267
- return `${headerLines.join("\n")}\n\n${String(normalized.text || "").trim()}`;
8268
- }
8255
+ headerLines.push(`reply_to_telegram_username: @${normalized.replyToFromUsername.replace(/^@+/, "")}`);
8256
+ }
8257
+ }
8258
+ if (intFromRawAllowZero(normalized.messageThreadID, 0) > 0) {
8259
+ headerLines.push(`message_thread_id: ${intFromRawAllowZero(normalized.messageThreadID, 0)}`);
8260
+ }
8261
+ return `${headerLines.join("\n")}\n\n${String(normalized.text || "").trim()}`;
8262
+ }
8269
8263
 
8270
8264
  async function postJSONWithAuthHeaders(urlText, timeoutSeconds, token, payload, extraHeaders = {}) {
8271
8265
  return new Promise((resolve, reject) => {
@@ -9043,25 +9037,25 @@ function summarizeRunnerRequestForStatusLookup(entryRaw) {
9043
9037
  updated_at: String(entry.updated_at || "").trim(),
9044
9038
  source_message_id: intFromRawAllowZero(entry.source_message_id, 0) || undefined,
9045
9039
  last_source_message_id: intFromRawAllowZero(entry.last_source_message_id, 0) || undefined,
9046
- ...buildRunnerValidationAndDeliverySummary({
9047
- aiReplyPreview: runnerRequestPreferredAIReplyPreview(entry),
9048
- executionContractType: runnerRequestPreferredExecutionContractType(entry),
9049
- executionContractTargets: runnerRequestPreferredExecutionContractTargets(entry),
9050
- nextExpectedResponders: runnerRequestPreferredNextExpectedResponders(entry),
9040
+ ...buildRunnerValidationAndDeliverySummary({
9041
+ aiReplyPreview: runnerRequestPreferredAIReplyPreview(entry),
9042
+ executionContractType: runnerRequestPreferredExecutionContractType(entry),
9043
+ executionContractTargets: runnerRequestPreferredExecutionContractTargets(entry),
9044
+ nextExpectedResponders: runnerRequestPreferredNextExpectedResponders(entry),
9051
9045
  responseContractValidationStatus: runnerRequestPreferredResponseContractValidationStatus(entry),
9052
9046
  responseContractValidationReason: runnerRequestPreferredResponseContractValidationReason(entry),
9053
9047
  responseContractValidationTargets: runnerRequestPreferredResponseContractValidationTargets(entry),
9054
9048
  assignmentValidationStatus: runnerRequestPreferredAssignmentValidationStatus(entry),
9055
9049
  assignmentValidationReason: runnerRequestPreferredAssignmentValidationReason(entry),
9056
9050
  assignmentValidationModes: runnerRequestPreferredAssignmentValidationModes(entry),
9057
- deliveryStatus: runnerRequestPreferredDeliveryStatus(entry),
9058
- archiveStatus: runnerRequestPreferredArchiveStatus(entry),
9059
- transportError: runnerRequestPreferredTransportError(entry),
9060
- archiveError: runnerRequestPreferredArchiveError(entry),
9061
- sourceMessageEnvelope: entry.source_message_envelope,
9062
- lastReplyMessageEnvelope: entry.last_reply_message_envelope,
9063
- attemptedDeliveryEnvelope: entry.attempted_delivery_envelope,
9064
- }),
9051
+ deliveryStatus: runnerRequestPreferredDeliveryStatus(entry),
9052
+ archiveStatus: runnerRequestPreferredArchiveStatus(entry),
9053
+ transportError: runnerRequestPreferredTransportError(entry),
9054
+ archiveError: runnerRequestPreferredArchiveError(entry),
9055
+ sourceMessageEnvelope: entry.source_message_envelope,
9056
+ lastReplyMessageEnvelope: entry.last_reply_message_envelope,
9057
+ attemptedDeliveryEnvelope: entry.attempted_delivery_envelope,
9058
+ }),
9065
9059
  selected_bot_usernames: ensureArray(entry.selected_bot_usernames)
9066
9060
  .map((value) => normalizeTelegramMentionUsername(value))
9067
9061
  .filter(Boolean),
@@ -9169,16 +9163,16 @@ function resolveRunnerStatusLookupRequests({
9169
9163
  };
9170
9164
  }
9171
9165
 
9172
- function buildRunnerShowLastRunPayload(lastRunSummaryRaw) {
9173
- const lastRunSummary = safeObject(lastRunSummaryRaw);
9174
- return {
9175
- action: lastRunSummary.action || "-",
9176
- reason: lastRunSummary.reason || "-",
9177
- visibility_status: lastRunSummary.visibility_status || "-",
9178
- visibility_reason: lastRunSummary.visibility_reason || "-",
9179
- visibility_source: lastRunSummary.visibility_source || "-",
9180
- candidate_bot_usernames: ensureArray(lastRunSummary.candidate_bot_usernames),
9181
- intent_type: lastRunSummary.intent_type || "-",
9166
+ function buildRunnerShowLastRunPayload(lastRunSummaryRaw) {
9167
+ const lastRunSummary = safeObject(lastRunSummaryRaw);
9168
+ return {
9169
+ action: lastRunSummary.action || "-",
9170
+ reason: lastRunSummary.reason || "-",
9171
+ visibility_status: lastRunSummary.visibility_status || "-",
9172
+ visibility_reason: lastRunSummary.visibility_reason || "-",
9173
+ visibility_source: lastRunSummary.visibility_source || "-",
9174
+ candidate_bot_usernames: ensureArray(lastRunSummary.candidate_bot_usernames),
9175
+ intent_type: lastRunSummary.intent_type || "-",
9182
9176
  ai_reply_preview: lastRunSummary.ai_reply_preview || "-",
9183
9177
  execution_contract_type: lastRunSummary.execution_contract_type || "-",
9184
9178
  execution_contract_targets: ensureArray(lastRunSummary.execution_contract_targets),
@@ -9191,13 +9185,13 @@ function buildRunnerShowLastRunPayload(lastRunSummaryRaw) {
9191
9185
  assignment_validation_modes: ensureArray(lastRunSummary.assignment_validation_modes),
9192
9186
  delivery_status: lastRunSummary.delivery_status || "-",
9193
9187
  archive_status: lastRunSummary.archive_status || "-",
9194
- transport_error: lastRunSummary.transport_error || "-",
9195
- archive_error: lastRunSummary.archive_error || "-",
9196
- source_message_envelope: safeObject(lastRunSummary.source_message_envelope),
9197
- last_reply_message_envelope: safeObject(lastRunSummary.last_reply_message_envelope),
9198
- attempted_delivery_envelope: safeObject(lastRunSummary.attempted_delivery_envelope),
9199
- workspace_dir: lastRunSummary.workspace_dir || "-",
9200
- artifact_validation: lastRunSummary.artifact_validation || "-",
9188
+ transport_error: lastRunSummary.transport_error || "-",
9189
+ archive_error: lastRunSummary.archive_error || "-",
9190
+ source_message_envelope: safeObject(lastRunSummary.source_message_envelope),
9191
+ last_reply_message_envelope: safeObject(lastRunSummary.last_reply_message_envelope),
9192
+ attempted_delivery_envelope: safeObject(lastRunSummary.attempted_delivery_envelope),
9193
+ workspace_dir: lastRunSummary.workspace_dir || "-",
9194
+ artifact_validation: lastRunSummary.artifact_validation || "-",
9201
9195
  artifact_paths: ensureArray(lastRunSummary.artifact_paths),
9202
9196
  artifact_errors: ensureArray(lastRunSummary.artifact_errors),
9203
9197
  boundary_violations: ensureArray(lastRunSummary.boundary_violations),
@@ -9513,16 +9507,16 @@ function buildRunnerExecutionDeps() {
9513
9507
  };
9514
9508
  }
9515
9509
 
9516
- function buildRunnerRuntimeDeps() {
9517
- return {
9518
- loadProviderEnvConfig,
9519
- loadBotRunnerState,
9520
- saveBotRunnerState,
9521
- getTelegramBotMe,
9522
- getTelegramWebhookInfo,
9523
- deleteTelegramWebhook,
9524
- saveRunnerRouteState,
9525
- getTelegramUpdates,
9510
+ function buildRunnerRuntimeDeps() {
9511
+ return {
9512
+ loadProviderEnvConfig,
9513
+ loadBotRunnerState,
9514
+ saveBotRunnerState,
9515
+ getTelegramBotMe,
9516
+ getTelegramWebhookInfo,
9517
+ deleteTelegramWebhook,
9518
+ saveRunnerRouteState,
9519
+ getTelegramUpdates,
9526
9520
  normalizeLocalTelegramUpdate,
9527
9521
  listThreadComments,
9528
9522
  listThreadCommentsTail,
@@ -10153,12 +10147,12 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10153
10147
  role_profile: executionPlan.roleProfileName,
10154
10148
  };
10155
10149
  };
10156
- const prepareRunnerRequestClaim = async (
10157
- selectedRecord,
10158
- selectedResponderSelectors = [],
10159
- sharedHumanIntent = null,
10160
- authoritativeSourceMessageEnvelope = {},
10161
- ) => {
10150
+ const prepareRunnerRequestClaim = async (
10151
+ selectedRecord,
10152
+ selectedResponderSelectors = [],
10153
+ sharedHumanIntent = null,
10154
+ authoritativeSourceMessageEnvelope = {},
10155
+ ) => {
10162
10156
  const parsed = safeObject(selectedRecord?.parsedArchive);
10163
10157
  const kind = String(parsed.kind || "").trim().toLowerCase();
10164
10158
  if (kind === "bot_reply") {
@@ -10183,18 +10177,18 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10183
10177
  }
10184
10178
  return continuation;
10185
10179
  }
10186
- return claimRunnerRequestForHumanComment({
10187
- normalizedRoute,
10188
- routeKey,
10189
- selectedRecord,
10190
- selectedBotUsernames: selectedResponderSelectors,
10191
- normalizedIntent: String(safeObject(sharedHumanIntent).intentType || "").trim().toLowerCase(),
10192
- sharedHumanIntent,
10193
- runtime,
10194
- archiveThreadID: archiveThread.threadID,
10195
- authoritativeSourceMessageEnvelope,
10196
- });
10197
- };
10180
+ return claimRunnerRequestForHumanComment({
10181
+ normalizedRoute,
10182
+ routeKey,
10183
+ selectedRecord,
10184
+ selectedBotUsernames: selectedResponderSelectors,
10185
+ normalizedIntent: String(safeObject(sharedHumanIntent).intentType || "").trim().toLowerCase(),
10186
+ sharedHumanIntent,
10187
+ runtime,
10188
+ archiveThreadID: archiveThread.threadID,
10189
+ authoritativeSourceMessageEnvelope,
10190
+ });
10191
+ };
10198
10192
  const buildContinuationResponderAdjudication = (selectedRecord, requestRaw) => {
10199
10193
  const request = safeObject(requestRaw);
10200
10194
  const referencedBotUsernames = ensureArray(safeObject(selectedRecord?.parsedArchive).mentionUsernames)
@@ -10228,11 +10222,11 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10228
10222
  clarification: "",
10229
10223
  };
10230
10224
  };
10231
- const finalizePreparedRunnerRequest = async (
10232
- selectedRecord,
10233
- requestClaim,
10234
- authoritativeSourceMessageEnvelope = {},
10235
- ) => {
10225
+ const finalizePreparedRunnerRequest = async (
10226
+ selectedRecord,
10227
+ requestClaim,
10228
+ authoritativeSourceMessageEnvelope = {},
10229
+ ) => {
10236
10230
  const inheritedRootReference = await inheritRunnerReferenceRootWorkItemForRequest({
10237
10231
  normalizedRoute,
10238
10232
  selectedRecord,
@@ -10259,20 +10253,20 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10259
10253
  || rootWorkItemClaim.reason
10260
10254
  || (missingRequiredRootWorkItem ? "root_work_item_missing" : "root_work_item_create_failed"),
10261
10255
  ).trim() || "root_work_item_create_failed";
10262
- if (String(requestClaim.requestKey || "").trim()) {
10263
- markRunnerRequestLifecycle({
10264
- normalizedRoute,
10265
- requestKey: requestClaim.requestKey,
10266
- selectedRecord,
10267
- routeKey,
10268
- outcome: "closed",
10269
- closedReason: rootWorkItemFailure,
10270
- authoritativeSourceMessageEnvelope,
10271
- });
10272
- await syncRunnerRequestLedgerForProjectToServer({
10273
- normalizedRoute,
10274
- runtime,
10275
- });
10256
+ if (String(requestClaim.requestKey || "").trim()) {
10257
+ markRunnerRequestLifecycle({
10258
+ normalizedRoute,
10259
+ requestKey: requestClaim.requestKey,
10260
+ selectedRecord,
10261
+ routeKey,
10262
+ outcome: "closed",
10263
+ closedReason: rootWorkItemFailure,
10264
+ authoritativeSourceMessageEnvelope,
10265
+ });
10266
+ await syncRunnerRequestLedgerForProjectToServer({
10267
+ normalizedRoute,
10268
+ runtime,
10269
+ });
10276
10270
  }
10277
10271
  return {
10278
10272
  ok: false,
@@ -10292,109 +10286,109 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10292
10286
  },
10293
10287
  };
10294
10288
  }
10295
- return {
10296
- ok: true,
10297
- claimedRequest,
10298
- };
10299
- };
10300
- const prepareRunnerBotReplyAuthorityContext = async (selectedRecord, routingExecutionDeps, triggerDecision) => {
10301
- const requestClaim = await prepareRunnerRequestClaim(selectedRecord);
10302
- if (!requestClaim.ok) {
10303
- const requestFailureReason = String(requestClaim.reason || "").trim();
10304
- if (requestFailureReason === "bot_reply_without_active_request") {
10305
- const conversationContext = await resolvePublicConversationContext({
10306
- selectedRecord,
10307
- pendingOrdered: pending.ordered,
10308
- normalizedRoute,
10309
- bot,
10310
- routeState: safeObject(refreshedState.routes?.[routeKey]),
10311
- executionPlan,
10312
- deps: routingExecutionDeps,
10313
- });
10314
- const conversationAdjudication = buildRunnerResponderAdjudicationFromConversationContext({
10315
- selectedRecord,
10316
- conversationContext,
10317
- currentBotSelector,
10318
- });
10319
- const currentBotSelectedFromConversation = ensureArray(conversationAdjudication?.selected_bot_usernames)
10320
- .map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase())
10321
- .includes(currentBotSelector);
10322
- if (currentBotSelectedFromConversation) {
10323
- return {
10324
- ok: true,
10325
- triggerDecision,
10326
- routingExecutionDeps,
10327
- sharedHumanIntentContext: null,
10328
- adjudication: conversationAdjudication,
10329
- requestClaim: {
10330
- ok: true,
10331
- requestKey: "",
10332
- request: {},
10333
- },
10334
- claimedRequest: {},
10335
- };
10336
- }
10337
- }
10338
- await syncRunnerRequestLedgerForProjectToServer({
10339
- normalizedRoute,
10340
- runtime,
10341
- });
10342
- return {
10343
- ok: false,
10344
- skip: {
10345
- kind: "skip",
10346
- skipAction: "request_skipped",
10347
- skipReason: requestFailureReason || "request_unavailable",
10348
- skipTrigger: "request_ledger",
10349
- skipRecordPatch: {
10350
- diagnosticType: "skip",
10351
- contextExcluded: requestFailureReason === "bot_reply_without_active_request",
10352
- action: "skip_invalid_request_state",
10353
- closedReason: requestFailureReason,
10354
- },
10355
- },
10356
- };
10357
- }
10358
- const continuationAdjudication = buildContinuationResponderAdjudication(
10359
- selectedRecord,
10360
- requestClaim.request,
10361
- );
10362
- const currentBotSelected = ensureArray(continuationAdjudication.selected_bot_usernames)
10363
- .map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase())
10364
- .includes(currentBotSelector);
10365
- if (!currentBotSelected) {
10366
- return {
10367
- ok: false,
10368
- skip: {
10369
- kind: "skip",
10370
- skipAction: "adjudication_skipped",
10371
- skipReason: String(continuationAdjudication.reason_code || "").trim() || "not_selected_by_request_contract",
10372
- skipTrigger: "request_contract",
10373
- },
10374
- };
10375
- }
10376
- const finalizedRequest = await finalizePreparedRunnerRequest(
10377
- selectedRecord,
10378
- requestClaim,
10379
- );
10380
- if (!finalizedRequest.ok) {
10381
- return {
10382
- ok: false,
10383
- skip: finalizedRequest.skip,
10384
- };
10385
- }
10386
- return {
10387
- ok: true,
10388
- triggerDecision,
10389
- routingExecutionDeps,
10390
- sharedHumanIntentContext: null,
10391
- adjudication: continuationAdjudication,
10392
- requestClaim,
10393
- claimedRequest: finalizedRequest.claimedRequest,
10394
- };
10395
- };
10396
- const prepareRunnerSelectedRecordForExecution = async (selectedRecord) => {
10397
- const selectedRecordKind = String(safeObject(selectedRecord?.parsedArchive).kind || "").trim().toLowerCase();
10289
+ return {
10290
+ ok: true,
10291
+ claimedRequest,
10292
+ };
10293
+ };
10294
+ const prepareRunnerBotReplyAuthorityContext = async (selectedRecord, routingExecutionDeps, triggerDecision) => {
10295
+ const requestClaim = await prepareRunnerRequestClaim(selectedRecord);
10296
+ if (!requestClaim.ok) {
10297
+ const requestFailureReason = String(requestClaim.reason || "").trim();
10298
+ if (requestFailureReason === "bot_reply_without_active_request") {
10299
+ const conversationContext = await resolvePublicConversationContext({
10300
+ selectedRecord,
10301
+ pendingOrdered: pending.ordered,
10302
+ normalizedRoute,
10303
+ bot,
10304
+ routeState: safeObject(refreshedState.routes?.[routeKey]),
10305
+ executionPlan,
10306
+ deps: routingExecutionDeps,
10307
+ });
10308
+ const conversationAdjudication = buildRunnerResponderAdjudicationFromConversationContext({
10309
+ selectedRecord,
10310
+ conversationContext,
10311
+ currentBotSelector,
10312
+ });
10313
+ const currentBotSelectedFromConversation = ensureArray(conversationAdjudication?.selected_bot_usernames)
10314
+ .map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase())
10315
+ .includes(currentBotSelector);
10316
+ if (currentBotSelectedFromConversation) {
10317
+ return {
10318
+ ok: true,
10319
+ triggerDecision,
10320
+ routingExecutionDeps,
10321
+ sharedHumanIntentContext: null,
10322
+ adjudication: conversationAdjudication,
10323
+ requestClaim: {
10324
+ ok: true,
10325
+ requestKey: "",
10326
+ request: {},
10327
+ },
10328
+ claimedRequest: {},
10329
+ };
10330
+ }
10331
+ }
10332
+ await syncRunnerRequestLedgerForProjectToServer({
10333
+ normalizedRoute,
10334
+ runtime,
10335
+ });
10336
+ return {
10337
+ ok: false,
10338
+ skip: {
10339
+ kind: "skip",
10340
+ skipAction: "request_skipped",
10341
+ skipReason: requestFailureReason || "request_unavailable",
10342
+ skipTrigger: "request_ledger",
10343
+ skipRecordPatch: {
10344
+ diagnosticType: "skip",
10345
+ contextExcluded: requestFailureReason === "bot_reply_without_active_request",
10346
+ action: "skip_invalid_request_state",
10347
+ closedReason: requestFailureReason,
10348
+ },
10349
+ },
10350
+ };
10351
+ }
10352
+ const continuationAdjudication = buildContinuationResponderAdjudication(
10353
+ selectedRecord,
10354
+ requestClaim.request,
10355
+ );
10356
+ const currentBotSelected = ensureArray(continuationAdjudication.selected_bot_usernames)
10357
+ .map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase())
10358
+ .includes(currentBotSelector);
10359
+ if (!currentBotSelected) {
10360
+ return {
10361
+ ok: false,
10362
+ skip: {
10363
+ kind: "skip",
10364
+ skipAction: "adjudication_skipped",
10365
+ skipReason: String(continuationAdjudication.reason_code || "").trim() || "not_selected_by_request_contract",
10366
+ skipTrigger: "request_contract",
10367
+ },
10368
+ };
10369
+ }
10370
+ const finalizedRequest = await finalizePreparedRunnerRequest(
10371
+ selectedRecord,
10372
+ requestClaim,
10373
+ );
10374
+ if (!finalizedRequest.ok) {
10375
+ return {
10376
+ ok: false,
10377
+ skip: finalizedRequest.skip,
10378
+ };
10379
+ }
10380
+ return {
10381
+ ok: true,
10382
+ triggerDecision,
10383
+ routingExecutionDeps,
10384
+ sharedHumanIntentContext: null,
10385
+ adjudication: continuationAdjudication,
10386
+ requestClaim,
10387
+ claimedRequest: finalizedRequest.claimedRequest,
10388
+ };
10389
+ };
10390
+ const prepareRunnerSelectedRecordForExecution = async (selectedRecord) => {
10391
+ const selectedRecordKind = String(safeObject(selectedRecord?.parsedArchive).kind || "").trim().toLowerCase();
10398
10392
  const duplicateArchivedSkip = maybeBuildDuplicateArchivedSkip(selectedRecord, refreshedState);
10399
10393
  if (duplicateArchivedSkip) {
10400
10394
  return {
@@ -10447,68 +10441,68 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10447
10441
  ? currentBotSelector !== representativeBotSelector
10448
10442
  : false,
10449
10443
  });
10450
- if (startupLoopSkipped) {
10451
- return {
10452
- kind: "startup_loop_skipped",
10453
- skippedRecord: startupLoopSkipped.skippedRecord,
10454
- };
10455
- }
10456
- const visibilityRouteState = safeObject(loadBotRunnerState().routes[routeKey]);
10457
- const humanInboundVisibility = resolveRunnerHumanInboundVisibility({
10458
- routeState: Object.keys(visibilityRouteState).length > 0
10459
- ? visibilityRouteState
10460
- : refreshedState,
10461
- persistedRequest: null,
10462
- selectedRecord,
10463
- routeKey,
10464
- currentBotSelector,
10465
- triggerDecision,
10466
- });
10467
- const visibilityStatePatch = humanInboundVisibility.applies
10468
- ? {
10469
- last_visibility_status: String(humanInboundVisibility.visibilityStatus || "").trim(),
10470
- last_visibility_reason: String(humanInboundVisibility.visibilityReason || "").trim(),
10471
- last_visibility_source: String(humanInboundVisibility.visibilitySource || "").trim(),
10472
- last_candidate_bot_usernames: ensureArray(humanInboundVisibility.candidateBotUsernames),
10473
- }
10474
- : {};
10475
- if (humanInboundVisibility.applies && humanInboundVisibility.executable !== true) {
10476
- return {
10477
- kind: "skip",
10478
- skipAction: "visibility_skipped",
10479
- skipReason: String(
10480
- humanInboundVisibility.visibilityReason || "telegram human inbound is not local to this route",
10481
- ).trim() || "telegram human inbound is not local to this route",
10482
- skipTrigger: "local_inbound_visibility",
10483
- skipStatePatch: visibilityStatePatch,
10484
- };
10485
- }
10486
- const routingExecutionDeps = {
10487
- ...buildRunnerExecutionDeps(),
10488
- managedConversationBots,
10489
- resolveConversationPeerBots: resolveRunnerConversationPeers,
10490
- };
10491
- if (selectedRecordKind === "bot_reply") {
10492
- const botReplyAuthorityContext = await prepareRunnerBotReplyAuthorityContext(
10493
- selectedRecord,
10494
- routingExecutionDeps,
10495
- triggerDecision,
10496
- );
10497
- if (!botReplyAuthorityContext.ok) {
10498
- return botReplyAuthorityContext.skip;
10499
- }
10500
- return {
10501
- kind: "ready",
10502
- triggerDecision: botReplyAuthorityContext.triggerDecision,
10503
- routingExecutionDeps: botReplyAuthorityContext.routingExecutionDeps,
10504
- sharedHumanIntentContext: botReplyAuthorityContext.sharedHumanIntentContext,
10505
- adjudication: botReplyAuthorityContext.adjudication,
10506
- requestClaim: botReplyAuthorityContext.requestClaim,
10507
- claimedRequest: botReplyAuthorityContext.claimedRequest,
10508
- humanInboundVisibility,
10509
- visibilityStatePatch,
10510
- };
10511
- }
10444
+ if (startupLoopSkipped) {
10445
+ return {
10446
+ kind: "startup_loop_skipped",
10447
+ skippedRecord: startupLoopSkipped.skippedRecord,
10448
+ };
10449
+ }
10450
+ const visibilityRouteState = safeObject(loadBotRunnerState().routes[routeKey]);
10451
+ const humanInboundVisibility = resolveRunnerHumanInboundVisibility({
10452
+ routeState: Object.keys(visibilityRouteState).length > 0
10453
+ ? visibilityRouteState
10454
+ : refreshedState,
10455
+ persistedRequest: null,
10456
+ selectedRecord,
10457
+ routeKey,
10458
+ currentBotSelector,
10459
+ triggerDecision,
10460
+ });
10461
+ const visibilityStatePatch = humanInboundVisibility.applies
10462
+ ? {
10463
+ last_visibility_status: String(humanInboundVisibility.visibilityStatus || "").trim(),
10464
+ last_visibility_reason: String(humanInboundVisibility.visibilityReason || "").trim(),
10465
+ last_visibility_source: String(humanInboundVisibility.visibilitySource || "").trim(),
10466
+ last_candidate_bot_usernames: ensureArray(humanInboundVisibility.candidateBotUsernames),
10467
+ }
10468
+ : {};
10469
+ if (humanInboundVisibility.applies && humanInboundVisibility.executable !== true) {
10470
+ return {
10471
+ kind: "skip",
10472
+ skipAction: "visibility_skipped",
10473
+ skipReason: String(
10474
+ humanInboundVisibility.visibilityReason || "telegram human inbound is not local to this route",
10475
+ ).trim() || "telegram human inbound is not local to this route",
10476
+ skipTrigger: "local_inbound_visibility",
10477
+ skipStatePatch: visibilityStatePatch,
10478
+ };
10479
+ }
10480
+ const routingExecutionDeps = {
10481
+ ...buildRunnerExecutionDeps(),
10482
+ managedConversationBots,
10483
+ resolveConversationPeerBots: resolveRunnerConversationPeers,
10484
+ };
10485
+ if (selectedRecordKind === "bot_reply") {
10486
+ const botReplyAuthorityContext = await prepareRunnerBotReplyAuthorityContext(
10487
+ selectedRecord,
10488
+ routingExecutionDeps,
10489
+ triggerDecision,
10490
+ );
10491
+ if (!botReplyAuthorityContext.ok) {
10492
+ return botReplyAuthorityContext.skip;
10493
+ }
10494
+ return {
10495
+ kind: "ready",
10496
+ triggerDecision: botReplyAuthorityContext.triggerDecision,
10497
+ routingExecutionDeps: botReplyAuthorityContext.routingExecutionDeps,
10498
+ sharedHumanIntentContext: botReplyAuthorityContext.sharedHumanIntentContext,
10499
+ adjudication: botReplyAuthorityContext.adjudication,
10500
+ requestClaim: botReplyAuthorityContext.requestClaim,
10501
+ claimedRequest: botReplyAuthorityContext.claimedRequest,
10502
+ humanInboundVisibility,
10503
+ visibilityStatePatch,
10504
+ };
10505
+ }
10512
10506
  const sharedHumanIntentContext = await resolveHumanIntentContext({
10513
10507
  selectedRecord,
10514
10508
  normalizedRoute,
@@ -10561,12 +10555,12 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10561
10555
  skipTrigger: "responder_adjudication",
10562
10556
  };
10563
10557
  }
10564
- const requestClaim = await prepareRunnerRequestClaim(
10565
- selectedRecord,
10566
- adjudication.selected_bot_usernames,
10567
- safeObject(sharedHumanIntentContext).humanIntent || null,
10568
- humanInboundVisibility.sourceMessageEnvelope,
10569
- );
10558
+ const requestClaim = await prepareRunnerRequestClaim(
10559
+ selectedRecord,
10560
+ adjudication.selected_bot_usernames,
10561
+ safeObject(sharedHumanIntentContext).humanIntent || null,
10562
+ humanInboundVisibility.sourceMessageEnvelope,
10563
+ );
10570
10564
  if (!requestClaim.ok) {
10571
10565
  await syncRunnerRequestLedgerForProjectToServer({
10572
10566
  normalizedRoute,
@@ -10585,26 +10579,26 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10585
10579
  },
10586
10580
  };
10587
10581
  }
10588
- const finalizedRequest = await finalizePreparedRunnerRequest(
10589
- selectedRecord,
10590
- requestClaim,
10591
- humanInboundVisibility.sourceMessageEnvelope,
10592
- );
10582
+ const finalizedRequest = await finalizePreparedRunnerRequest(
10583
+ selectedRecord,
10584
+ requestClaim,
10585
+ humanInboundVisibility.sourceMessageEnvelope,
10586
+ );
10593
10587
  if (!finalizedRequest.ok) {
10594
10588
  return finalizedRequest.skip;
10595
10589
  }
10596
- return {
10597
- kind: "ready",
10598
- triggerDecision,
10599
- routingExecutionDeps,
10600
- sharedHumanIntentContext,
10601
- adjudication,
10602
- requestClaim,
10603
- claimedRequest: finalizedRequest.claimedRequest,
10604
- humanInboundVisibility,
10605
- visibilityStatePatch,
10606
- };
10607
- };
10590
+ return {
10591
+ kind: "ready",
10592
+ triggerDecision,
10593
+ routingExecutionDeps,
10594
+ sharedHumanIntentContext,
10595
+ adjudication,
10596
+ requestClaim,
10597
+ claimedRequest: finalizedRequest.claimedRequest,
10598
+ humanInboundVisibility,
10599
+ visibilityStatePatch,
10600
+ };
10601
+ };
10608
10602
  if (deferExecution) {
10609
10603
  const skippedRecords = [];
10610
10604
  for (const selectedRecord of pending.pending) {
@@ -10623,27 +10617,27 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10623
10617
  skippedRecords.push(preparation.skippedRecord);
10624
10618
  continue;
10625
10619
  }
10626
- const {
10627
- triggerDecision,
10628
- sharedHumanIntentContext,
10629
- adjudication,
10630
- requestClaim,
10631
- claimedRequest,
10632
- humanInboundVisibility,
10633
- visibilityStatePatch,
10634
- } = preparation;
10635
- saveRunnerRouteState(routeKey, {
10636
- ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey, {
10637
- id: String(claimedRequest.root_work_item_id || "").trim(),
10638
- title: String(claimedRequest.root_work_item_title || "").trim(),
10620
+ const {
10621
+ triggerDecision,
10622
+ sharedHumanIntentContext,
10623
+ adjudication,
10624
+ requestClaim,
10625
+ claimedRequest,
10626
+ humanInboundVisibility,
10627
+ visibilityStatePatch,
10628
+ } = preparation;
10629
+ saveRunnerRouteState(routeKey, {
10630
+ ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey, {
10631
+ id: String(claimedRequest.root_work_item_id || "").trim(),
10632
+ title: String(claimedRequest.root_work_item_title || "").trim(),
10639
10633
  status: String(claimedRequest.root_work_item_status || "").trim(),
10640
10634
  }),
10641
- last_request_key: String(requestClaim.requestKey || "").trim(),
10642
- last_root_work_item_id: String(claimedRequest.root_work_item_id || "").trim(),
10643
- last_root_work_item_title: String(claimedRequest.root_work_item_title || "").trim(),
10644
- last_root_work_item_status: String(claimedRequest.root_work_item_status || "").trim(),
10645
- ...safeObject(visibilityStatePatch),
10646
- });
10635
+ last_request_key: String(requestClaim.requestKey || "").trim(),
10636
+ last_root_work_item_id: String(claimedRequest.root_work_item_id || "").trim(),
10637
+ last_root_work_item_title: String(claimedRequest.root_work_item_title || "").trim(),
10638
+ last_root_work_item_status: String(claimedRequest.root_work_item_status || "").trim(),
10639
+ ...safeObject(visibilityStatePatch),
10640
+ });
10647
10641
  await syncRunnerRequestLedgerForProjectToServer({
10648
10642
  normalizedRoute,
10649
10643
  runtime,
@@ -10672,14 +10666,14 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10672
10666
  executionPlan,
10673
10667
  runtime,
10674
10668
  managedConversationBots,
10675
- requestKey: String(requestClaim.requestKey || "").trim(),
10676
- triggerDecision,
10677
- responderAdjudication: adjudication,
10678
- humanIntentContext: sharedHumanIntentContext,
10679
- humanInboundVisibility,
10680
- },
10681
- };
10682
- }
10669
+ requestKey: String(requestClaim.requestKey || "").trim(),
10670
+ triggerDecision,
10671
+ responderAdjudication: adjudication,
10672
+ humanIntentContext: sharedHumanIntentContext,
10673
+ humanInboundVisibility,
10674
+ },
10675
+ };
10676
+ }
10683
10677
  {
10684
10678
  const skippedOutcome = await finalizeSkippedPendingRecords(skippedRecords);
10685
10679
  if (skippedOutcome) {
@@ -10705,39 +10699,39 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10705
10699
  continue;
10706
10700
  }
10707
10701
  const currentRouteState = safeObject(loadBotRunnerState().routes[routeKey]);
10708
- const {
10709
- triggerDecision,
10710
- sharedHumanIntentContext,
10711
- adjudication: inlineAdjudication,
10712
- requestClaim,
10713
- claimedRequest,
10714
- humanInboundVisibility,
10715
- visibilityStatePatch,
10716
- } = preparation;
10717
- saveRunnerRouteState(routeKey, {
10718
- ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey, {
10719
- id: String(claimedRequest.root_work_item_id || "").trim(),
10720
- title: String(claimedRequest.root_work_item_title || "").trim(),
10702
+ const {
10703
+ triggerDecision,
10704
+ sharedHumanIntentContext,
10705
+ adjudication: inlineAdjudication,
10706
+ requestClaim,
10707
+ claimedRequest,
10708
+ humanInboundVisibility,
10709
+ visibilityStatePatch,
10710
+ } = preparation;
10711
+ saveRunnerRouteState(routeKey, {
10712
+ ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey, {
10713
+ id: String(claimedRequest.root_work_item_id || "").trim(),
10714
+ title: String(claimedRequest.root_work_item_title || "").trim(),
10721
10715
  status: String(claimedRequest.root_work_item_status || "").trim(),
10722
10716
  }),
10723
- last_request_key: String(requestClaim.requestKey || "").trim(),
10724
- last_root_work_item_id: String(claimedRequest.root_work_item_id || "").trim(),
10725
- last_root_work_item_title: String(claimedRequest.root_work_item_title || "").trim(),
10726
- last_root_work_item_status: String(claimedRequest.root_work_item_status || "").trim(),
10727
- ...safeObject(visibilityStatePatch),
10728
- });
10717
+ last_request_key: String(requestClaim.requestKey || "").trim(),
10718
+ last_root_work_item_id: String(claimedRequest.root_work_item_id || "").trim(),
10719
+ last_root_work_item_title: String(claimedRequest.root_work_item_title || "").trim(),
10720
+ last_root_work_item_status: String(claimedRequest.root_work_item_status || "").trim(),
10721
+ ...safeObject(visibilityStatePatch),
10722
+ });
10729
10723
  if (String(requestClaim.requestKey || "").trim()) {
10730
10724
  const resolvedIntentType = String(safeObject(loadBotRunnerState().routes[routeKey]).last_intent_type || "").trim();
10731
- markRunnerRequestLifecycle({
10732
- normalizedRoute,
10733
- requestKey: requestClaim.requestKey,
10734
- selectedRecord,
10735
- routeKey,
10736
- outcome: "running",
10737
- authoritativeSourceMessageEnvelope: safeObject(
10738
- humanInboundVisibility.sourceMessageEnvelope,
10739
- ),
10740
- });
10725
+ markRunnerRequestLifecycle({
10726
+ normalizedRoute,
10727
+ requestKey: requestClaim.requestKey,
10728
+ selectedRecord,
10729
+ routeKey,
10730
+ outcome: "running",
10731
+ authoritativeSourceMessageEnvelope: safeObject(
10732
+ humanInboundVisibility.sourceMessageEnvelope,
10733
+ ),
10734
+ });
10741
10735
  const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
10742
10736
  normalizedRoute,
10743
10737
  runtime,
@@ -10777,15 +10771,15 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10777
10771
  archiveThread,
10778
10772
  executionPlan,
10779
10773
  runtime,
10780
- triggerDecision,
10781
- responderAdjudication: inlineAdjudication,
10782
- persistedHumanIntentRequest: claimedRequest,
10783
- precomputedHumanIntentContext: sharedHumanIntentContext,
10784
- precomputedHumanInboundVisibility: humanInboundVisibility,
10785
- deps: {
10786
- saveRunnerRouteState,
10787
- startRunnerTypingHeartbeat,
10788
- runRunnerAIExecution,
10774
+ triggerDecision,
10775
+ responderAdjudication: inlineAdjudication,
10776
+ persistedHumanIntentRequest: claimedRequest,
10777
+ precomputedHumanIntentContext: sharedHumanIntentContext,
10778
+ precomputedHumanInboundVisibility: humanInboundVisibility,
10779
+ deps: {
10780
+ saveRunnerRouteState,
10781
+ startRunnerTypingHeartbeat,
10782
+ runRunnerAIExecution,
10789
10783
  explainExecutionFailureWithAI,
10790
10784
  performLocalBotDelivery,
10791
10785
  serializeRunnerTriggerPolicy,
@@ -10799,17 +10793,17 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10799
10793
  });
10800
10794
  if (processed.kind === "skipped") {
10801
10795
  if (String(requestClaim.requestKey || "").trim()) {
10802
- markRunnerRequestLifecycle({
10803
- normalizedRoute,
10804
- requestKey: requestClaim.requestKey,
10805
- selectedRecord,
10806
- routeKey,
10807
- outcome: "skipped",
10808
- closedReason: String(processed.skippedRecord?.reason || "skipped").trim() || "skipped",
10809
- authoritativeSourceMessageEnvelope: safeObject(
10810
- humanInboundVisibility.sourceMessageEnvelope,
10811
- ),
10812
- });
10796
+ markRunnerRequestLifecycle({
10797
+ normalizedRoute,
10798
+ requestKey: requestClaim.requestKey,
10799
+ selectedRecord,
10800
+ routeKey,
10801
+ outcome: "skipped",
10802
+ closedReason: String(processed.skippedRecord?.reason || "skipped").trim() || "skipped",
10803
+ authoritativeSourceMessageEnvelope: safeObject(
10804
+ humanInboundVisibility.sourceMessageEnvelope,
10805
+ ),
10806
+ });
10813
10807
  const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
10814
10808
  normalizedRoute,
10815
10809
  runtime,
@@ -10833,11 +10827,11 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10833
10827
  }
10834
10828
  if (String(requestClaim.requestKey || "").trim()) {
10835
10829
  const resolvedIntentType = String(safeObject(loadBotRunnerState().routes[routeKey]).last_intent_type || "").trim();
10836
- markRunnerRequestLifecycle({
10837
- normalizedRoute,
10838
- requestKey: requestClaim.requestKey,
10839
- selectedRecord,
10840
- routeKey,
10830
+ markRunnerRequestLifecycle({
10831
+ normalizedRoute,
10832
+ requestKey: requestClaim.requestKey,
10833
+ selectedRecord,
10834
+ routeKey,
10841
10835
  outcome: processed.kind === "delivery_failed"
10842
10836
  ? "delivery_failed_after_generation"
10843
10837
  : String(processed.result?.outcome || "replied").trim().toLowerCase(),
@@ -10867,19 +10861,19 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10867
10861
  responseContractValidationTargets: ensureArray(processed.result?.response_contract_validation_targets),
10868
10862
  assignmentValidationStatus: String(processed.result?.assignment_validation_status || "").trim(),
10869
10863
  assignmentValidationReason: String(processed.result?.assignment_validation_reason || "").trim(),
10870
- assignmentValidationModes: ensureArray(processed.result?.assignment_validation_modes),
10871
- deliveryStatus: String(processed.result?.delivery_status || "").trim(),
10872
- archiveStatus: String(processed.result?.archive_status || "").trim(),
10873
- transportError: String(processed.result?.transport_error || "").trim(),
10874
- archiveError: String(processed.result?.archive_error || "").trim(),
10875
- lastReplyMessageID: intFromRawAllowZero(processed.result?.last_reply_message_id, 0),
10876
- lastReplyMessageThreadID: intFromRawAllowZero(processed.result?.last_reply_message_thread_id, 0),
10877
- replyToMessageID: intFromRawAllowZero(processed.result?.reply_to_message_id, 0),
10878
- replyFallbackUsed: processed.result?.reply_fallback_used === true,
10879
- authoritativeSourceMessageEnvelope: safeObject(
10880
- humanInboundVisibility.sourceMessageEnvelope,
10881
- ),
10882
- });
10864
+ assignmentValidationModes: ensureArray(processed.result?.assignment_validation_modes),
10865
+ deliveryStatus: String(processed.result?.delivery_status || "").trim(),
10866
+ archiveStatus: String(processed.result?.archive_status || "").trim(),
10867
+ transportError: String(processed.result?.transport_error || "").trim(),
10868
+ archiveError: String(processed.result?.archive_error || "").trim(),
10869
+ lastReplyMessageID: intFromRawAllowZero(processed.result?.last_reply_message_id, 0),
10870
+ lastReplyMessageThreadID: intFromRawAllowZero(processed.result?.last_reply_message_thread_id, 0),
10871
+ replyToMessageID: intFromRawAllowZero(processed.result?.reply_to_message_id, 0),
10872
+ replyFallbackUsed: processed.result?.reply_fallback_used === true,
10873
+ authoritativeSourceMessageEnvelope: safeObject(
10874
+ humanInboundVisibility.sourceMessageEnvelope,
10875
+ ),
10876
+ });
10883
10877
  if (processed.kind !== "delivery_failed") {
10884
10878
  await ensureRunnerRootWorkItemForRequest({
10885
10879
  normalizedRoute,
@@ -11831,11 +11825,24 @@ function resolveRunnerProjectUpRoutes({
11831
11825
  server_bot_name: botName,
11832
11826
  server_bot_id: botID,
11833
11827
  });
11834
- return ensureArray(config.routes)
11828
+ const matched = ensureArray(config.routes)
11835
11829
  .map((rawRoute) => normalizeRunnerRoute(rawRoute))
11836
11830
  .filter((route) => route.enabled)
11837
11831
  .filter((route) => configuredRunnerRouteMatchesInlineSelection(route, selectionRoute, selectionFlags, { telegramEntries }))
11838
11832
  .filter((route) => !normalizedRoles.length || normalizedRoles.includes(normalizeBotRole(route.role)));
11833
+ if (normalizedRoles.length) {
11834
+ return matched;
11835
+ }
11836
+ const seenBotDestinations = new Map();
11837
+ return matched.filter((route) => {
11838
+ const botIdentity = String(route.serverBotID || route.botID || route.botName || route.serverBotName || "").trim().toLowerCase();
11839
+ const destIdentity = String(route.destinationID || route.destinationLabel || "").trim().toLowerCase();
11840
+ const dedupeKey = `${botIdentity}::${destIdentity}`;
11841
+ if (!dedupeKey || dedupeKey === "::") return true;
11842
+ if (seenBotDestinations.has(dedupeKey)) return false;
11843
+ seenBotDestinations.set(dedupeKey, true);
11844
+ return true;
11845
+ });
11839
11846
  }
11840
11847
 
11841
11848
  async function runRunnerProjectUp(flags) {
@@ -13158,16 +13165,16 @@ async function syncRunnerDeferredExecutionRunningState(deferredExecution) {
13158
13165
  if (!String(deferredExecution?.requestKey || "").trim()) {
13159
13166
  return;
13160
13167
  }
13161
- markRunnerRequestLifecycle({
13162
- normalizedRoute: deferredExecution.normalizedRoute,
13163
- requestKey: deferredExecution.requestKey,
13164
- selectedRecord: deferredExecution.selectedRecord,
13165
- routeKey: deferredExecution.routeKey,
13166
- outcome: "running",
13167
- authoritativeSourceMessageEnvelope: safeObject(
13168
- safeObject(deferredExecution.humanInboundVisibility).sourceMessageEnvelope,
13169
- ),
13170
- });
13168
+ markRunnerRequestLifecycle({
13169
+ normalizedRoute: deferredExecution.normalizedRoute,
13170
+ requestKey: deferredExecution.requestKey,
13171
+ selectedRecord: deferredExecution.selectedRecord,
13172
+ routeKey: deferredExecution.routeKey,
13173
+ outcome: "running",
13174
+ authoritativeSourceMessageEnvelope: safeObject(
13175
+ safeObject(deferredExecution.humanInboundVisibility).sourceMessageEnvelope,
13176
+ ),
13177
+ });
13171
13178
  const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
13172
13179
  normalizedRoute: deferredExecution.normalizedRoute,
13173
13180
  runtime: deferredExecution.runtime,
@@ -13192,17 +13199,17 @@ async function syncRunnerDeferredExecutionRunningState(deferredExecution) {
13192
13199
 
13193
13200
  async function finalizeRunnerDeferredExecutionSkipped(deferredExecution, processed) {
13194
13201
  if (String(deferredExecution?.requestKey || "").trim()) {
13195
- markRunnerRequestLifecycle({
13196
- normalizedRoute: deferredExecution.normalizedRoute,
13197
- requestKey: deferredExecution.requestKey,
13198
- selectedRecord: deferredExecution.selectedRecord,
13199
- routeKey: deferredExecution.routeKey,
13200
- outcome: "skipped",
13201
- closedReason: String(processed?.skippedRecord?.reason || "skipped").trim() || "skipped",
13202
- authoritativeSourceMessageEnvelope: safeObject(
13203
- safeObject(deferredExecution.humanInboundVisibility).sourceMessageEnvelope,
13204
- ),
13205
- });
13202
+ markRunnerRequestLifecycle({
13203
+ normalizedRoute: deferredExecution.normalizedRoute,
13204
+ requestKey: deferredExecution.requestKey,
13205
+ selectedRecord: deferredExecution.selectedRecord,
13206
+ routeKey: deferredExecution.routeKey,
13207
+ outcome: "skipped",
13208
+ closedReason: String(processed?.skippedRecord?.reason || "skipped").trim() || "skipped",
13209
+ authoritativeSourceMessageEnvelope: safeObject(
13210
+ safeObject(deferredExecution.humanInboundVisibility).sourceMessageEnvelope,
13211
+ ),
13212
+ });
13206
13213
  const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
13207
13214
  normalizedRoute: deferredExecution.normalizedRoute,
13208
13215
  runtime: deferredExecution.runtime,
@@ -13241,11 +13248,11 @@ async function finalizeRunnerDeferredExecutionProcessed(deferredExecution, proce
13241
13248
  const resolvedIntentType = String(
13242
13249
  safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type || "",
13243
13250
  ).trim();
13244
- markRunnerRequestLifecycle({
13245
- normalizedRoute: deferredExecution.normalizedRoute,
13246
- requestKey: deferredExecution.requestKey,
13247
- selectedRecord: deferredExecution.selectedRecord,
13248
- routeKey: deferredExecution.routeKey,
13251
+ markRunnerRequestLifecycle({
13252
+ normalizedRoute: deferredExecution.normalizedRoute,
13253
+ requestKey: deferredExecution.requestKey,
13254
+ selectedRecord: deferredExecution.selectedRecord,
13255
+ routeKey: deferredExecution.routeKey,
13249
13256
  outcome: processed.kind === "delivery_failed"
13250
13257
  ? "delivery_failed_after_generation"
13251
13258
  : String(processed.result?.outcome || "replied").trim().toLowerCase(),
@@ -13277,19 +13284,19 @@ async function finalizeRunnerDeferredExecutionProcessed(deferredExecution, proce
13277
13284
  responseContractValidationTargets: ensureArray(processed.result?.response_contract_validation_targets),
13278
13285
  assignmentValidationStatus: String(processed.result?.assignment_validation_status || "").trim(),
13279
13286
  assignmentValidationReason: String(processed.result?.assignment_validation_reason || "").trim(),
13280
- assignmentValidationModes: ensureArray(processed.result?.assignment_validation_modes),
13281
- deliveryStatus: String(processed.result?.delivery_status || "").trim(),
13282
- archiveStatus: String(processed.result?.archive_status || "").trim(),
13283
- transportError: String(processed.result?.transport_error || "").trim(),
13284
- archiveError: String(processed.result?.archive_error || "").trim(),
13285
- lastReplyMessageID: intFromRawAllowZero(processed.result?.last_reply_message_id, 0),
13286
- lastReplyMessageThreadID: intFromRawAllowZero(processed.result?.last_reply_message_thread_id, 0),
13287
- replyToMessageID: intFromRawAllowZero(processed.result?.reply_to_message_id, 0),
13288
- replyFallbackUsed: processed.result?.reply_fallback_used === true,
13289
- authoritativeSourceMessageEnvelope: safeObject(
13290
- safeObject(deferredExecution.humanInboundVisibility).sourceMessageEnvelope,
13291
- ),
13292
- });
13287
+ assignmentValidationModes: ensureArray(processed.result?.assignment_validation_modes),
13288
+ deliveryStatus: String(processed.result?.delivery_status || "").trim(),
13289
+ archiveStatus: String(processed.result?.archive_status || "").trim(),
13290
+ transportError: String(processed.result?.transport_error || "").trim(),
13291
+ archiveError: String(processed.result?.archive_error || "").trim(),
13292
+ lastReplyMessageID: intFromRawAllowZero(processed.result?.last_reply_message_id, 0),
13293
+ lastReplyMessageThreadID: intFromRawAllowZero(processed.result?.last_reply_message_thread_id, 0),
13294
+ replyToMessageID: intFromRawAllowZero(processed.result?.reply_to_message_id, 0),
13295
+ replyFallbackUsed: processed.result?.reply_fallback_used === true,
13296
+ authoritativeSourceMessageEnvelope: safeObject(
13297
+ safeObject(deferredExecution.humanInboundVisibility).sourceMessageEnvelope,
13298
+ ),
13299
+ });
13293
13300
  if (processed.kind !== "delivery_failed") {
13294
13301
  await ensureRunnerRootWorkItemForRequest({
13295
13302
  normalizedRoute: deferredExecution.normalizedRoute,
@@ -13346,18 +13353,18 @@ async function finalizeRunnerDeferredExecutionError(deferredExecution, errorText
13346
13353
  || currentRequest.normalized_intent
13347
13354
  || "",
13348
13355
  ).trim();
13349
- markRunnerRequestLifecycle({
13350
- normalizedRoute: deferredExecution.normalizedRoute,
13351
- requestKey: deferredExecution.requestKey,
13352
- selectedRecord: deferredExecution.selectedRecord,
13353
- routeKey: deferredExecution.routeKey,
13354
- outcome: "error",
13355
- closedReason: errorText || "execution_error",
13356
- normalizedIntent: resolvedIntentType,
13357
- authoritativeSourceMessageEnvelope: safeObject(
13358
- safeObject(deferredExecution.humanInboundVisibility).sourceMessageEnvelope,
13359
- ),
13360
- });
13356
+ markRunnerRequestLifecycle({
13357
+ normalizedRoute: deferredExecution.normalizedRoute,
13358
+ requestKey: deferredExecution.requestKey,
13359
+ selectedRecord: deferredExecution.selectedRecord,
13360
+ routeKey: deferredExecution.routeKey,
13361
+ outcome: "error",
13362
+ closedReason: errorText || "execution_error",
13363
+ normalizedIntent: resolvedIntentType,
13364
+ authoritativeSourceMessageEnvelope: safeObject(
13365
+ safeObject(deferredExecution.humanInboundVisibility).sourceMessageEnvelope,
13366
+ ),
13367
+ });
13361
13368
  await ensureRunnerRootWorkItemForRequest({
13362
13369
  normalizedRoute: deferredExecution.normalizedRoute,
13363
13370
  routeKey: deferredExecution.routeKey,
@@ -13572,10 +13579,10 @@ function createRunnerDeferredExecutionPromise({
13572
13579
  }) {
13573
13580
  return (async () => {
13574
13581
  try {
13575
- const processed = await processRunnerSelectedRecord({
13576
- routeKey: deferredExecution.routeKey,
13577
- normalizedRoute: deferredExecution.normalizedRoute,
13578
- routeState: deferredExecution.routeState,
13582
+ const processed = await processRunnerSelectedRecord({
13583
+ routeKey: deferredExecution.routeKey,
13584
+ normalizedRoute: deferredExecution.normalizedRoute,
13585
+ routeState: deferredExecution.routeState,
13579
13586
  selectedRecord: deferredExecution.selectedRecord,
13580
13587
  pendingOrdered: deferredExecution.pendingOrdered,
13581
13588
  bot: deferredExecution.bot,
@@ -13583,14 +13590,14 @@ function createRunnerDeferredExecutionPromise({
13583
13590
  archiveThread: deferredExecution.archiveThread,
13584
13591
  executionPlan: deferredExecution.executionPlan,
13585
13592
  runtime: deferredExecution.runtime,
13586
- triggerDecision: deferredExecution.triggerDecision,
13587
- responderAdjudication: deferredExecution.responderAdjudication,
13588
- persistedHumanIntentRequest: loadRunnerRequestByKey(deferredExecution.requestKey),
13589
- precomputedHumanIntentContext: safeObject(deferredExecution.humanIntentContext),
13590
- precomputedHumanInboundVisibility: safeObject(deferredExecution.humanInboundVisibility),
13591
- deps: {
13592
- saveRunnerRouteState,
13593
- startRunnerTypingHeartbeat,
13593
+ triggerDecision: deferredExecution.triggerDecision,
13594
+ responderAdjudication: deferredExecution.responderAdjudication,
13595
+ persistedHumanIntentRequest: loadRunnerRequestByKey(deferredExecution.requestKey),
13596
+ precomputedHumanIntentContext: safeObject(deferredExecution.humanIntentContext),
13597
+ precomputedHumanInboundVisibility: safeObject(deferredExecution.humanInboundVisibility),
13598
+ deps: {
13599
+ saveRunnerRouteState,
13600
+ startRunnerTypingHeartbeat,
13594
13601
  runRunnerAIExecution,
13595
13602
  explainExecutionFailureWithAI,
13596
13603
  performLocalBotDelivery,
@@ -17918,25 +17925,25 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
17918
17925
  mergeServerRunnerRequestLedgerIntoLocalState,
17919
17926
  buildRunnerStatusQueryLookup,
17920
17927
  tryJsonParse,
17921
- safeObject,
17922
- normalizeRunnerTriggerPolicy,
17923
- evaluateTelegramRunnerTrigger,
17924
- resolveHumanIntentContext,
17925
- resolveRunnerResponderAdjudication,
17926
- selectPendingArchiveComments,
17928
+ safeObject,
17929
+ normalizeRunnerTriggerPolicy,
17930
+ evaluateTelegramRunnerTrigger,
17931
+ resolveHumanIntentContext,
17932
+ resolveRunnerResponderAdjudication,
17933
+ selectPendingArchiveComments,
17927
17934
  selectRunnerPendingWork,
17928
17935
  processRunnerSelectedRecord,
17929
- resolveRunnerStartupLoopAdjudication,
17930
- shouldBypassRunnerStartupLoopForContractFollowup,
17931
- claimRunnerRequestForHumanComment,
17932
- markRunnerRequestLifecycle,
17933
- resolveRunnerContinuationRequestForBotReply,
17934
- normalizeBotRunnerRequests,
17935
- buildRunnerRequestRecoveryPatchFromRouteState,
17936
- peekRunnerContinuationRequestForBotReply,
17937
- cleanupBotRunnerRequestState,
17938
- runRunnerAIExecution,
17939
- buildLocalBotPrompt,
17936
+ resolveRunnerStartupLoopAdjudication,
17937
+ shouldBypassRunnerStartupLoopForContractFollowup,
17938
+ claimRunnerRequestForHumanComment,
17939
+ markRunnerRequestLifecycle,
17940
+ resolveRunnerContinuationRequestForBotReply,
17941
+ normalizeBotRunnerRequests,
17942
+ buildRunnerRequestRecoveryPatchFromRouteState,
17943
+ peekRunnerContinuationRequestForBotReply,
17944
+ cleanupBotRunnerRequestState,
17945
+ runRunnerAIExecution,
17946
+ buildLocalBotPrompt,
17940
17947
  formatBotReplyArchiveComment,
17941
17948
  findArchivedBotReplyRecord,
17942
17949
  parseArchivedChatComment,