metheus-governance-mcp-cli 0.2.172 → 0.2.178

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
@@ -11,6 +11,7 @@ import http from "node:http";
11
11
  import https from "node:https";
12
12
  import {
13
13
  DEFAULT_LOCAL_AI_CLIENT,
14
+ adjudicateRunnerRespondersWithAI,
14
15
  analyzeHumanConversationIntentWithAI,
15
16
  auditRoleExecutionPlanWithAI,
16
17
  auditDirectHumanReplyWithAI,
@@ -126,6 +127,7 @@ import {
126
127
  withWorkspaceDirArg,
127
128
  } from "./lib/client-registration.mjs";
128
129
  import {
130
+ applyPendingAgeSelection,
129
131
  buildRunnerRouteStateFromComment,
130
132
  isInboundArchiveKind,
131
133
  normalizeArchiveCommentRecord,
@@ -165,6 +167,7 @@ import {
165
167
  } from "./lib/runner-execution.mjs";
166
168
  import {
167
169
  processRunnerSelectedRecord,
170
+ resolveRunnerResponderAdjudication,
168
171
  selectRunnerPendingWork,
169
172
  } from "./lib/runner-orchestration.mjs";
170
173
  import {
@@ -196,6 +199,7 @@ const BOT_RUNNER_ROUTE_LOCK_STALE_MS = 30 * 60 * 1000;
196
199
  const BOT_RUNNER_ACTIVE_EXECUTION_STUCK_WARNING_MS = 20 * 60 * 1000;
197
200
  const BOT_RUNNER_ACTIVE_EXECUTION_HEARTBEAT_INTERVAL_MS = 5000;
198
201
  const BOT_RUNNER_ACTIVE_EXECUTION_HEARTBEAT_STALE_MS = 60 * 1000;
202
+ const BOT_RUNNER_PENDING_COMMENT_MAX_AGE_MS = 15 * 60 * 1000;
199
203
  const BOT_RUNNER_DEFAULT_CONCURRENCY = 2;
200
204
  const BOT_RUNNER_MAX_CONCURRENCY = 8;
201
205
  const TELEGRAM_ROOT_LEGACY_ENV_RELATIVE_PATH = path.join(".metheus", "telegram.env");
@@ -3989,6 +3993,7 @@ async function resolveInformationalQueryReply({
3989
3993
 
3990
3994
  function buildRunnerExecutionDeps() {
3991
3995
  return {
3996
+ adjudicateRunnerRespondersWithAI,
3992
3997
  analyzeHumanConversationIntentWithAI,
3993
3998
  auditRoleExecutionPlanWithAI,
3994
3999
  auditDirectHumanReplyWithAI,
@@ -4031,6 +4036,7 @@ function buildRunnerRuntimeDeps() {
4031
4036
  getTelegramUpdates,
4032
4037
  normalizeLocalTelegramUpdate,
4033
4038
  listThreadComments,
4039
+ applyPendingAgeSelection,
4034
4040
  normalizeArchiveCommentRecord,
4035
4041
  isInboundArchiveKind,
4036
4042
  buildArchivedInboundMessageKey,
@@ -4223,6 +4229,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
4223
4229
  timeoutSeconds: runtime.timeoutSeconds,
4224
4230
  });
4225
4231
  const bot = selectRunnerBot(bots, normalizedRoute);
4232
+ const managedConversationBots = resolveRunnerConversationManagedBots(normalizedRoute, bots);
4226
4233
  const executionPlan = resolveRunnerExecutionPlan(
4227
4234
  normalizedRoute,
4228
4235
  bot,
@@ -4293,11 +4300,21 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
4293
4300
  refreshedState,
4294
4301
  mode,
4295
4302
  parseArchivedChatComment,
4303
+ pendingSelectionOptions: {
4304
+ maxPendingAgeMs: BOT_RUNNER_PENDING_COMMENT_MAX_AGE_MS,
4305
+ },
4296
4306
  deps: {
4307
+ applyPendingAgeSelection,
4297
4308
  normalizeArchiveCommentRecord,
4298
4309
  },
4299
4310
  });
4300
4311
  const pending = pendingWork.pending;
4312
+ if (pending.staleSkippedLatest) {
4313
+ saveRunnerRouteState(routeKey, buildRunnerRouteStateFromComment(pending.staleSkippedLatest, {
4314
+ last_reason: `skipped ${ensureArray(pending.staleSkipped).length} stale archive message(s) older than ${Math.floor(BOT_RUNNER_PENDING_COMMENT_MAX_AGE_MS / 60000)} minutes`,
4315
+ }));
4316
+ refreshedState = safeObject(loadBotRunnerState().routes[routeKey]);
4317
+ }
4301
4318
  if (pending.shouldPrime && pending.latest) {
4302
4319
  saveRunnerRouteState(routeKey, buildRunnerRouteStateFromComment(pending.latest, { primed: true }));
4303
4320
  return {
@@ -4318,7 +4335,9 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
4318
4335
  route_name: normalizedRoute.name,
4319
4336
  logical_signature: runnerRouteLogicalSignature(normalizedRoute),
4320
4337
  outcome: "idle",
4321
- detail: importOutcome.importedCount > 0
4338
+ detail: pending.staleSkippedLatest
4339
+ ? `skipped ${ensureArray(pending.staleSkipped).length} stale archive message(s)`
4340
+ : importOutcome.importedCount > 0
4322
4341
  ? "local telegram updates imported but no pending archive comments were selected"
4323
4342
  : "no new local telegram messages or archived inbound messages",
4324
4343
  archive_source: String(archiveThread.source || "").trim() || "-",
@@ -4344,33 +4363,85 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
4344
4363
  };
4345
4364
  }
4346
4365
  if (deferExecution) {
4347
- const selectedRecord = pending.pending[0];
4348
- saveRunnerRouteState(routeKey, buildRunnerActiveExecutionPatch(selectedRecord));
4349
- return {
4350
- route_key: routeKey,
4351
- route_name: normalizedRoute.name,
4352
- logical_signature: runnerRouteLogicalSignature(normalizedRoute),
4353
- outcome: "accepted",
4354
- detail: `accepted comment ${selectedRecord.id} for background execution`,
4355
- archive_source: String(archiveThread.source || "").trim() || "-",
4356
- archive_work_item_id: String(archiveThread.workItemID || "").trim() || "",
4357
- thread_id: archiveThread.threadID,
4358
- comment_id: selectedRecord.id,
4359
- execution_mode: executionPlan.mode,
4360
- role_profile: executionPlan.roleProfileName,
4361
- deferred_execution: {
4362
- routeKey,
4363
- normalizedRoute,
4364
- routeState: refreshedState,
4366
+ const skippedRecords = [];
4367
+ for (const selectedRecord of pending.pending) {
4368
+ const adjudication = await resolveRunnerResponderAdjudication({
4365
4369
  selectedRecord,
4366
4370
  pendingOrdered: pending.ordered,
4371
+ normalizedRoute,
4367
4372
  bot,
4368
- destination,
4369
- archiveThread,
4370
4373
  executionPlan,
4371
- runtime,
4372
- },
4373
- };
4374
+ deps: {
4375
+ ...buildRunnerExecutionDeps(),
4376
+ managedConversationBots,
4377
+ resolveConversationPeerBots: resolveRunnerConversationPeers,
4378
+ },
4379
+ });
4380
+ const currentBotSelector = String(bot?.username || bot?.name || "").trim().replace(/^@+/, "").toLowerCase();
4381
+ const currentBotSelected = ensureArray(adjudication.selected_bot_usernames)
4382
+ .map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase())
4383
+ .includes(currentBotSelector);
4384
+ if (!currentBotSelected) {
4385
+ saveRunnerRouteState(
4386
+ routeKey,
4387
+ buildRunnerRouteStateFromComment(selectedRecord, {
4388
+ last_action: "adjudication_skipped",
4389
+ last_reason: String(adjudication.reason_code || "").trim() || "not_selected_by_adjudicator",
4390
+ last_trigger: "responder_adjudication",
4391
+ }),
4392
+ );
4393
+ skippedRecords.push({
4394
+ id: selectedRecord.id,
4395
+ reason: String(adjudication.reason_code || "").trim() || "not_selected_by_adjudicator",
4396
+ });
4397
+ continue;
4398
+ }
4399
+ saveRunnerRouteState(routeKey, buildRunnerActiveExecutionPatch(selectedRecord));
4400
+ return {
4401
+ route_key: routeKey,
4402
+ route_name: normalizedRoute.name,
4403
+ logical_signature: runnerRouteLogicalSignature(normalizedRoute),
4404
+ outcome: "accepted",
4405
+ detail: `accepted comment ${selectedRecord.id} for background execution`,
4406
+ archive_source: String(archiveThread.source || "").trim() || "-",
4407
+ archive_work_item_id: String(archiveThread.workItemID || "").trim() || "",
4408
+ thread_id: archiveThread.threadID,
4409
+ comment_id: selectedRecord.id,
4410
+ execution_mode: executionPlan.mode,
4411
+ role_profile: executionPlan.roleProfileName,
4412
+ deferred_execution: {
4413
+ routeKey,
4414
+ normalizedRoute,
4415
+ routeState: refreshedState,
4416
+ selectedRecord,
4417
+ pendingOrdered: pending.ordered,
4418
+ bot,
4419
+ destination,
4420
+ archiveThread,
4421
+ executionPlan,
4422
+ runtime,
4423
+ managedConversationBots,
4424
+ },
4425
+ };
4426
+ }
4427
+ if (skippedRecords.length > 0) {
4428
+ const distinctReasons = Array.from(new Set(skippedRecords.map((item) => item.reason).filter(Boolean)));
4429
+ const lastSkipped = skippedRecords[skippedRecords.length - 1];
4430
+ return {
4431
+ route_key: routeKey,
4432
+ route_name: normalizedRoute.name,
4433
+ logical_signature: runnerRouteLogicalSignature(normalizedRoute),
4434
+ outcome: "skipped",
4435
+ detail: distinctReasons.join("; ") || "all pending messages were skipped",
4436
+ archive_source: String(archiveThread.source || "").trim() || "-",
4437
+ archive_work_item_id: String(archiveThread.workItemID || "").trim() || "",
4438
+ thread_id: archiveThread.threadID,
4439
+ comment_id: lastSkipped.id,
4440
+ skipped_count: skippedRecords.length,
4441
+ execution_mode: executionPlan.mode,
4442
+ role_profile: executionPlan.roleProfileName,
4443
+ };
4444
+ }
4374
4445
  }
4375
4446
  const skippedRecords = [];
4376
4447
  for (const selectedRecord of pending.pending) {
@@ -4396,6 +4467,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
4396
4467
  buildRunnerExecutionDeps,
4397
4468
  buildRunnerDeliveryDeps,
4398
4469
  buildRunnerRuntimeDeps,
4470
+ managedConversationBots,
4399
4471
  resolveConversationPeerBots: resolveRunnerConversationPeers,
4400
4472
  },
4401
4473
  });
@@ -5184,6 +5256,45 @@ function resolveRunnerConversationPeers(routeRaw) {
5184
5256
  return peers;
5185
5257
  }
5186
5258
 
5259
+ function resolveRunnerConversationManagedBots(routeRaw, availableBots = []) {
5260
+ const normalizedRoute = normalizeRunnerRoute(routeRaw);
5261
+ const config = loadBotRunnerConfig({ persistIfNeeded: true });
5262
+ const seen = new Set();
5263
+ const managed = [];
5264
+ ensureArray(config.routes)
5265
+ .map((rawRoute) => normalizeRunnerRoute(rawRoute))
5266
+ .filter((route) => route.enabled)
5267
+ .filter((route) => String(route.projectID || "").trim() === String(normalizedRoute.projectID || "").trim())
5268
+ .filter((route) => String(route.provider || "").trim() === String(normalizedRoute.provider || "").trim())
5269
+ .filter((route) => {
5270
+ const routeDestinationID = String(route.destinationID || "").trim();
5271
+ const targetDestinationID = String(normalizedRoute.destinationID || "").trim();
5272
+ const routeDestinationLabel = normalizeRunnerRouteIdentityText(route.destinationLabel);
5273
+ const targetDestinationLabel = normalizeRunnerRouteIdentityText(normalizedRoute.destinationLabel);
5274
+ return targetDestinationID
5275
+ ? (routeDestinationID === targetDestinationID || (!routeDestinationID && routeDestinationLabel === targetDestinationLabel))
5276
+ : routeDestinationLabel === targetDestinationLabel;
5277
+ })
5278
+ .forEach((route) => {
5279
+ try {
5280
+ const bot = selectRunnerBot(availableBots, route);
5281
+ const username = String(bot?.username || bot?.name || route.botName || "").trim().replace(/^@+/, "").toLowerCase();
5282
+ if (!username || seen.has(username)) {
5283
+ return;
5284
+ }
5285
+ seen.add(username);
5286
+ managed.push({
5287
+ username,
5288
+ display_name: String(bot?.name || bot?.username || route.botName || username).trim(),
5289
+ route_name: String(route.name || "").trim(),
5290
+ route,
5291
+ bot,
5292
+ });
5293
+ } catch {}
5294
+ });
5295
+ return managed;
5296
+ }
5297
+
5187
5298
  function applyRunnerProjectUpRouteSuggestions(routeSuggestions) {
5188
5299
  const config = loadBotRunnerConfig({ persistIfNeeded: true });
5189
5300
  let nextConfig = config;
@@ -6591,6 +6702,7 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
6591
6702
  buildRunnerExecutionDeps,
6592
6703
  buildRunnerDeliveryDeps,
6593
6704
  buildRunnerRuntimeDeps,
6705
+ managedConversationBots: deferredExecution.managedConversationBots,
6594
6706
  resolveConversationPeerBots: resolveRunnerConversationPeers,
6595
6707
  reportRunnerStage: (stage) => {
6596
6708
  runnerLogger?.append("execution_stage", {
@@ -11760,6 +11872,47 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
11760
11872
  push("runner_active_execution_heartbeat_marks_live_execution_stale_when_expired", false, String(err?.message || err));
11761
11873
  }
11762
11874
 
11875
+ try {
11876
+ const nowMs = Date.now();
11877
+ const pendingSelection = selectPendingArchiveComments(
11878
+ [
11879
+ {
11880
+ id: "comment-seen",
11881
+ created_at: new Date(nowMs - (BOT_RUNNER_PENDING_COMMENT_MAX_AGE_MS + 5 * 60_000)).toISOString(),
11882
+ body: "[Telegram message]\nmessage_id: 100\n\nseen pending mention",
11883
+ },
11884
+ {
11885
+ id: "comment-old",
11886
+ created_at: new Date(nowMs - (BOT_RUNNER_PENDING_COMMENT_MAX_AGE_MS + 60_000)).toISOString(),
11887
+ body: "[Telegram message]\nmessage_id: 101\n\nold pending mention",
11888
+ },
11889
+ {
11890
+ id: "comment-fresh",
11891
+ created_at: new Date(nowMs - 60_000).toISOString(),
11892
+ body: "[Telegram message]\nmessage_id: 102\n\nfresh pending mention",
11893
+ },
11894
+ ],
11895
+ {
11896
+ last_processed_comment_id: "comment-seen",
11897
+ },
11898
+ "start",
11899
+ (record) => normalizeArchiveCommentRecord(record, parseArchivedChatComment),
11900
+ {
11901
+ maxPendingAgeMs: BOT_RUNNER_PENDING_COMMENT_MAX_AGE_MS,
11902
+ nowMs,
11903
+ },
11904
+ );
11905
+ push(
11906
+ "runner_pending_selection_skips_stale_archive_backlog",
11907
+ ensureArray(pendingSelection.pending).length === 1
11908
+ && String(ensureArray(pendingSelection.pending)[0]?.id || "").trim() === "comment-fresh"
11909
+ && String(safeObject(pendingSelection.staleSkippedLatest).id || "").trim() === "comment-old",
11910
+ `pending=${ensureArray(pendingSelection.pending).map((item) => String(item?.id || "").trim()).join(",") || "-"} stale=${String(safeObject(pendingSelection.staleSkippedLatest).id || "").trim() || "-"}`,
11911
+ );
11912
+ } catch (err) {
11913
+ push("runner_pending_selection_skips_stale_archive_backlog", false, String(err?.message || err));
11914
+ }
11915
+
11763
11916
  try {
11764
11917
  const startResponse = await handleLocalProjectToolDispatchImpl(
11765
11918
  {