metheus-governance-mcp-cli 0.2.60 → 0.2.61
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/README.md +6 -0
- package/cli.mjs +742 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -190,6 +190,7 @@ Checks:
|
|
|
190
190
|
- local bot runner v2 config validity
|
|
191
191
|
- project workspace mapping presence
|
|
192
192
|
- role profile -> local CLI availability
|
|
193
|
+
- route dry-run / trigger-policy safety warnings
|
|
193
194
|
- local provider token presence for active project destinations
|
|
194
195
|
- codex/claude/gemini/antigravity/cursor registration state
|
|
195
196
|
- gateway `tools/list` reachability
|
|
@@ -202,6 +203,7 @@ Direct bot posting:
|
|
|
202
203
|
- it does not use a server-stored bot token
|
|
203
204
|
- the destination identifier is resolved from the current project's saved Chat Destinations on the Metheus server
|
|
204
205
|
- if multiple active destinations exist for the same provider, pass `destination_id` or `destination_label`
|
|
206
|
+
- pass `dry_run_delivery=true` to preview the resolved bot/destination locally without sending the provider message
|
|
205
207
|
- direct local delivery is implemented today for:
|
|
206
208
|
- Telegram
|
|
207
209
|
- Slack
|
|
@@ -239,6 +241,7 @@ Common flags:
|
|
|
239
241
|
metheus-governance-mcp-cli runner once --project-id <project_uuid> --provider telegram --role monitor
|
|
240
242
|
metheus-governance-mcp-cli runner start --project-id <project_uuid> --provider telegram --role monitor --poll-interval-ms 5000
|
|
241
243
|
metheus-governance-mcp-cli runner start --project-id <project_uuid> --provider telegram --role monitor --mentions-only true
|
|
244
|
+
metheus-governance-mcp-cli runner once --project-id <project_uuid> --provider telegram --role monitor --dry-run-delivery true
|
|
242
245
|
metheus-governance-mcp-cli runner once --project-id <project_uuid> --provider telegram --role monitor --role-profile review
|
|
243
246
|
metheus-governance-mcp-cli runner once --project-id <project_uuid> --provider telegram --role monitor --command "python C:\\path\\to\\reply.py"
|
|
244
247
|
```
|
|
@@ -299,6 +302,9 @@ Notes:
|
|
|
299
302
|
- `runner once` processes the most recent pending archived inbound message
|
|
300
303
|
- `runner start` keeps polling and stores per-route cursor state in `~/.metheus/bot-runner-state.json`
|
|
301
304
|
- first start primes the cursor to the latest inbound message and does not reply to old backlog
|
|
305
|
+
- when inline filters match a configured route in `~/.metheus/bot-runner.json`, the runner reuses that route's canonical name/destination and state cursor instead of creating a new anonymous route key
|
|
306
|
+
- stale anonymous route keys in `~/.metheus/bot-runner-state.json` are auto-migrated to the matching configured route when possible; `doctor` warns if ambiguous legacy keys still remain
|
|
307
|
+
- `--dry-run-delivery true` resolves the real bot and destination but skips provider send and archive mirror writes
|
|
302
308
|
- when `trigger_policy.mentions_only=true`, unmentioned group messages are archived but skipped for reply generation
|
|
303
309
|
- mirrored bot replies are deduped by `chat_id + message_id`
|
|
304
310
|
- provider bot messages are ignored during inbound import by default
|
package/cli.mjs
CHANGED
|
@@ -78,8 +78,8 @@ function printUsage() {
|
|
|
78
78
|
` ${cmd} proxy [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path|auto>] [--include-drafts <true|false>] [--auto-pull-on-conflict <true|false>] [--timeout-seconds <n>]`,
|
|
79
79
|
` ${cmd} selftest [--json <true|false>]`,
|
|
80
80
|
` ${cmd} local-bot-bridge [--client <codex|claude|gemini|sample>] [--cwd <path>] [--model <name>] [--permission-mode <read_only|workspace_write|danger_full_access>] [--reasoning-effort <low|medium|high>]`,
|
|
81
|
-
` ${cmd} runner once [--project-id <uuid>] [--provider <telegram|slack|kakaotalk>] [--role <monitor|review|worker|approval>] [--role-profile <name>] [--bot-name <name>] [--bot-id <uuid>] [--mentions-only <true|false>] [--reply-to-bot-messages <true|false>] [--direct-messages <true|false>] [--ignore-edited-messages <true|false>] [--command <shell command>] [--context-comments <n>] [--archive-replies <true|false>]`,
|
|
82
|
-
` ${cmd} runner start [--project-id <uuid>] [--provider <telegram|slack|kakaotalk>] [--role <monitor|review|worker|approval>] [--role-profile <name>] [--bot-name <name>] [--bot-id <uuid>] [--mentions-only <true|false>] [--reply-to-bot-messages <true|false>] [--direct-messages <true|false>] [--ignore-edited-messages <true|false>] [--command <shell command>] [--poll-interval-ms <n>] [--context-comments <n>] [--archive-replies <true|false>]`,
|
|
81
|
+
` ${cmd} runner once [--project-id <uuid>] [--provider <telegram|slack|kakaotalk>] [--role <monitor|review|worker|approval>] [--role-profile <name>] [--bot-name <name>] [--bot-id <uuid>] [--mentions-only <true|false>] [--reply-to-bot-messages <true|false>] [--direct-messages <true|false>] [--ignore-edited-messages <true|false>] [--dry-run-delivery <true|false>] [--command <shell command>] [--context-comments <n>] [--archive-replies <true|false>]`,
|
|
82
|
+
` ${cmd} runner start [--project-id <uuid>] [--provider <telegram|slack|kakaotalk>] [--role <monitor|review|worker|approval>] [--role-profile <name>] [--bot-name <name>] [--bot-id <uuid>] [--mentions-only <true|false>] [--reply-to-bot-messages <true|false>] [--direct-messages <true|false>] [--ignore-edited-messages <true|false>] [--dry-run-delivery <true|false>] [--command <shell command>] [--poll-interval-ms <n>] [--context-comments <n>] [--archive-replies <true|false>]`,
|
|
83
83
|
` ${cmd} ctxpack pull [--project-id <uuid>] [--base-url <url>] [--workspace-dir <path|auto>] [--paths <csv>] [--timeout-seconds <n>]`,
|
|
84
84
|
` ${cmd} auth status`,
|
|
85
85
|
` ${cmd} auth login [--base-url <url>] [--flow <auto|device|callback|manual>] [--keycloak-url <url>] [--realm <name>] [--client-id <id>] [--open-browser <true|false>] [--callback-port <n>] [--timeout-seconds <n>] [--manual <true|false>]`,
|
|
@@ -823,6 +823,7 @@ function botRunnerConfigTemplate() {
|
|
|
823
823
|
dedupe_outbound: defaultArchivePolicy.dedupeOutbound,
|
|
824
824
|
skip_bot_messages: defaultArchivePolicy.skipBotMessages,
|
|
825
825
|
},
|
|
826
|
+
dry_run_delivery: false,
|
|
826
827
|
poll_interval_ms: 5000,
|
|
827
828
|
context_comments: 8,
|
|
828
829
|
archive_replies: true,
|
|
@@ -848,6 +849,7 @@ function serializeRunnerRoute(route) {
|
|
|
848
849
|
archive_work_item_id: normalized.archiveWorkItemID,
|
|
849
850
|
trigger_policy: serializeRunnerTriggerPolicy(normalized.triggerPolicy),
|
|
850
851
|
archive_policy: serializeRunnerArchivePolicy(normalized.archivePolicy),
|
|
852
|
+
...(normalized.dryRunDelivery ? { dry_run_delivery: true } : {}),
|
|
851
853
|
...(normalized.workspaceDir ? { workspace_dir: normalized.workspaceDir } : {}),
|
|
852
854
|
...(normalized.command ? { command: normalized.command } : {}),
|
|
853
855
|
poll_interval_ms: normalized.pollIntervalMs,
|
|
@@ -1060,17 +1062,239 @@ function rememberProjectWorkspaceMapping({ projectID, workspaceDir, source }) {
|
|
|
1060
1062
|
};
|
|
1061
1063
|
}
|
|
1062
1064
|
|
|
1065
|
+
function parseRunnerStateRouteKey(routeKey) {
|
|
1066
|
+
const text = String(routeKey || "").trim();
|
|
1067
|
+
if (!text) return null;
|
|
1068
|
+
const parts = text.split("::");
|
|
1069
|
+
if (parts.length < 6) return null;
|
|
1070
|
+
return {
|
|
1071
|
+
raw: text,
|
|
1072
|
+
name: String(parts[0] || "").trim(),
|
|
1073
|
+
projectID: String(parts[1] || "").trim(),
|
|
1074
|
+
provider: normalizeBotProvider(parts[2]),
|
|
1075
|
+
role: normalizeBotRole(parts[3]),
|
|
1076
|
+
botSelector: String(parts[4] || "").trim(),
|
|
1077
|
+
destinationSelector: parts.slice(5).join("::").trim(),
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function runnerStateRouteKeyIsUnnamed(parsedKey) {
|
|
1082
|
+
const routeKey = safeObject(parsedKey);
|
|
1083
|
+
if (!routeKey.projectID || !routeKey.provider || !routeKey.role) {
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
1086
|
+
return routeKey.name === "-";
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function runnerStateRouteKeyLooksAnonymous(parsedKey) {
|
|
1090
|
+
const routeKey = safeObject(parsedKey);
|
|
1091
|
+
if (!runnerStateRouteKeyIsUnnamed(routeKey)) {
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
return routeKey.botSelector !== "-" || routeKey.destinationSelector !== "-";
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function runnerStateSelectorMatchesRoute(selector, idValue, labelValue) {
|
|
1098
|
+
const normalizedSelector = String(selector || "").trim();
|
|
1099
|
+
if (!normalizedSelector || normalizedSelector === "-") {
|
|
1100
|
+
return true;
|
|
1101
|
+
}
|
|
1102
|
+
if (normalizedSelector === String(idValue || "").trim()) {
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
return matchesRunnerRouteText(labelValue, normalizedSelector);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function findConfiguredRoutesForAnonymousStateKey(parsedKey, runnerConfig) {
|
|
1109
|
+
const routeKey = safeObject(parsedKey);
|
|
1110
|
+
const enabledRoutes = ensureArray(runnerConfig?.routes)
|
|
1111
|
+
.map((route) => normalizeRunnerRoute(route))
|
|
1112
|
+
.filter((route) => route.enabled);
|
|
1113
|
+
return enabledRoutes.filter((route) => {
|
|
1114
|
+
if (route.projectID !== routeKey.projectID) return false;
|
|
1115
|
+
if (route.provider !== routeKey.provider) return false;
|
|
1116
|
+
if (route.role !== routeKey.role) return false;
|
|
1117
|
+
if (!runnerStateSelectorMatchesRoute(routeKey.botSelector, route.botID, route.botName)) {
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
1120
|
+
if (!runnerStateSelectorMatchesRoute(routeKey.destinationSelector, route.destinationID, route.destinationLabel)) {
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
return true;
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function configuredRunnerRouteKeys(runnerConfig) {
|
|
1128
|
+
return new Set(
|
|
1129
|
+
ensureArray(runnerConfig?.routes)
|
|
1130
|
+
.map((route) => normalizeRunnerRoute(route))
|
|
1131
|
+
.filter((route) => route.enabled)
|
|
1132
|
+
.map((route) => runnerRouteKey(route)),
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function runnerStateCursorRank(state) {
|
|
1137
|
+
const record = safeObject(state);
|
|
1138
|
+
const processedAt = Date.parse(String(record.last_processed_created_at || "").trim());
|
|
1139
|
+
const updatedAt = Date.parse(String(record.updated_at || "").trim());
|
|
1140
|
+
return {
|
|
1141
|
+
processedAt: Number.isFinite(processedAt) ? processedAt : 0,
|
|
1142
|
+
updatedAt: Number.isFinite(updatedAt) ? updatedAt : 0,
|
|
1143
|
+
providerUpdateID: intFromRawAllowZero(record.last_provider_update_id, 0),
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function prefersRunnerStateRecord(candidate, current) {
|
|
1148
|
+
const candidateRank = runnerStateCursorRank(candidate);
|
|
1149
|
+
const currentRank = runnerStateCursorRank(current);
|
|
1150
|
+
if (candidateRank.processedAt !== currentRank.processedAt) {
|
|
1151
|
+
return candidateRank.processedAt > currentRank.processedAt;
|
|
1152
|
+
}
|
|
1153
|
+
if (candidateRank.providerUpdateID !== currentRank.providerUpdateID) {
|
|
1154
|
+
return candidateRank.providerUpdateID > currentRank.providerUpdateID;
|
|
1155
|
+
}
|
|
1156
|
+
if (candidateRank.updatedAt !== currentRank.updatedAt) {
|
|
1157
|
+
return candidateRank.updatedAt > currentRank.updatedAt;
|
|
1158
|
+
}
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function mergeRunnerStateRecords(preferred, fallback) {
|
|
1163
|
+
const primary = safeObject(preferred);
|
|
1164
|
+
const secondary = safeObject(fallback);
|
|
1165
|
+
const pickString = (...values) => firstNonEmptyString(values);
|
|
1166
|
+
const pickNumber = (...values) => {
|
|
1167
|
+
for (const value of values) {
|
|
1168
|
+
if (value === 0 || value === "0") return 0;
|
|
1169
|
+
const parsed = intFromRawAllowZero(value, Number.NaN);
|
|
1170
|
+
if (Number.isFinite(parsed) && !Number.isNaN(parsed) && parsed > 0) {
|
|
1171
|
+
return parsed;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return 0;
|
|
1175
|
+
};
|
|
1176
|
+
return {
|
|
1177
|
+
last_processed_comment_id: pickString(primary.last_processed_comment_id, secondary.last_processed_comment_id),
|
|
1178
|
+
last_processed_created_at: pickString(primary.last_processed_created_at, secondary.last_processed_created_at),
|
|
1179
|
+
last_source_message_id: pickNumber(primary.last_source_message_id, secondary.last_source_message_id) || undefined,
|
|
1180
|
+
last_source_kind: pickString(primary.last_source_kind, secondary.last_source_kind),
|
|
1181
|
+
last_error: pickString(primary.last_error, secondary.last_error),
|
|
1182
|
+
updated_at: pickString(primary.updated_at, secondary.updated_at, new Date().toISOString()),
|
|
1183
|
+
last_action: pickString(primary.last_action, secondary.last_action),
|
|
1184
|
+
last_reason: pickString(primary.last_reason, secondary.last_reason),
|
|
1185
|
+
last_reply_message_id: pickNumber(primary.last_reply_message_id, secondary.last_reply_message_id) || undefined,
|
|
1186
|
+
local_receive_mode: pickString(primary.local_receive_mode, secondary.local_receive_mode),
|
|
1187
|
+
local_telegram_polling_ready: Boolean(primary.local_telegram_polling_ready || secondary.local_telegram_polling_ready),
|
|
1188
|
+
local_telegram_webhook_cleared_url: pickString(primary.local_telegram_webhook_cleared_url, secondary.local_telegram_webhook_cleared_url),
|
|
1189
|
+
local_telegram_webhook_cleared_at: pickString(primary.local_telegram_webhook_cleared_at, secondary.local_telegram_webhook_cleared_at),
|
|
1190
|
+
last_provider_update_id: pickNumber(primary.last_provider_update_id, secondary.last_provider_update_id) || undefined,
|
|
1191
|
+
last_local_poll_at: pickString(primary.last_local_poll_at, secondary.last_local_poll_at),
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function cleanupRunnerStateRecord(record) {
|
|
1196
|
+
const cleaned = {};
|
|
1197
|
+
for (const [key, value] of Object.entries(safeObject(record))) {
|
|
1198
|
+
if (value === undefined) continue;
|
|
1199
|
+
cleaned[key] = value;
|
|
1200
|
+
}
|
|
1201
|
+
return cleaned;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function migrateBotRunnerStateRoutes(routes, runnerConfig) {
|
|
1205
|
+
const currentRoutes = safeObject(routes);
|
|
1206
|
+
const nextRoutes = { ...currentRoutes };
|
|
1207
|
+
const migratedKeys = [];
|
|
1208
|
+
const remainingAnonymousKeys = [];
|
|
1209
|
+
let changed = false;
|
|
1210
|
+
const configuredKeys = configuredRunnerRouteKeys(runnerConfig);
|
|
1211
|
+
|
|
1212
|
+
for (const [routeKey, routeState] of Object.entries(currentRoutes)) {
|
|
1213
|
+
const parsedKey = parseRunnerStateRouteKey(routeKey);
|
|
1214
|
+
if (!runnerStateRouteKeyLooksAnonymous(parsedKey)) {
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
const matches = findConfiguredRoutesForAnonymousStateKey(parsedKey, runnerConfig);
|
|
1218
|
+
if (matches.length !== 1) {
|
|
1219
|
+
remainingAnonymousKeys.push(routeKey);
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
const canonicalRoute = normalizeRunnerRoute(matches[0]);
|
|
1223
|
+
const canonicalKey = runnerRouteKey(canonicalRoute);
|
|
1224
|
+
if (!canonicalKey || canonicalKey === routeKey) {
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
const existingCanonicalState = safeObject(nextRoutes[canonicalKey]);
|
|
1228
|
+
const sourceState = safeObject(routeState);
|
|
1229
|
+
const preferredState = prefersRunnerStateRecord(sourceState, existingCanonicalState)
|
|
1230
|
+
? sourceState
|
|
1231
|
+
: existingCanonicalState;
|
|
1232
|
+
const fallbackState = preferredState === sourceState ? existingCanonicalState : sourceState;
|
|
1233
|
+
nextRoutes[canonicalKey] = cleanupRunnerStateRecord(
|
|
1234
|
+
mergeRunnerStateRecords(preferredState, fallbackState),
|
|
1235
|
+
);
|
|
1236
|
+
delete nextRoutes[routeKey];
|
|
1237
|
+
migratedKeys.push({
|
|
1238
|
+
from: routeKey,
|
|
1239
|
+
to: canonicalKey,
|
|
1240
|
+
});
|
|
1241
|
+
changed = true;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
for (const routeKey of Object.keys(nextRoutes)) {
|
|
1245
|
+
const parsedKey = parseRunnerStateRouteKey(routeKey);
|
|
1246
|
+
if (!runnerStateRouteKeyIsUnnamed(parsedKey)) {
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (configuredKeys.has(routeKey)) {
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
if (!remainingAnonymousKeys.includes(routeKey)) {
|
|
1253
|
+
remainingAnonymousKeys.push(routeKey);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
return {
|
|
1258
|
+
routes: nextRoutes,
|
|
1259
|
+
changed,
|
|
1260
|
+
migratedKeys,
|
|
1261
|
+
remainingAnonymousKeys,
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1063
1265
|
function loadBotRunnerState() {
|
|
1064
1266
|
const filePath = botRunnerStateFilePath();
|
|
1065
1267
|
try {
|
|
1066
1268
|
if (!fs.existsSync(filePath)) {
|
|
1067
|
-
return {
|
|
1269
|
+
return {
|
|
1270
|
+
filePath,
|
|
1271
|
+
routes: {},
|
|
1272
|
+
migrated: false,
|
|
1273
|
+
migratedKeys: [],
|
|
1274
|
+
remainingAnonymousKeys: [],
|
|
1275
|
+
};
|
|
1068
1276
|
}
|
|
1069
1277
|
const parsed = tryJsonParse(fs.readFileSync(filePath, "utf8"));
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1278
|
+
const runnerConfig = loadBotRunnerConfig({ persistIfNeeded: true });
|
|
1279
|
+
const migratedState = migrateBotRunnerStateRoutes(safeObject(parsed?.routes), runnerConfig);
|
|
1280
|
+
if (migratedState.changed) {
|
|
1281
|
+
saveBotRunnerState({ routes: migratedState.routes });
|
|
1282
|
+
}
|
|
1283
|
+
return {
|
|
1284
|
+
filePath,
|
|
1285
|
+
routes: migratedState.routes,
|
|
1286
|
+
migrated: migratedState.changed,
|
|
1287
|
+
migratedKeys: migratedState.migratedKeys,
|
|
1288
|
+
remainingAnonymousKeys: migratedState.remainingAnonymousKeys,
|
|
1289
|
+
};
|
|
1072
1290
|
} catch {
|
|
1073
|
-
return {
|
|
1291
|
+
return {
|
|
1292
|
+
filePath,
|
|
1293
|
+
routes: {},
|
|
1294
|
+
migrated: false,
|
|
1295
|
+
migratedKeys: [],
|
|
1296
|
+
remainingAnonymousKeys: [],
|
|
1297
|
+
};
|
|
1074
1298
|
}
|
|
1075
1299
|
}
|
|
1076
1300
|
|
|
@@ -1121,6 +1345,10 @@ function normalizeRunnerRoute(rawRoute, overrides = {}) {
|
|
|
1121
1345
|
const workspaceDir = String(route.workspace_dir || route.workspaceDir || "").trim();
|
|
1122
1346
|
const triggerPolicy = normalizeRunnerTriggerPolicy(route.trigger_policy || route.triggerPolicy, route);
|
|
1123
1347
|
const archivePolicy = normalizeRunnerArchivePolicy(route.archive_policy || route.archivePolicy, route);
|
|
1348
|
+
const dryRunDelivery = boolFromRaw(
|
|
1349
|
+
route.dry_run_delivery ?? route.dryRunDelivery ?? process.env.METHEUS_BOT_DELIVERY_DRY_RUN,
|
|
1350
|
+
false,
|
|
1351
|
+
);
|
|
1124
1352
|
return {
|
|
1125
1353
|
name,
|
|
1126
1354
|
enabled: boolFromRaw(route.enabled, true),
|
|
@@ -1136,6 +1364,7 @@ function normalizeRunnerRoute(rawRoute, overrides = {}) {
|
|
|
1136
1364
|
archiveWorkItemID,
|
|
1137
1365
|
triggerPolicy,
|
|
1138
1366
|
archivePolicy,
|
|
1367
|
+
dryRunDelivery,
|
|
1139
1368
|
workspaceDir,
|
|
1140
1369
|
command,
|
|
1141
1370
|
pollIntervalMs: intFromRaw(route.poll_interval_ms || route.pollIntervalMs, 5000),
|
|
@@ -1195,6 +1424,161 @@ function validateRunnerRoute(route, { requireCommand = true, config = null } = {
|
|
|
1195
1424
|
return errors;
|
|
1196
1425
|
}
|
|
1197
1426
|
|
|
1427
|
+
function hasRunnerFlag(flags, flagName) {
|
|
1428
|
+
return Object.prototype.hasOwnProperty.call(flags, flagName);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function matchesRunnerRouteText(candidate, expected) {
|
|
1432
|
+
const expectedText = String(expected || "").trim();
|
|
1433
|
+
if (!expectedText) {
|
|
1434
|
+
return true;
|
|
1435
|
+
}
|
|
1436
|
+
return String(candidate || "").trim().toLowerCase() === expectedText.toLowerCase();
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function configuredRunnerRouteMatchesInlineSelection(route, inlineRoute, flags) {
|
|
1440
|
+
const candidate = normalizeRunnerRoute(route);
|
|
1441
|
+
if (hasRunnerFlag(flags, "route-name") && !matchesRunnerRouteText(candidate.name, flags["route-name"])) {
|
|
1442
|
+
return false;
|
|
1443
|
+
}
|
|
1444
|
+
if (inlineRoute.projectID && candidate.projectID !== inlineRoute.projectID) {
|
|
1445
|
+
return false;
|
|
1446
|
+
}
|
|
1447
|
+
if (inlineRoute.provider && candidate.provider !== inlineRoute.provider) {
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
if (inlineRoute.role && candidate.role !== inlineRoute.role) {
|
|
1451
|
+
return false;
|
|
1452
|
+
}
|
|
1453
|
+
if (hasRunnerFlag(flags, "role-profile") && candidate.roleProfile !== inlineRoute.roleProfile) {
|
|
1454
|
+
return false;
|
|
1455
|
+
}
|
|
1456
|
+
if (inlineRoute.botID && candidate.botID !== inlineRoute.botID) {
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1459
|
+
if (inlineRoute.botName && !matchesRunnerRouteText(candidate.botName, inlineRoute.botName)) {
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
if (inlineRoute.destinationID && candidate.destinationID !== inlineRoute.destinationID) {
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
if (inlineRoute.destinationLabel && !matchesRunnerRouteText(candidate.destinationLabel, inlineRoute.destinationLabel)) {
|
|
1466
|
+
return false;
|
|
1467
|
+
}
|
|
1468
|
+
if (inlineRoute.archiveThreadID && candidate.archiveThreadID !== inlineRoute.archiveThreadID) {
|
|
1469
|
+
return false;
|
|
1470
|
+
}
|
|
1471
|
+
if (inlineRoute.archiveWorkItemID && candidate.archiveWorkItemID !== inlineRoute.archiveWorkItemID) {
|
|
1472
|
+
return false;
|
|
1473
|
+
}
|
|
1474
|
+
return true;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function applyRunnerRouteFlagOverrides(route, flags) {
|
|
1478
|
+
const merged = normalizeRunnerRoute(route);
|
|
1479
|
+
if (hasRunnerFlag(flags, "route-name")) {
|
|
1480
|
+
merged.name = String(flags["route-name"] || "").trim();
|
|
1481
|
+
}
|
|
1482
|
+
if (hasRunnerFlag(flags, "project-id")) {
|
|
1483
|
+
merged.projectID = String(flags["project-id"] || "").trim();
|
|
1484
|
+
}
|
|
1485
|
+
if (hasRunnerFlag(flags, "provider")) {
|
|
1486
|
+
merged.provider = normalizeBotProvider(flags.provider);
|
|
1487
|
+
}
|
|
1488
|
+
if (hasRunnerFlag(flags, "role")) {
|
|
1489
|
+
merged.role = normalizeBotRole(flags.role);
|
|
1490
|
+
}
|
|
1491
|
+
if (hasRunnerFlag(flags, "role-profile")) {
|
|
1492
|
+
merged.roleProfile = normalizeRunnerRoleProfileName(flags["role-profile"]);
|
|
1493
|
+
}
|
|
1494
|
+
if (hasRunnerFlag(flags, "bot-name")) {
|
|
1495
|
+
merged.botName = String(flags["bot-name"] || "").trim();
|
|
1496
|
+
}
|
|
1497
|
+
if (hasRunnerFlag(flags, "bot-id")) {
|
|
1498
|
+
merged.botID = String(flags["bot-id"] || "").trim();
|
|
1499
|
+
}
|
|
1500
|
+
if (hasRunnerFlag(flags, "destination-id")) {
|
|
1501
|
+
merged.destinationID = String(flags["destination-id"] || "").trim();
|
|
1502
|
+
}
|
|
1503
|
+
if (hasRunnerFlag(flags, "destination-label")) {
|
|
1504
|
+
merged.destinationLabel = String(flags["destination-label"] || "").trim();
|
|
1505
|
+
}
|
|
1506
|
+
if (hasRunnerFlag(flags, "archive-thread-id")) {
|
|
1507
|
+
merged.archiveThreadID = String(flags["archive-thread-id"] || "").trim();
|
|
1508
|
+
}
|
|
1509
|
+
if (hasRunnerFlag(flags, "archive-work-item-id")) {
|
|
1510
|
+
merged.archiveWorkItemID = String(flags["archive-work-item-id"] || "").trim();
|
|
1511
|
+
}
|
|
1512
|
+
if (hasRunnerFlag(flags, "workspace-dir")) {
|
|
1513
|
+
merged.workspaceDir = String(flags["workspace-dir"] || "").trim();
|
|
1514
|
+
}
|
|
1515
|
+
if (hasRunnerFlag(flags, "command")) {
|
|
1516
|
+
merged.command = String(flags.command || "").trim();
|
|
1517
|
+
}
|
|
1518
|
+
if (hasRunnerFlag(flags, "dry-run-delivery")) {
|
|
1519
|
+
merged.dryRunDelivery = boolFromRaw(flags["dry-run-delivery"], merged.dryRunDelivery);
|
|
1520
|
+
}
|
|
1521
|
+
if (hasRunnerFlag(flags, "poll-interval-ms")) {
|
|
1522
|
+
merged.pollIntervalMs = intFromRaw(flags["poll-interval-ms"], merged.pollIntervalMs);
|
|
1523
|
+
}
|
|
1524
|
+
if (hasRunnerFlag(flags, "context-comments")) {
|
|
1525
|
+
merged.contextComments = intFromRaw(flags["context-comments"], merged.contextComments);
|
|
1526
|
+
}
|
|
1527
|
+
if (hasRunnerFlag(flags, "archive-replies")) {
|
|
1528
|
+
const archiveReplies = boolFromRaw(flags["archive-replies"], merged.archiveReplies);
|
|
1529
|
+
merged.archiveReplies = archiveReplies;
|
|
1530
|
+
merged.archivePolicy = {
|
|
1531
|
+
...safeObject(merged.archivePolicy),
|
|
1532
|
+
mirrorReplies: archiveReplies,
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
if (
|
|
1536
|
+
hasRunnerFlag(flags, "mentions-only")
|
|
1537
|
+
|| hasRunnerFlag(flags, "direct-messages")
|
|
1538
|
+
|| hasRunnerFlag(flags, "reply-to-bot-messages")
|
|
1539
|
+
|| hasRunnerFlag(flags, "ignore-edited-messages")
|
|
1540
|
+
) {
|
|
1541
|
+
merged.triggerPolicy = {
|
|
1542
|
+
...safeObject(merged.triggerPolicy),
|
|
1543
|
+
...(hasRunnerFlag(flags, "mentions-only")
|
|
1544
|
+
? { mentionsOnly: boolFromRaw(flags["mentions-only"], merged.triggerPolicy?.mentionsOnly) }
|
|
1545
|
+
: {}),
|
|
1546
|
+
...(hasRunnerFlag(flags, "direct-messages")
|
|
1547
|
+
? { directMessages: boolFromRaw(flags["direct-messages"], merged.triggerPolicy?.directMessages) }
|
|
1548
|
+
: {}),
|
|
1549
|
+
...(hasRunnerFlag(flags, "reply-to-bot-messages")
|
|
1550
|
+
? { replyToBotMessages: boolFromRaw(flags["reply-to-bot-messages"], merged.triggerPolicy?.replyToBotMessages) }
|
|
1551
|
+
: {}),
|
|
1552
|
+
...(hasRunnerFlag(flags, "ignore-edited-messages")
|
|
1553
|
+
? { ignoreEditedMessages: boolFromRaw(flags["ignore-edited-messages"], merged.triggerPolicy?.ignoreEditedMessages) }
|
|
1554
|
+
: {}),
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
if (
|
|
1558
|
+
hasRunnerFlag(flags, "dedupe-inbound")
|
|
1559
|
+
|| hasRunnerFlag(flags, "dedupe-outbound")
|
|
1560
|
+
|| hasRunnerFlag(flags, "skip-bot-messages")
|
|
1561
|
+
|| hasRunnerFlag(flags, "archive-replies")
|
|
1562
|
+
) {
|
|
1563
|
+
merged.archivePolicy = {
|
|
1564
|
+
...safeObject(merged.archivePolicy),
|
|
1565
|
+
...(hasRunnerFlag(flags, "archive-replies")
|
|
1566
|
+
? { mirrorReplies: boolFromRaw(flags["archive-replies"], merged.archivePolicy?.mirrorReplies) }
|
|
1567
|
+
: {}),
|
|
1568
|
+
...(hasRunnerFlag(flags, "dedupe-inbound")
|
|
1569
|
+
? { dedupeInbound: boolFromRaw(flags["dedupe-inbound"], merged.archivePolicy?.dedupeInbound) }
|
|
1570
|
+
: {}),
|
|
1571
|
+
...(hasRunnerFlag(flags, "dedupe-outbound")
|
|
1572
|
+
? { dedupeOutbound: boolFromRaw(flags["dedupe-outbound"], merged.archivePolicy?.dedupeOutbound) }
|
|
1573
|
+
: {}),
|
|
1574
|
+
...(hasRunnerFlag(flags, "skip-bot-messages")
|
|
1575
|
+
? { skipBotMessages: boolFromRaw(flags["skip-bot-messages"], merged.archivePolicy?.skipBotMessages) }
|
|
1576
|
+
: {}),
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
return merged;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1198
1582
|
function resolveRunnerRoutes(flags, mode) {
|
|
1199
1583
|
const inlineRoute = normalizeRunnerRoute({
|
|
1200
1584
|
name: flags["route-name"],
|
|
@@ -1221,45 +1605,76 @@ function resolveRunnerRoutes(flags, mode) {
|
|
|
1221
1605
|
dedupe_outbound: flags["dedupe-outbound"],
|
|
1222
1606
|
skip_bot_messages: flags["skip-bot-messages"],
|
|
1223
1607
|
},
|
|
1608
|
+
dry_run_delivery: flags["dry-run-delivery"],
|
|
1224
1609
|
workspace_dir: flags["workspace-dir"],
|
|
1225
1610
|
command: flags.command,
|
|
1226
1611
|
poll_interval_ms: flags["poll-interval-ms"],
|
|
1227
1612
|
context_comments: flags["context-comments"],
|
|
1228
1613
|
archive_replies: flags["archive-replies"],
|
|
1229
1614
|
});
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1232
|
-
|| inlineRoute.
|
|
1615
|
+
const selectionRequested = Boolean(
|
|
1616
|
+
String(flags["route-name"] || "").trim()
|
|
1617
|
+
|| inlineRoute.projectID
|
|
1233
1618
|
|| inlineRoute.botID
|
|
1234
1619
|
|| inlineRoute.botName
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1620
|
+
|| inlineRoute.roleProfile
|
|
1621
|
+
|| inlineRoute.destinationID
|
|
1622
|
+
|| inlineRoute.destinationLabel
|
|
1623
|
+
|| inlineRoute.archiveThreadID
|
|
1624
|
+
|| inlineRoute.archiveWorkItemID
|
|
1625
|
+
|| flags.provider
|
|
1626
|
+
|| flags.role
|
|
1627
|
+
);
|
|
1628
|
+
const overrideRequested = Boolean(
|
|
1629
|
+
inlineRoute.command
|
|
1630
|
+
|| inlineRoute.workspaceDir
|
|
1631
|
+
|| hasRunnerFlag(flags, "mentions-only")
|
|
1632
|
+
|| hasRunnerFlag(flags, "direct-messages")
|
|
1633
|
+
|| hasRunnerFlag(flags, "reply-to-bot-messages")
|
|
1634
|
+
|| hasRunnerFlag(flags, "ignore-edited-messages")
|
|
1635
|
+
|| hasRunnerFlag(flags, "dry-run-delivery")
|
|
1636
|
+
|| hasRunnerFlag(flags, "dedupe-inbound")
|
|
1637
|
+
|| hasRunnerFlag(flags, "dedupe-outbound")
|
|
1638
|
+
|| hasRunnerFlag(flags, "skip-bot-messages")
|
|
1639
|
+
|| hasRunnerFlag(flags, "archive-replies")
|
|
1640
|
+
|| hasRunnerFlag(flags, "poll-interval-ms")
|
|
1641
|
+
|| hasRunnerFlag(flags, "context-comments")
|
|
1642
|
+
);
|
|
1643
|
+
const inlineRequested = Boolean(
|
|
1644
|
+
selectionRequested
|
|
1645
|
+
|| overrideRequested
|
|
1646
|
+
|| hasRunnerFlag(flags, "project-id")
|
|
1647
|
+
|| inlineRoute.projectID
|
|
1250
1648
|
);
|
|
1251
|
-
if (inlineRequested) {
|
|
1252
|
-
return [inlineRoute];
|
|
1253
|
-
}
|
|
1254
1649
|
|
|
1255
1650
|
const config = loadBotRunnerConfig({ persistIfNeeded: true });
|
|
1256
1651
|
const routeNameFilter = String(flags["route-name"] || "").trim().toLowerCase();
|
|
1257
|
-
const
|
|
1652
|
+
const configuredRoutes = ensureArray(config.routes)
|
|
1258
1653
|
.map((rawRoute) => normalizeRunnerRoute(rawRoute))
|
|
1259
1654
|
.filter((route) => route.enabled)
|
|
1260
1655
|
.filter((route) => !routeNameFilter || String(route.name || "").trim().toLowerCase() === routeNameFilter);
|
|
1261
|
-
|
|
1262
|
-
|
|
1656
|
+
|
|
1657
|
+
if (selectionRequested) {
|
|
1658
|
+
const matchedRoutes = configuredRoutes.filter((route) => configuredRunnerRouteMatchesInlineSelection(route, inlineRoute, flags));
|
|
1659
|
+
if (matchedRoutes.length === 1) {
|
|
1660
|
+
return [applyRunnerRouteFlagOverrides(matchedRoutes[0], flags)];
|
|
1661
|
+
}
|
|
1662
|
+
if (matchedRoutes.length > 1) {
|
|
1663
|
+
const matchList = matchedRoutes
|
|
1664
|
+
.map((route) => route.name || runnerRouteKey(route))
|
|
1665
|
+
.join(", ");
|
|
1666
|
+
throw new Error(
|
|
1667
|
+
`Multiple enabled runner routes matched the provided filters. Narrow with --route-name, --bot-name, or --destination-label. Matches: ${matchList}`,
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (inlineRequested) {
|
|
1673
|
+
return [inlineRoute];
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (configuredRoutes.length > 0) {
|
|
1677
|
+
return configuredRoutes;
|
|
1263
1678
|
}
|
|
1264
1679
|
|
|
1265
1680
|
const help = routeNameFilter
|
|
@@ -2474,6 +2889,7 @@ function buildRunnerInputPayload({
|
|
|
2474
2889
|
workspace_dir: String(executionPlan?.workspaceDir || "").trim(),
|
|
2475
2890
|
workspace_source: String(executionPlan?.workspaceSource || "").trim(),
|
|
2476
2891
|
command_fallback: executionPlan?.usedCommandFallback === true,
|
|
2892
|
+
dry_run_delivery: Boolean(route.dryRunDelivery),
|
|
2477
2893
|
},
|
|
2478
2894
|
trigger_policy: serializeRunnerTriggerPolicy(route.triggerPolicy),
|
|
2479
2895
|
archive_policy: serializeRunnerArchivePolicy(route.archivePolicy),
|
|
@@ -2513,6 +2929,9 @@ function printRunnerResult(mode, result, jsonMode) {
|
|
|
2513
2929
|
if (result.detail) {
|
|
2514
2930
|
process.stdout.write(` detail: ${result.detail}\n`);
|
|
2515
2931
|
}
|
|
2932
|
+
if (result.delivery?.dry_run) {
|
|
2933
|
+
process.stdout.write(" dry_run_delivery: true\n");
|
|
2934
|
+
}
|
|
2516
2935
|
if (result.thread_id) {
|
|
2517
2936
|
process.stdout.write(` thread: ${result.thread_id}\n`);
|
|
2518
2937
|
}
|
|
@@ -2692,13 +3111,17 @@ async function processRunnerRouteOnce(route, runtime, mode) {
|
|
|
2692
3111
|
contextWindow,
|
|
2693
3112
|
executionPlan,
|
|
2694
3113
|
});
|
|
2695
|
-
const typingHeartbeat =
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
3114
|
+
const typingHeartbeat = normalizedRoute.dryRunDelivery
|
|
3115
|
+
? {
|
|
3116
|
+
async stop() {},
|
|
3117
|
+
}
|
|
3118
|
+
: startRunnerTypingHeartbeat({
|
|
3119
|
+
provider: normalizedRoute.provider,
|
|
3120
|
+
destination,
|
|
3121
|
+
timeoutSeconds: runtime.timeoutSeconds,
|
|
3122
|
+
action: "typing",
|
|
3123
|
+
intervalMs: 4000,
|
|
3124
|
+
});
|
|
2702
3125
|
let aiResult;
|
|
2703
3126
|
try {
|
|
2704
3127
|
aiResult = await runRunnerAIExecution({
|
|
@@ -2752,6 +3175,7 @@ async function processRunnerRouteOnce(route, runtime, mode) {
|
|
|
2752
3175
|
archiveDedupeOutbound: normalizedRoute.archivePolicy.dedupeOutbound,
|
|
2753
3176
|
archiveThreadID: archiveThread.threadID,
|
|
2754
3177
|
archiveWorkItemID: archiveThread.workItemID,
|
|
3178
|
+
dryRun: normalizedRoute.dryRunDelivery,
|
|
2755
3179
|
});
|
|
2756
3180
|
saveRunnerRouteState(
|
|
2757
3181
|
routeKey,
|
|
@@ -2766,8 +3190,8 @@ async function processRunnerRouteOnce(route, runtime, mode) {
|
|
|
2766
3190
|
return {
|
|
2767
3191
|
route_key: routeKey,
|
|
2768
3192
|
route_name: normalizedRoute.name,
|
|
2769
|
-
outcome: "replied",
|
|
2770
|
-
detail:
|
|
3193
|
+
outcome: deliveryResult.delivery.dryRun ? "dry_run" : "replied",
|
|
3194
|
+
detail: `${deliveryResult.delivery.dryRun ? "dry-run prepared" : "replied"} as ${bot.name || bot.id}${executionPlan.usedCommandFallback ? " (legacy command fallback)" : ""}`,
|
|
2771
3195
|
thread_id: archiveThread.threadID,
|
|
2772
3196
|
comment_id: selectedRecord.id,
|
|
2773
3197
|
execution_mode: executionPlan.mode,
|
|
@@ -4857,6 +5281,9 @@ function collectRunnerRouteDiagnostics(route, runnerConfig) {
|
|
|
4857
5281
|
if (normalizedRoute.command) {
|
|
4858
5282
|
warnings.push("legacy command fallback is still configured");
|
|
4859
5283
|
}
|
|
5284
|
+
if (normalizedRoute.dryRunDelivery) {
|
|
5285
|
+
warnings.push("dry_run_delivery is enabled");
|
|
5286
|
+
}
|
|
4860
5287
|
if (normalizedRoute.workspaceDir) {
|
|
4861
5288
|
warnings.push("legacy route.workspace_dir override is still configured");
|
|
4862
5289
|
}
|
|
@@ -4940,6 +5367,7 @@ async function runDoctor(flags) {
|
|
|
4940
5367
|
addDoctorCheck(rows, "warn", "local bot runner config", `${runnerTemplate.error} (${runnerTemplate.filePath})`);
|
|
4941
5368
|
} else {
|
|
4942
5369
|
const runnerConfig = loadBotRunnerConfig({ persistIfNeeded: true });
|
|
5370
|
+
const runnerState = loadBotRunnerState();
|
|
4943
5371
|
const enabledRoutes = ensureArray(runnerConfig.routes).filter((routeRaw) => safeObject(routeRaw).enabled);
|
|
4944
5372
|
if (!enabledRoutes.length) {
|
|
4945
5373
|
addDoctorCheck(rows, "warn", "local bot runner config", `template ready (${runnerTemplate.filePath}), no enabled routes`);
|
|
@@ -4996,6 +5424,24 @@ async function runDoctor(flags) {
|
|
|
4996
5424
|
}
|
|
4997
5425
|
}
|
|
4998
5426
|
}
|
|
5427
|
+
if (runnerState.migrated && runnerState.migratedKeys.length > 0) {
|
|
5428
|
+
addDoctorCheck(
|
|
5429
|
+
rows,
|
|
5430
|
+
"ok",
|
|
5431
|
+
"runner state migration",
|
|
5432
|
+
`migrated ${runnerState.migratedKeys.length} stale route key(s) in ${runnerState.filePath}`,
|
|
5433
|
+
);
|
|
5434
|
+
}
|
|
5435
|
+
if (runnerState.remainingAnonymousKeys.length > 0) {
|
|
5436
|
+
addDoctorCheck(
|
|
5437
|
+
rows,
|
|
5438
|
+
"warn",
|
|
5439
|
+
"runner state migration",
|
|
5440
|
+
`anonymous route keys remain in ${runnerState.filePath}: ${runnerState.remainingAnonymousKeys.join(", ")}`,
|
|
5441
|
+
);
|
|
5442
|
+
} else {
|
|
5443
|
+
addDoctorCheck(rows, "ok", "runner state migration", "no stale anonymous route keys detected");
|
|
5444
|
+
}
|
|
4999
5445
|
}
|
|
5000
5446
|
|
|
5001
5447
|
const resolved = await resolveAccessTokenForCommand(context.baseURL, timeoutSeconds);
|
|
@@ -6855,6 +7301,10 @@ function patchServerToolSpecsForLocalRouting(tools) {
|
|
|
6855
7301
|
type: "string",
|
|
6856
7302
|
description: "Optional project chat destination label when a project has multiple active destinations.",
|
|
6857
7303
|
},
|
|
7304
|
+
dry_run_delivery: {
|
|
7305
|
+
type: "boolean",
|
|
7306
|
+
description: "If true, resolve bot and destination locally but do not send the provider message or mirror an archive reply.",
|
|
7307
|
+
},
|
|
6858
7308
|
},
|
|
6859
7309
|
};
|
|
6860
7310
|
return {
|
|
@@ -6966,6 +7416,7 @@ async function performLocalBotDelivery({
|
|
|
6966
7416
|
archiveDedupeOutbound = true,
|
|
6967
7417
|
archiveThreadID = "",
|
|
6968
7418
|
archiveWorkItemID = "",
|
|
7419
|
+
dryRun = false,
|
|
6969
7420
|
}) {
|
|
6970
7421
|
const normalizedProvider = normalizeBotProvider(provider);
|
|
6971
7422
|
const destinations = await listProjectChatDestinations({
|
|
@@ -6980,15 +7431,35 @@ async function performLocalBotDelivery({
|
|
|
6980
7431
|
throw new Error(`local ${normalizedProvider} env is not ready (${providerEnv.error})`);
|
|
6981
7432
|
}
|
|
6982
7433
|
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
7434
|
+
let delivery;
|
|
7435
|
+
if (dryRun) {
|
|
7436
|
+
delivery = {
|
|
7437
|
+
statusCode: 204,
|
|
7438
|
+
body: {
|
|
7439
|
+
ok: true,
|
|
7440
|
+
dry_run: true,
|
|
7441
|
+
provider: normalizedProvider,
|
|
7442
|
+
text,
|
|
7443
|
+
disable_web_page_preview: Boolean(disableWebPagePreview),
|
|
7444
|
+
reply_to_message_id: intFromRawAllowZero(replyToMessageID, 0),
|
|
7445
|
+
},
|
|
7446
|
+
ok: true,
|
|
7447
|
+
url: "",
|
|
7448
|
+
dryRun: true,
|
|
7449
|
+
effectiveReplyToMessageID: intFromRawAllowZero(replyToMessageID, 0),
|
|
7450
|
+
replySupported: normalizedProvider === "telegram",
|
|
7451
|
+
};
|
|
7452
|
+
} else {
|
|
7453
|
+
delivery = await deliverLocalProviderMessage({
|
|
7454
|
+
provider: normalizedProvider,
|
|
7455
|
+
token: providerEnv.token,
|
|
7456
|
+
destination,
|
|
7457
|
+
text,
|
|
7458
|
+
disableWebPagePreview,
|
|
7459
|
+
replyToMessageID,
|
|
7460
|
+
timeoutSeconds,
|
|
7461
|
+
});
|
|
7462
|
+
}
|
|
6992
7463
|
if (!delivery.ok) {
|
|
6993
7464
|
const responseJSON = safeObject(delivery.body);
|
|
6994
7465
|
const errorDetail =
|
|
@@ -7016,6 +7487,22 @@ async function performLocalBotDelivery({
|
|
|
7016
7487
|
archiveThreadID,
|
|
7017
7488
|
archiveWorkItemID,
|
|
7018
7489
|
});
|
|
7490
|
+
if (dryRun) {
|
|
7491
|
+
archive = {
|
|
7492
|
+
ok: true,
|
|
7493
|
+
dry_run: true,
|
|
7494
|
+
skipped: true,
|
|
7495
|
+
thread_id: thread.threadID,
|
|
7496
|
+
work_item_id: thread.workItemID,
|
|
7497
|
+
reason: "dry_run_delivery",
|
|
7498
|
+
};
|
|
7499
|
+
return {
|
|
7500
|
+
provider: normalizedProvider,
|
|
7501
|
+
destination,
|
|
7502
|
+
delivery,
|
|
7503
|
+
archive,
|
|
7504
|
+
};
|
|
7505
|
+
}
|
|
7019
7506
|
const deliveredBody = safeObject(delivery.body);
|
|
7020
7507
|
const deliveredResult = safeObject(deliveredBody.result || deliveredBody.message || deliveredBody.data);
|
|
7021
7508
|
const deliveredMessageID = intFromRawAllowZero(
|
|
@@ -7154,6 +7641,12 @@ async function handleLocalBotMessageToolCall({
|
|
|
7154
7641
|
: toolArgs.archiveReplies,
|
|
7155
7642
|
true,
|
|
7156
7643
|
);
|
|
7644
|
+
const dryRunDelivery = boolFromRaw(
|
|
7645
|
+
Object.prototype.hasOwnProperty.call(toolArgs, "dry_run_delivery")
|
|
7646
|
+
? toolArgs.dry_run_delivery
|
|
7647
|
+
: toolArgs.dryRunDelivery,
|
|
7648
|
+
false,
|
|
7649
|
+
);
|
|
7157
7650
|
const result = await performLocalBotDelivery({
|
|
7158
7651
|
siteBaseURL,
|
|
7159
7652
|
token,
|
|
@@ -7174,6 +7667,7 @@ async function handleLocalBotMessageToolCall({
|
|
|
7174
7667
|
disableWebPagePreview,
|
|
7175
7668
|
replyToMessageID,
|
|
7176
7669
|
archiveReplies,
|
|
7670
|
+
dryRun: dryRunDelivery,
|
|
7177
7671
|
});
|
|
7178
7672
|
|
|
7179
7673
|
return jsonRpcResult(
|
|
@@ -7192,6 +7686,7 @@ async function handleLocalBotMessageToolCall({
|
|
|
7192
7686
|
status: result.delivery.statusCode,
|
|
7193
7687
|
url: result.delivery.url,
|
|
7194
7688
|
body: result.delivery.body,
|
|
7689
|
+
dry_run_delivery: Boolean(result.delivery.dryRun),
|
|
7195
7690
|
reply_supported: result.delivery.replySupported,
|
|
7196
7691
|
archive: result.archive,
|
|
7197
7692
|
}),
|
|
@@ -9122,6 +9617,176 @@ async function runSelftest(flags = {}) {
|
|
|
9122
9617
|
`mode=${commandFallbackPlan.mode} fallback=${String(commandFallbackPlan.fallbackReason || "(none)")}`,
|
|
9123
9618
|
);
|
|
9124
9619
|
|
|
9620
|
+
let runnerRouteResolveTempRoot = "";
|
|
9621
|
+
let originalResolveHome;
|
|
9622
|
+
let originalResolveUserProfile;
|
|
9623
|
+
try {
|
|
9624
|
+
runnerRouteResolveTempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-route-selftest-"));
|
|
9625
|
+
originalResolveHome = process.env.HOME;
|
|
9626
|
+
originalResolveUserProfile = process.env.USERPROFILE;
|
|
9627
|
+
const resolveHome = path.join(runnerRouteResolveTempRoot, "home");
|
|
9628
|
+
const resolveMetheusDir = path.join(resolveHome, ".metheus");
|
|
9629
|
+
fs.mkdirSync(resolveMetheusDir, { recursive: true });
|
|
9630
|
+
fs.writeFileSync(
|
|
9631
|
+
path.join(resolveMetheusDir, "bot-runner.json"),
|
|
9632
|
+
`${JSON.stringify({
|
|
9633
|
+
version: 2,
|
|
9634
|
+
project_mappings: {
|
|
9635
|
+
[selftestProjectID]: {
|
|
9636
|
+
workspace_dir: path.join(runnerRouteResolveTempRoot, "workspace"),
|
|
9637
|
+
source: "selftest",
|
|
9638
|
+
},
|
|
9639
|
+
},
|
|
9640
|
+
role_profiles: defaultBotRunnerRoleProfiles(),
|
|
9641
|
+
routes: [
|
|
9642
|
+
{
|
|
9643
|
+
name: "telegram-monitor",
|
|
9644
|
+
enabled: true,
|
|
9645
|
+
project_id: selftestProjectID,
|
|
9646
|
+
provider: "telegram",
|
|
9647
|
+
role: "monitor",
|
|
9648
|
+
role_profile: "monitor",
|
|
9649
|
+
bot_name: "RyoAI_bot",
|
|
9650
|
+
destination_label: "AI incubating CHAT ROOM",
|
|
9651
|
+
archive_work_item_id: "304ce77a-7032-421c-aeda-bc54daf088dd",
|
|
9652
|
+
},
|
|
9653
|
+
],
|
|
9654
|
+
}, null, 2)}\n`,
|
|
9655
|
+
"utf8",
|
|
9656
|
+
);
|
|
9657
|
+
process.env.HOME = resolveHome;
|
|
9658
|
+
process.env.USERPROFILE = resolveHome;
|
|
9659
|
+
const resolvedRunnerRoutes = resolveRunnerRoutes(
|
|
9660
|
+
{
|
|
9661
|
+
"project-id": selftestProjectID,
|
|
9662
|
+
provider: "telegram",
|
|
9663
|
+
role: "monitor",
|
|
9664
|
+
"role-profile": "monitor",
|
|
9665
|
+
"bot-name": "RyoAI_bot",
|
|
9666
|
+
"dry-run-delivery": true,
|
|
9667
|
+
},
|
|
9668
|
+
"once",
|
|
9669
|
+
);
|
|
9670
|
+
const resolvedRunnerRoute = normalizeRunnerRoute(resolvedRunnerRoutes[0]);
|
|
9671
|
+
push(
|
|
9672
|
+
"bot_runner_inline_filters_reuse_configured_route",
|
|
9673
|
+
resolvedRunnerRoutes.length === 1
|
|
9674
|
+
&& resolvedRunnerRoute.name === "telegram-monitor"
|
|
9675
|
+
&& resolvedRunnerRoute.destinationLabel === "AI incubating CHAT ROOM"
|
|
9676
|
+
&& resolvedRunnerRoute.dryRunDelivery === true
|
|
9677
|
+
&& runnerRouteKey(resolvedRunnerRoute) === "telegram-monitor::11111111-1111-1111-1111-111111111111::telegram::monitor::RyoAI_bot::AI incubating CHAT ROOM",
|
|
9678
|
+
`name=${resolvedRunnerRoute.name || "(none)"} destination=${resolvedRunnerRoute.destinationLabel || "(none)"} key=${runnerRouteKey(resolvedRunnerRoute)}`,
|
|
9679
|
+
);
|
|
9680
|
+
} catch (err) {
|
|
9681
|
+
push("bot_runner_inline_filters_reuse_configured_route", false, String(err?.message || err));
|
|
9682
|
+
} finally {
|
|
9683
|
+
if (typeof originalResolveHome === "string") {
|
|
9684
|
+
process.env.HOME = originalResolveHome;
|
|
9685
|
+
} else {
|
|
9686
|
+
delete process.env.HOME;
|
|
9687
|
+
}
|
|
9688
|
+
if (typeof originalResolveUserProfile === "string") {
|
|
9689
|
+
process.env.USERPROFILE = originalResolveUserProfile;
|
|
9690
|
+
} else {
|
|
9691
|
+
delete process.env.USERPROFILE;
|
|
9692
|
+
}
|
|
9693
|
+
if (runnerRouteResolveTempRoot) {
|
|
9694
|
+
try {
|
|
9695
|
+
fs.rmSync(runnerRouteResolveTempRoot, { recursive: true, force: true });
|
|
9696
|
+
} catch {}
|
|
9697
|
+
}
|
|
9698
|
+
}
|
|
9699
|
+
|
|
9700
|
+
let runnerStateMigrationTempRoot = "";
|
|
9701
|
+
let originalStateMigrationHome;
|
|
9702
|
+
let originalStateMigrationUserProfile;
|
|
9703
|
+
try {
|
|
9704
|
+
runnerStateMigrationTempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-state-selftest-"));
|
|
9705
|
+
originalStateMigrationHome = process.env.HOME;
|
|
9706
|
+
originalStateMigrationUserProfile = process.env.USERPROFILE;
|
|
9707
|
+
const migrationHome = path.join(runnerStateMigrationTempRoot, "home");
|
|
9708
|
+
const migrationMetheusDir = path.join(migrationHome, ".metheus");
|
|
9709
|
+
fs.mkdirSync(migrationMetheusDir, { recursive: true });
|
|
9710
|
+
fs.writeFileSync(
|
|
9711
|
+
path.join(migrationMetheusDir, "bot-runner.json"),
|
|
9712
|
+
`${JSON.stringify({
|
|
9713
|
+
version: 2,
|
|
9714
|
+
project_mappings: {
|
|
9715
|
+
[selftestProjectID]: {
|
|
9716
|
+
workspace_dir: path.join(runnerStateMigrationTempRoot, "workspace"),
|
|
9717
|
+
source: "selftest",
|
|
9718
|
+
},
|
|
9719
|
+
},
|
|
9720
|
+
role_profiles: defaultBotRunnerRoleProfiles(),
|
|
9721
|
+
routes: [
|
|
9722
|
+
{
|
|
9723
|
+
name: "telegram-monitor",
|
|
9724
|
+
enabled: true,
|
|
9725
|
+
project_id: selftestProjectID,
|
|
9726
|
+
provider: "telegram",
|
|
9727
|
+
role: "monitor",
|
|
9728
|
+
role_profile: "monitor",
|
|
9729
|
+
bot_name: "RyoAI_bot",
|
|
9730
|
+
destination_label: "AI incubating CHAT ROOM",
|
|
9731
|
+
},
|
|
9732
|
+
],
|
|
9733
|
+
}, null, 2)}\n`,
|
|
9734
|
+
"utf8",
|
|
9735
|
+
);
|
|
9736
|
+
fs.writeFileSync(
|
|
9737
|
+
path.join(migrationMetheusDir, "bot-runner-state.json"),
|
|
9738
|
+
`${JSON.stringify({
|
|
9739
|
+
version: 1,
|
|
9740
|
+
routes: {
|
|
9741
|
+
"-::11111111-1111-1111-1111-111111111111::telegram::monitor::RyoAI_bot::-": {
|
|
9742
|
+
last_processed_comment_id: "comment-a",
|
|
9743
|
+
last_processed_created_at: "2026-03-13T06:00:00.000Z",
|
|
9744
|
+
last_source_message_id: 41,
|
|
9745
|
+
last_source_kind: "telegram_message",
|
|
9746
|
+
updated_at: "2026-03-13T06:00:01.000Z",
|
|
9747
|
+
last_action: "replied",
|
|
9748
|
+
last_reply_message_id: 501,
|
|
9749
|
+
},
|
|
9750
|
+
},
|
|
9751
|
+
}, null, 2)}\n`,
|
|
9752
|
+
"utf8",
|
|
9753
|
+
);
|
|
9754
|
+
process.env.HOME = migrationHome;
|
|
9755
|
+
process.env.USERPROFILE = migrationHome;
|
|
9756
|
+
const migratedRunnerState = loadBotRunnerState();
|
|
9757
|
+
const canonicalRunnerStateKey = "telegram-monitor::11111111-1111-1111-1111-111111111111::telegram::monitor::RyoAI_bot::AI incubating CHAT ROOM";
|
|
9758
|
+
const persistedRunnerState = tryJsonParse(
|
|
9759
|
+
fs.readFileSync(path.join(migrationMetheusDir, "bot-runner-state.json"), "utf8"),
|
|
9760
|
+
);
|
|
9761
|
+
push(
|
|
9762
|
+
"bot_runner_state_auto_migrates_anonymous_route_key",
|
|
9763
|
+
migratedRunnerState.migrated === true
|
|
9764
|
+
&& migratedRunnerState.migratedKeys.length === 1
|
|
9765
|
+
&& Boolean(migratedRunnerState.routes[canonicalRunnerStateKey])
|
|
9766
|
+
&& !migratedRunnerState.routes["-::11111111-1111-1111-1111-111111111111::telegram::monitor::RyoAI_bot::-"]
|
|
9767
|
+
&& Boolean(safeObject(persistedRunnerState.routes)[canonicalRunnerStateKey]),
|
|
9768
|
+
`migrated=${String(migratedRunnerState.migrated)} key=${canonicalRunnerStateKey}`,
|
|
9769
|
+
);
|
|
9770
|
+
} catch (err) {
|
|
9771
|
+
push("bot_runner_state_auto_migrates_anonymous_route_key", false, String(err?.message || err));
|
|
9772
|
+
} finally {
|
|
9773
|
+
if (typeof originalStateMigrationHome === "string") {
|
|
9774
|
+
process.env.HOME = originalStateMigrationHome;
|
|
9775
|
+
} else {
|
|
9776
|
+
delete process.env.HOME;
|
|
9777
|
+
}
|
|
9778
|
+
if (typeof originalStateMigrationUserProfile === "string") {
|
|
9779
|
+
process.env.USERPROFILE = originalStateMigrationUserProfile;
|
|
9780
|
+
} else {
|
|
9781
|
+
delete process.env.USERPROFILE;
|
|
9782
|
+
}
|
|
9783
|
+
if (runnerStateMigrationTempRoot) {
|
|
9784
|
+
try {
|
|
9785
|
+
fs.rmSync(runnerStateMigrationTempRoot, { recursive: true, force: true });
|
|
9786
|
+
} catch {}
|
|
9787
|
+
}
|
|
9788
|
+
}
|
|
9789
|
+
|
|
9125
9790
|
const defaultMonitorTriggerPolicy = normalizeRunnerTriggerPolicy({}, { role: "monitor" });
|
|
9126
9791
|
push(
|
|
9127
9792
|
"bot_runner_default_monitor_trigger_policy",
|
|
@@ -9356,6 +10021,35 @@ async function runSelftest(flags = {}) {
|
|
|
9356
10021
|
&& intFromRawAllowZero(e2eState.last_reply_message_id, 0) === 501,
|
|
9357
10022
|
`outcome=${e2eResult.outcome} sent=${telegramE2EServer.state.sentMessages.length} mirrored=${String(mirroredReply?.id || "(none)")} state_reply=${String(e2eState.last_reply_message_id || "(none)")}`,
|
|
9358
10023
|
);
|
|
10024
|
+
const sentCountBeforeDryRun = telegramE2EServer.state.sentMessages.length;
|
|
10025
|
+
const commentCountBeforeDryRun = telegramE2EServer.state.comments.length;
|
|
10026
|
+
const dryRunResult = await performLocalBotDelivery({
|
|
10027
|
+
siteBaseURL: telegramE2EServer.baseURL,
|
|
10028
|
+
token: e2eToken,
|
|
10029
|
+
timeoutSeconds: 10,
|
|
10030
|
+
actorUserID: e2eActorUserID,
|
|
10031
|
+
bot: e2eBot,
|
|
10032
|
+
projectID: selftestProjectID,
|
|
10033
|
+
provider: "telegram",
|
|
10034
|
+
destinationSelectors: {
|
|
10035
|
+
destinationLabel: e2eDestination.label,
|
|
10036
|
+
},
|
|
10037
|
+
text: "dry-run smoke",
|
|
10038
|
+
disableWebPagePreview: true,
|
|
10039
|
+
replyToMessageID: 41,
|
|
10040
|
+
archiveReplies: true,
|
|
10041
|
+
archiveThreadID: e2eThreadID,
|
|
10042
|
+
archiveWorkItemID: e2eWorkItemID,
|
|
10043
|
+
dryRun: true,
|
|
10044
|
+
});
|
|
10045
|
+
push(
|
|
10046
|
+
"telegram_delivery_dry_run_skips_send_and_archive",
|
|
10047
|
+
Boolean(dryRunResult.delivery?.dryRun)
|
|
10048
|
+
&& String(dryRunResult.archive?.reason || "") === "dry_run_delivery"
|
|
10049
|
+
&& telegramE2EServer.state.sentMessages.length === sentCountBeforeDryRun
|
|
10050
|
+
&& telegramE2EServer.state.comments.length === commentCountBeforeDryRun,
|
|
10051
|
+
`dry_run=${String(dryRunResult.delivery?.dryRun || false)} sent=${telegramE2EServer.state.sentMessages.length} comments=${telegramE2EServer.state.comments.length}`,
|
|
10052
|
+
);
|
|
9359
10053
|
} catch (err) {
|
|
9360
10054
|
push("telegram_runner_e2e_local_mock", false, String(err?.message || err));
|
|
9361
10055
|
} finally {
|