metheus-governance-mcp-cli 0.2.293 → 0.2.294

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
@@ -151,6 +151,9 @@ import {
151
151
  selectPendingArchiveComments,
152
152
  printRunnerResult,
153
153
  } from "./lib/runner-helpers.mjs";
154
+ import {
155
+ normalizeRunnerRecentLocalInboundReceiptMap,
156
+ } from "./lib/runner-local-inbound-receipts.mjs";
154
157
  import {
155
158
  normalizeRunnerConversationDecisionBundle,
156
159
  validateRunnerConversationDecisionBundle,
@@ -1966,88 +1969,6 @@ function prefersRunnerStateRecord(candidate, current) {
1966
1969
  return false;
1967
1970
  }
1968
1971
 
1969
- function normalizeRunnerRecentLocalInboundReceiptRecord(rawReceipt, fallbackKey = "") {
1970
- const receipt = safeObject(rawReceipt);
1971
- const chatID = String(receipt.chat_id || receipt.chatID || "").trim();
1972
- const messageID = intFromRawAllowZero(receipt.message_id ?? receipt.messageID, 0);
1973
- const receiptKey = String(fallbackKey || `${chatID}:${messageID}`).trim();
1974
- if (!receiptKey || !chatID || !(messageID > 0)) {
1975
- return null;
1976
- }
1977
- const normalized = {
1978
- chat_id: chatID,
1979
- message_id: messageID,
1980
- receipt_origin: firstNonEmptyString([
1981
- receipt.receipt_origin,
1982
- receipt.receiptOrigin,
1983
- receipt.source_origin,
1984
- receipt.sourceOrigin,
1985
- "local_telegram_inbound",
1986
- ]),
1987
- receipt_route_key: firstNonEmptyString([
1988
- receipt.receipt_route_key,
1989
- receipt.receiptRouteKey,
1990
- receipt.source_route_key,
1991
- receipt.sourceRouteKey,
1992
- ]),
1993
- receipt_bot_username: normalizeTelegramMentionUsername(
1994
- firstNonEmptyString([
1995
- receipt.receipt_bot_username,
1996
- receipt.receiptBotUsername,
1997
- receipt.source_bot_username,
1998
- receipt.sourceBotUsername,
1999
- ]),
2000
- ),
2001
- kind: firstNonEmptyString([receipt.kind, "telegram_message"]),
2002
- sender: firstNonEmptyString([receipt.sender, receipt.from_name, receipt.fromName]),
2003
- sender_username: firstNonEmptyString([
2004
- receipt.sender_username,
2005
- receipt.senderUsername,
2006
- receipt.from_username,
2007
- receipt.fromUsername,
2008
- ]),
2009
- sender_is_bot: Boolean(receipt.sender_is_bot ?? receipt.senderIsBot ?? false),
2010
- body: firstNonEmptyString([receipt.body, receipt.text]),
2011
- occurred_at: firstNonEmptyString([receipt.occurred_at, receipt.occurredAt]),
2012
- received_at: firstNonEmptyString([receipt.received_at, receipt.receivedAt, new Date().toISOString()]),
2013
- };
2014
- const senderID = firstNonEmptyString([receipt.sender_id, receipt.senderID, receipt.from_id, receipt.fromID]);
2015
- if (senderID) {
2016
- normalized.sender_id = senderID;
2017
- }
2018
- const updateID = intFromRawAllowZero(receipt.update_id ?? receipt.updateID, 0);
2019
- if (updateID > 0) {
2020
- normalized.update_id = updateID;
2021
- }
2022
- const messageThreadID = intFromRawAllowZero(receipt.message_thread_id ?? receipt.messageThreadID, 0);
2023
- if (messageThreadID > 0) {
2024
- normalized.message_thread_id = messageThreadID;
2025
- }
2026
- const replyToMessageID = intFromRawAllowZero(receipt.reply_to_message_id ?? receipt.replyToMessageID, 0);
2027
- if (replyToMessageID > 0) {
2028
- normalized.reply_to_message_id = replyToMessageID;
2029
- }
2030
- const chatType = firstNonEmptyString([receipt.chat_type, receipt.chatType]);
2031
- if (chatType) {
2032
- normalized.chat_type = chatType;
2033
- }
2034
- const chatTitle = firstNonEmptyString([receipt.chat_title, receipt.chatTitle]);
2035
- if (chatTitle) {
2036
- normalized.chat_title = chatTitle;
2037
- }
2038
- return [receiptKey, normalized];
2039
- }
2040
-
2041
- function normalizeRunnerRecentLocalInboundReceiptMap(rawReceipts) {
2042
- const normalizedEntries = [];
2043
- for (const [key, value] of Object.entries(safeObject(rawReceipts))) {
2044
- const normalizedEntry = normalizeRunnerRecentLocalInboundReceiptRecord(value, key);
2045
- if (!normalizedEntry) continue;
2046
- normalizedEntries.push(normalizedEntry);
2047
- }
2048
- return Object.fromEntries(normalizedEntries);
2049
- }
2050
-
2051
1972
  function mergeRunnerStateRecords(preferred, fallback) {
2052
1973
  const primary = safeObject(preferred);
2053
1974
  const secondary = safeObject(fallback);
@@ -2143,6 +2143,11 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
2143
2143
  "Do not mention another managed bot unless the contract explicitly names that bot in assignments or next_responders.",
2144
2144
  "Without a matching contract, mentioned bots will not act.",
2145
2145
  "When delegating to another managed bot, use contract.type=\"delegation\" with actionable=true, assignments, and next_responders.",
2146
+ "Decision bundle consistency rules:",
2147
+ "- If execution_contract_type is \"delegation\", conversation_intent_mode must be \"delegated_single_lead\".",
2148
+ "- If execution_contract_type is \"delegation\", initial_responders must contain only the lead bot. Delegated target bots belong in next_expected_responders, not initial_responders.",
2149
+ "- If multiple bots should each answer the opening human message, do not set should_close_after_reply=true on the first reply. Keep should_close_after_reply=false and list the remaining bots in next_expected_responders until the multi-bot opening is complete.",
2150
+ "- Never combine conversation_intent_mode=\"multi_bot_direct\" with execution_contract_type=\"delegation\".",
2146
2151
  "Each assignment must declare whether it is a conversational contribution or a real execution task.",
2147
2152
  "Use assignment.mode=\"conversation_contribution\" for opinions, discussion, review, comparison, synthesis, greetings, or other room-visible contributions that do not require workspace artifacts.",
2148
2153
  "Use assignment.mode=\"execution_task\" only when the delegated bot must change workspace files, create artifacts, update ctxpack, or produce other concrete project outputs. If it is an execution task, also set artifacts_required=true.",
@@ -0,0 +1,212 @@
1
+ import {
2
+ buildCanonicalHumanInboundKey,
3
+ } from "./runner-helpers.mjs";
4
+
5
+ function safeObject(value) {
6
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
7
+ return {};
8
+ }
9
+ return value;
10
+ }
11
+
12
+ function ensureArray(value) {
13
+ return Array.isArray(value) ? value : [];
14
+ }
15
+
16
+ function intFromRawAllowZero(raw, fallback = 0) {
17
+ if (raw === null || raw === undefined || raw === "") {
18
+ return fallback;
19
+ }
20
+ const parsed = Number.parseInt(String(raw).trim(), 10);
21
+ return Number.isFinite(parsed) ? parsed : fallback;
22
+ }
23
+
24
+ function firstNonEmptyString(values) {
25
+ for (const value of ensureArray(values)) {
26
+ const normalized = String(value ?? "").trim();
27
+ if (normalized) return normalized;
28
+ }
29
+ return "";
30
+ }
31
+
32
+ function normalizeMentionSelector(value) {
33
+ return String(value || "").trim().replace(/^@+/, "").toLowerCase();
34
+ }
35
+
36
+ function buildRunnerInboundTargetSelectorSuffix(selector) {
37
+ const normalizedSelector = normalizeMentionSelector(selector);
38
+ return normalizedSelector ? `:${normalizedSelector}` : "";
39
+ }
40
+
41
+ export function buildRunnerRecentLocalInboundReceiptKey(chatID, messageID) {
42
+ return `${String(chatID || "").trim()}:${intFromRawAllowZero(messageID, 0)}`;
43
+ }
44
+
45
+ export function normalizeRunnerRecentLocalInboundReceiptEntry(rawReceipt, fallbackKey = "") {
46
+ const receipt = safeObject(rawReceipt);
47
+ const chatID = String(receipt.chat_id || receipt.chatID || "").trim();
48
+ const messageID = intFromRawAllowZero(receipt.message_id ?? receipt.messageID, 0);
49
+ const receiptKey = String(fallbackKey || buildRunnerRecentLocalInboundReceiptKey(chatID, messageID)).trim();
50
+ if (!receiptKey || !chatID || !(messageID > 0)) {
51
+ return null;
52
+ }
53
+ const normalized = {
54
+ chat_id: chatID,
55
+ message_id: messageID,
56
+ receipt_origin: firstNonEmptyString([
57
+ receipt.receipt_origin,
58
+ receipt.receiptOrigin,
59
+ receipt.source_origin,
60
+ receipt.sourceOrigin,
61
+ "local_telegram_inbound",
62
+ ]),
63
+ receipt_route_key: firstNonEmptyString([
64
+ receipt.receipt_route_key,
65
+ receipt.receiptRouteKey,
66
+ receipt.source_route_key,
67
+ receipt.sourceRouteKey,
68
+ ]),
69
+ receipt_bot_username: normalizeMentionSelector(
70
+ firstNonEmptyString([
71
+ receipt.receipt_bot_username,
72
+ receipt.receiptBotUsername,
73
+ receipt.source_bot_username,
74
+ receipt.sourceBotUsername,
75
+ ]),
76
+ ),
77
+ kind: firstNonEmptyString([receipt.kind, "telegram_message"]),
78
+ sender: firstNonEmptyString([receipt.sender, receipt.from_name, receipt.fromName]),
79
+ sender_username: firstNonEmptyString([
80
+ receipt.sender_username,
81
+ receipt.senderUsername,
82
+ receipt.from_username,
83
+ receipt.fromUsername,
84
+ ]),
85
+ sender_is_bot: Boolean(receipt.sender_is_bot ?? receipt.senderIsBot ?? false),
86
+ body: firstNonEmptyString([receipt.body, receipt.text]),
87
+ occurred_at: firstNonEmptyString([receipt.occurred_at, receipt.occurredAt]),
88
+ received_at: firstNonEmptyString([receipt.received_at, receipt.receivedAt, new Date().toISOString()]),
89
+ };
90
+ const senderID = firstNonEmptyString([receipt.sender_id, receipt.senderID, receipt.from_id, receipt.fromID]);
91
+ if (senderID) {
92
+ normalized.sender_id = senderID;
93
+ }
94
+ const updateID = intFromRawAllowZero(receipt.update_id ?? receipt.updateID, 0);
95
+ if (updateID > 0) {
96
+ normalized.update_id = updateID;
97
+ }
98
+ const messageThreadID = intFromRawAllowZero(receipt.message_thread_id ?? receipt.messageThreadID, 0);
99
+ if (messageThreadID > 0) {
100
+ normalized.message_thread_id = messageThreadID;
101
+ }
102
+ const replyToMessageID = intFromRawAllowZero(receipt.reply_to_message_id ?? receipt.replyToMessageID, 0);
103
+ if (replyToMessageID > 0) {
104
+ normalized.reply_to_message_id = replyToMessageID;
105
+ }
106
+ const replyToFromUsername = firstNonEmptyString([
107
+ receipt.reply_to_from_username,
108
+ receipt.replyToFromUsername,
109
+ receipt.reply_to_username,
110
+ receipt.replyToUsername,
111
+ ]);
112
+ if (replyToFromUsername) {
113
+ normalized.reply_to_from_username = replyToFromUsername;
114
+ }
115
+ if (receipt.reply_to_from_is_bot === true || receipt.replyToFromIsBot === true) {
116
+ normalized.reply_to_from_is_bot = true;
117
+ }
118
+ const chatType = firstNonEmptyString([receipt.chat_type, receipt.chatType]);
119
+ if (chatType) {
120
+ normalized.chat_type = chatType;
121
+ }
122
+ const chatTitle = firstNonEmptyString([receipt.chat_title, receipt.chatTitle]);
123
+ if (chatTitle) {
124
+ normalized.chat_title = chatTitle;
125
+ }
126
+ const canonicalHumanMessageKey = String(buildCanonicalHumanInboundKey({
127
+ chat_id: normalized.chat_id,
128
+ message_id: normalized.message_id,
129
+ message_thread_id: normalized.message_thread_id,
130
+ reply_to_message_id: normalized.reply_to_message_id,
131
+ kind: normalized.kind,
132
+ sender_id: normalized.sender_id,
133
+ sender_username: normalized.sender_username,
134
+ sender_is_bot: normalized.sender_is_bot === true,
135
+ body: normalized.body,
136
+ occurred_at: normalized.occurred_at,
137
+ canonical_human_message_key: firstNonEmptyString([
138
+ receipt.canonical_human_message_key,
139
+ receipt.canonicalHumanMessageKey,
140
+ ]),
141
+ }) || "").trim();
142
+ if (canonicalHumanMessageKey) {
143
+ normalized.canonical_human_message_key = canonicalHumanMessageKey;
144
+ }
145
+ return [receiptKey, normalized];
146
+ }
147
+
148
+ export function normalizeRunnerRecentLocalInboundReceipt(rawReceipt, fallbackKey = "") {
149
+ const normalizedEntry = normalizeRunnerRecentLocalInboundReceiptEntry(rawReceipt, fallbackKey);
150
+ return normalizedEntry ? safeObject(normalizedEntry[1]) : {};
151
+ }
152
+
153
+ export function normalizeRunnerRecentLocalInboundReceiptMap(rawReceipts) {
154
+ const normalizedEntries = [];
155
+ for (const [key, value] of Object.entries(safeObject(rawReceipts))) {
156
+ const normalizedEntry = normalizeRunnerRecentLocalInboundReceiptEntry(value, key);
157
+ if (!normalizedEntry) continue;
158
+ normalizedEntries.push(normalizedEntry);
159
+ }
160
+ return Object.fromEntries(normalizedEntries);
161
+ }
162
+
163
+ export function buildRunnerLocalInboundReceiptKey(receiptRaw, { resolveTargetSelector = null } = {}) {
164
+ const receipt = normalizeRunnerRecentLocalInboundReceipt(receiptRaw);
165
+ const canonicalHumanKey = String(receipt.canonical_human_message_key || "").trim();
166
+ if (canonicalHumanKey) {
167
+ return `human:${canonicalHumanKey}`;
168
+ }
169
+ const chatID = String(receipt.chat_id || "").trim();
170
+ const messageID = intFromRawAllowZero(receipt.message_id, 0);
171
+ if (!chatID || !(messageID > 0)) {
172
+ return "";
173
+ }
174
+ const targetSelector = typeof resolveTargetSelector === "function"
175
+ ? normalizeMentionSelector(resolveTargetSelector(receiptRaw))
176
+ : normalizeMentionSelector(receipt.receipt_bot_username || "");
177
+ return `${chatID}:${messageID}${buildRunnerInboundTargetSelectorSuffix(targetSelector)}`;
178
+ }
179
+
180
+ export function findRecentTelegramInboundReceipt(rawMap, {
181
+ chatID = "",
182
+ messageID = 0,
183
+ canonicalHumanMessageKey = "",
184
+ } = {}) {
185
+ const normalizedChatID = String(chatID || "").trim();
186
+ const normalizedMessageID = intFromRawAllowZero(messageID, 0);
187
+ const normalizedCanonicalHumanMessageKey = String(canonicalHumanMessageKey || "").trim();
188
+ if (!normalizedChatID || (!(normalizedMessageID > 0) && !normalizedCanonicalHumanMessageKey)) {
189
+ return {};
190
+ }
191
+ if (normalizedMessageID > 0) {
192
+ const exactReceipt = normalizeRunnerRecentLocalInboundReceipt(
193
+ safeObject(safeObject(rawMap)[buildRunnerRecentLocalInboundReceiptKey(normalizedChatID, normalizedMessageID)]),
194
+ );
195
+ if (Object.keys(exactReceipt).length > 0) {
196
+ return exactReceipt;
197
+ }
198
+ }
199
+ if (!normalizedCanonicalHumanMessageKey) {
200
+ return {};
201
+ }
202
+ for (const value of Object.values(safeObject(rawMap))) {
203
+ const normalizedReceipt = normalizeRunnerRecentLocalInboundReceipt(value);
204
+ if (
205
+ String(normalizedReceipt.chat_id || "").trim() === normalizedChatID
206
+ && String(normalizedReceipt.canonical_human_message_key || "").trim() === normalizedCanonicalHumanMessageKey
207
+ ) {
208
+ return normalizedReceipt;
209
+ }
210
+ }
211
+ return {};
212
+ }
@@ -132,6 +132,14 @@ export function validateRunnerConversationDecisionBundle(bundleRaw) {
132
132
  bundle,
133
133
  };
134
134
  }
135
+ if (bundle.should_close_after_reply === true && bundle.initial_responders.length > 1) {
136
+ return {
137
+ ok: false,
138
+ status: "contradictory_multi_initial_close_state",
139
+ reason: "should_close_after_reply cannot be true when multiple initial_responders are expected to answer the opening turn",
140
+ bundle,
141
+ };
142
+ }
135
143
  if (
136
144
  bundle.conversation_intent_mode === "single_bot"
137
145
  && bundle.allowed_responders.length > 1
@@ -154,6 +162,28 @@ export function validateRunnerConversationDecisionBundle(bundleRaw) {
154
162
  bundle,
155
163
  };
156
164
  }
165
+ if (
166
+ bundle.conversation_intent_mode === "delegated_single_lead"
167
+ && bundle.initial_responders.length !== 1
168
+ ) {
169
+ return {
170
+ ok: false,
171
+ status: "invalid_delegated_single_lead_initial_responders",
172
+ reason: "delegated_single_lead requires exactly one initial responder",
173
+ bundle,
174
+ };
175
+ }
176
+ if (
177
+ bundle.execution_contract_type === "delegation"
178
+ && bundle.conversation_intent_mode === "multi_bot_direct"
179
+ ) {
180
+ return {
181
+ ok: false,
182
+ status: "contradictory_delegation_intent_mode",
183
+ reason: "multi_bot_direct cannot be combined with a delegation contract",
184
+ bundle,
185
+ };
186
+ }
157
187
  if (bundle.visible_handoff_required === true && bundle.visible_handoff_targets.length === 0) {
158
188
  return {
159
189
  ok: false,
@@ -11,6 +11,9 @@ import {
11
11
  resolveRunnerAuthoritativeTriggerDecision,
12
12
  shouldRunnerTriggerProceed,
13
13
  } from "./runner-trigger.mjs";
14
+ import {
15
+ buildRunnerLocalInboundReceiptKey,
16
+ } from "./runner-local-inbound-receipts.mjs";
14
17
 
15
18
  function safeObject(value) {
16
19
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -116,31 +119,6 @@ function buildRunnerArchiveSourceMessageKey(recordRaw) {
116
119
  return `${chatID}:${messageID}${buildRunnerInboundTargetSelectorSuffix(resolveRunnerArchiveSourceTargetSelector(recordRaw))}`;
117
120
  }
118
121
 
119
- function buildRunnerLocalInboundReceiptKey(receiptRaw) {
120
- const receipt = safeObject(receiptRaw);
121
- const canonicalHumanKey = buildCanonicalHumanInboundKey({
122
- chat_id: receipt.chat_id,
123
- message_id: receipt.message_id,
124
- message_thread_id: receipt.message_thread_id,
125
- reply_to_message_id: receipt.reply_to_message_id,
126
- kind: receipt.kind,
127
- sender_id: receipt.sender_id,
128
- sender_username: receipt.sender_username,
129
- sender_is_bot: receipt.sender_is_bot === true,
130
- body: receipt.body,
131
- occurred_at: receipt.occurred_at,
132
- });
133
- if (canonicalHumanKey) {
134
- return `human:${canonicalHumanKey}`;
135
- }
136
- const chatID = String(receipt.chat_id || receipt.chatID || "").trim();
137
- const messageID = intFromRawAllowZero(receipt.message_id || receipt.messageID, 0);
138
- if (!chatID || !(messageID > 0)) {
139
- return "";
140
- }
141
- return `${chatID}:${messageID}${buildRunnerInboundTargetSelectorSuffix(resolveRunnerLocalInboundReceiptTargetSelector(receiptRaw))}`;
142
- }
143
-
144
122
  function buildRunnerReceiptReplaySortTime(receiptRaw) {
145
123
  const receipt = safeObject(receiptRaw);
146
124
  const receiptTime = Date.parse(firstNonEmptyString([
@@ -190,7 +168,9 @@ function buildRunnerReceiptBackedPendingArchiveComments({
190
168
  const latestReceiptsByKey = new Map();
191
169
  for (const receiptRaw of ensureArray(currentPollLocalInboundReceipts)) {
192
170
  const receipt = safeObject(receiptRaw);
193
- const receiptKey = buildRunnerLocalInboundReceiptKey(receipt);
171
+ const receiptKey = buildRunnerLocalInboundReceiptKey(receipt, {
172
+ resolveTargetSelector: resolveRunnerLocalInboundReceiptTargetSelector,
173
+ });
194
174
  if (!receiptKey) {
195
175
  continue;
196
176
  }
@@ -21,36 +21,6 @@ function escapeRegexText(value) {
21
21
  return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22
22
  }
23
23
 
24
- function detectDirectedManagedReplyTarget({
25
- text,
26
- currentBotSelector = "",
27
- managedMentions = [],
28
- }) {
29
- const normalizedText = String(text || "").trim();
30
- const currentSelector = String(currentBotSelector || "").trim().toLowerCase();
31
- if (!normalizedText || !currentSelector) {
32
- return "";
33
- }
34
- const instructionPattern = /(?:\b(?:say|tell|greet|reply|answer|introduce|mention)\b|인사(?:해|하)|말(?:해|하)|얘기(?:해|하)|전달(?:해|하)|전해|알려|소개(?:해|하)|답(?:해|하)|응답(?:해|하)|말씀(?:해|하))/i;
35
- if (!instructionPattern.test(normalizedText)) {
36
- return "";
37
- }
38
- const candidates = ensureArray(managedMentions)
39
- .map((item) => String(item || "").trim().toLowerCase())
40
- .filter((item) => item && item !== currentSelector);
41
- for (const selector of candidates) {
42
- const escapedSelector = escapeRegexText(String(selector || "").replace(/^@+/, ""));
43
- const targetPattern = new RegExp(
44
- `(?:@${escapedSelector}\\s*(?:에게|한테|께|보고)?|(?:to|for)\\s+@${escapedSelector}\\b)`,
45
- "i",
46
- );
47
- if (targetPattern.test(normalizedText)) {
48
- return selector;
49
- }
50
- }
51
- return "";
52
- }
53
-
54
24
  const DIRECTED_MANAGED_REPLY_ENGLISH_ACTION_PATTERN = "\\b(?:say|tell|greet|reply|answer|introduce|mention)\\b";
55
25
  const DIRECTED_MANAGED_REPLY_KOREAN_ACTION_PATTERN = "(?:\\uC778\\uC0AC|\\uB9D0|\\uC804\\uB2EC|\\uB2F5|\\uC18C\\uAC1C|\\uC5B8\\uAE09)";
56
26
  const DIRECTED_MANAGED_REPLY_ACTION_PATTERN = `(?:${DIRECTED_MANAGED_REPLY_ENGLISH_ACTION_PATTERN}|${DIRECTED_MANAGED_REPLY_KOREAN_ACTION_PATTERN})`;
@@ -8,6 +8,10 @@ import {
8
8
  import {
9
9
  resolveRunnerAuthoritativeTriggerDecision,
10
10
  } from "./runner-trigger.mjs";
11
+ import {
12
+ findRecentTelegramInboundReceipt,
13
+ normalizeRunnerRecentLocalInboundReceipt,
14
+ } from "./runner-local-inbound-receipts.mjs";
11
15
 
12
16
  function safeObject(value) {
13
17
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -106,106 +110,6 @@ function doesTelegramEnvelopeMatchCanonicalHumanMessage(rawEnvelope, canonicalHu
106
110
  return String(envelope.canonical_human_message_key || "").trim() === normalizedCanonicalHumanMessageKey;
107
111
  }
108
112
 
109
- function normalizeRunnerRecentLocalInboundReceipt(rawReceipt) {
110
- const receipt = safeObject(rawReceipt);
111
- const chatID = String(receipt.chat_id || receipt.chatID || "").trim();
112
- const messageID = intFromRawAllowZero(receipt.message_id ?? receipt.messageID, 0);
113
- if (!chatID || !(messageID > 0)) {
114
- return {};
115
- }
116
- const normalized = {
117
- chat_id: chatID,
118
- message_id: messageID,
119
- ...(intFromRawAllowZero(receipt.message_thread_id ?? receipt.messageThreadID, 0) > 0
120
- ? { message_thread_id: intFromRawAllowZero(receipt.message_thread_id ?? receipt.messageThreadID, 0) }
121
- : {}),
122
- ...(intFromRawAllowZero(receipt.reply_to_message_id ?? receipt.replyToMessageID, 0) > 0
123
- ? { reply_to_message_id: intFromRawAllowZero(receipt.reply_to_message_id ?? receipt.replyToMessageID, 0) }
124
- : {}),
125
- ...(String(receipt.kind || "").trim()
126
- ? { kind: String(receipt.kind || "").trim().toLowerCase() }
127
- : {}),
128
- ...(String(receipt.sender || "").trim()
129
- ? { sender: String(receipt.sender || "").trim() }
130
- : {}),
131
- ...(normalizeMentionSelector(receipt.sender_username || receipt.senderUsername || "")
132
- ? { sender_username: normalizeMentionSelector(receipt.sender_username || receipt.senderUsername || "") }
133
- : {}),
134
- ...(String(receipt.sender_id || receipt.senderID || "").trim()
135
- ? { sender_id: String(receipt.sender_id || receipt.senderID || "").trim() }
136
- : {}),
137
- sender_is_bot: receipt.sender_is_bot === true || receipt.senderIsBot === true,
138
- ...(String(receipt.body || "").trim()
139
- ? { body: String(receipt.body || "").trim() }
140
- : {}),
141
- ...(String(receipt.occurred_at || receipt.occurredAt || "").trim()
142
- ? { occurred_at: String(receipt.occurred_at || receipt.occurredAt || "").trim() }
143
- : {}),
144
- ...(String(receipt.receipt_origin || receipt.receiptOrigin || "").trim()
145
- ? { receipt_origin: String(receipt.receipt_origin || receipt.receiptOrigin || "").trim().toLowerCase() }
146
- : {}),
147
- ...(String(receipt.receipt_route_key || receipt.receiptRouteKey || "").trim()
148
- ? { receipt_route_key: String(receipt.receipt_route_key || receipt.receiptRouteKey || "").trim() }
149
- : {}),
150
- ...(normalizeMentionSelector(receipt.receipt_bot_username || receipt.receiptBotUsername || "")
151
- ? { receipt_bot_username: normalizeMentionSelector(receipt.receipt_bot_username || receipt.receiptBotUsername || "") }
152
- : {}),
153
- };
154
- const canonicalHumanMessageKey = buildCanonicalHumanInboundKey({
155
- chat_id: normalized.chat_id,
156
- message_id: normalized.message_id,
157
- message_thread_id: normalized.message_thread_id,
158
- reply_to_message_id: normalized.reply_to_message_id,
159
- kind: normalized.kind,
160
- sender_id: normalized.sender_id,
161
- sender_username: normalized.sender_username,
162
- sender_is_bot: normalized.sender_is_bot === true,
163
- body: normalized.body,
164
- occurred_at: normalized.occurred_at,
165
- canonical_human_message_key: firstNonEmptyString([
166
- receipt.canonical_human_message_key,
167
- receipt.canonicalHumanMessageKey,
168
- ]),
169
- });
170
- if (canonicalHumanMessageKey) {
171
- normalized.canonical_human_message_key = canonicalHumanMessageKey;
172
- }
173
- return normalized;
174
- }
175
-
176
- function findRecentTelegramInboundReceipt(rawMap, {
177
- chatID = "",
178
- messageID = 0,
179
- canonicalHumanMessageKey = "",
180
- } = {}) {
181
- const normalizedChatID = String(chatID || "").trim();
182
- const normalizedMessageID = intFromRawAllowZero(messageID, 0);
183
- const normalizedCanonicalHumanMessageKey = normalizeCanonicalHumanMessageKey(canonicalHumanMessageKey);
184
- if (!normalizedChatID || (!(normalizedMessageID > 0) && !normalizedCanonicalHumanMessageKey)) {
185
- return {};
186
- }
187
- if (normalizedMessageID > 0) {
188
- const key = `${normalizedChatID}:${normalizedMessageID}`;
189
- const exactReceipt = normalizeRunnerRecentLocalInboundReceipt(safeObject(safeObject(rawMap)[key]));
190
- if (Object.keys(exactReceipt).length > 0) {
191
- return exactReceipt;
192
- }
193
- }
194
- if (!normalizedCanonicalHumanMessageKey) {
195
- return {};
196
- }
197
- for (const value of Object.values(safeObject(rawMap))) {
198
- const normalizedReceipt = normalizeRunnerRecentLocalInboundReceipt(value);
199
- if (
200
- String(normalizedReceipt.chat_id || "").trim() === normalizedChatID
201
- && String(normalizedReceipt.canonical_human_message_key || "").trim() === normalizedCanonicalHumanMessageKey
202
- ) {
203
- return normalizedReceipt;
204
- }
205
- }
206
- return {};
207
- }
208
-
209
113
  function buildTelegramMessageEnvelopeFromRecentReceipt(rawReceipt) {
210
114
  const receipt = normalizeRunnerRecentLocalInboundReceipt(rawReceipt);
211
115
  if (!Object.keys(receipt).length) {
@@ -896,57 +896,6 @@ function doesTelegramEnvelopeMatchMessage(rawEnvelope, {
896
896
  && intFromRawAllowZero(envelope.message_id, 0) === normalizedMessageID;
897
897
  }
898
898
 
899
- function normalizeRunnerRecentLocalInboundReceipt(rawReceipt) {
900
- const receipt = safeObject(rawReceipt);
901
- const chatID = String(receipt.chat_id || receipt.chatID || "").trim();
902
- const messageID = intFromRawAllowZero(receipt.message_id ?? receipt.messageID, 0);
903
- if (!chatID || !(messageID > 0)) {
904
- return {};
905
- }
906
- return {
907
- chat_id: chatID,
908
- message_id: messageID,
909
- ...(intFromRawAllowZero(receipt.message_thread_id ?? receipt.messageThreadID, 0) > 0
910
- ? { message_thread_id: intFromRawAllowZero(receipt.message_thread_id ?? receipt.messageThreadID, 0) }
911
- : {}),
912
- ...(intFromRawAllowZero(receipt.reply_to_message_id ?? receipt.replyToMessageID, 0) > 0
913
- ? { reply_to_message_id: intFromRawAllowZero(receipt.reply_to_message_id ?? receipt.replyToMessageID, 0) }
914
- : {}),
915
- ...(String(receipt.kind || "").trim()
916
- ? { kind: String(receipt.kind || "").trim().toLowerCase() }
917
- : {}),
918
- ...(String(receipt.sender || "").trim()
919
- ? { sender: String(receipt.sender || "").trim() }
920
- : {}),
921
- ...(normalizeMentionSelector(receipt.sender_username || receipt.senderUsername || "")
922
- ? { sender_username: normalizeMentionSelector(receipt.sender_username || receipt.senderUsername || "") }
923
- : {}),
924
- sender_is_bot: receipt.sender_is_bot === true || receipt.senderIsBot === true,
925
- ...(String(receipt.body || "").trim()
926
- ? { body: String(receipt.body || "").trim() }
927
- : {}),
928
- ...(String(receipt.receipt_origin || receipt.receiptOrigin || "").trim()
929
- ? { receipt_origin: String(receipt.receipt_origin || receipt.receiptOrigin || "").trim().toLowerCase() }
930
- : {}),
931
- ...(String(receipt.receipt_route_key || receipt.receiptRouteKey || "").trim()
932
- ? { receipt_route_key: String(receipt.receipt_route_key || receipt.receiptRouteKey || "").trim() }
933
- : {}),
934
- ...(normalizeMentionSelector(receipt.receipt_bot_username || receipt.receiptBotUsername || "")
935
- ? { receipt_bot_username: normalizeMentionSelector(receipt.receipt_bot_username || receipt.receiptBotUsername || "") }
936
- : {}),
937
- };
938
- }
939
-
940
- function findRecentTelegramInboundReceipt(rawMap, { chatID = "", messageID = 0 } = {}) {
941
- const normalizedChatID = String(chatID || "").trim();
942
- const normalizedMessageID = intFromRawAllowZero(messageID, 0);
943
- if (!normalizedChatID || !(normalizedMessageID > 0)) {
944
- return {};
945
- }
946
- const key = `${normalizedChatID}:${normalizedMessageID}`;
947
- return normalizeRunnerRecentLocalInboundReceipt(safeObject(safeObject(rawMap)[key]));
948
- }
949
-
950
899
  export function resolveRunnerHumanInboundVisibility(args) {
951
900
  return resolveRunnerHumanInboundVisibilityImpl(args);
952
901
  }
@@ -2,6 +2,11 @@ import {
2
2
  mergeRecentTelegramMessageEnvelopes,
3
3
  normalizeTelegramMessageEnvelope,
4
4
  } from "./runner-helpers.mjs";
5
+ import {
6
+ buildRunnerRecentLocalInboundReceiptKey,
7
+ normalizeRunnerRecentLocalInboundReceipt,
8
+ normalizeRunnerRecentLocalInboundReceiptEntry,
9
+ } from "./runner-local-inbound-receipts.mjs";
5
10
 
6
11
  function safeObject(value) {
7
12
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -203,7 +208,7 @@ function buildRunnerLocalInboundOwnerMap({
203
208
  return owners;
204
209
  }
205
210
 
206
- function resolveRunnerLocalInboundArtifactOwners({
211
+ export function resolveRunnerLocalInboundArtifactOwners({
207
212
  update,
208
213
  routeKey,
209
214
  route,
@@ -233,15 +238,15 @@ function resolveRunnerLocalInboundArtifactOwners({
233
238
  .filter(Boolean),
234
239
  ));
235
240
  if (explicitMentions.length > 0) {
241
+ const explicitOwners = explicitMentions
242
+ .map((selector) => ownersBySelector.get(selector))
243
+ .filter((owner) => owner && owner.routeKey);
236
244
  if (normalizedUpdate.fromIsBot !== true) {
237
- if (currentBotSelector && explicitMentions.includes(currentBotSelector) && currentOwner) {
238
- return [currentOwner];
245
+ if (explicitOwners.length > 0) {
246
+ return explicitOwners;
239
247
  }
240
248
  return [];
241
249
  }
242
- const explicitOwners = explicitMentions
243
- .map((selector) => ownersBySelector.get(selector))
244
- .filter((owner) => owner && owner.routeKey);
245
250
  if (explicitOwners.length > 0) {
246
251
  return explicitOwners;
247
252
  }
@@ -443,115 +448,6 @@ const RUNNER_RECENT_LOCAL_INBOUND_ENVELOPE_LIMIT = 200;
443
448
  const RUNNER_RECENT_LOCAL_INBOUND_RECEIPT_LIMIT = 200;
444
449
  const RUNNER_TELEGRAM_BOT_CAPABILITY_REFRESH_MS = 6 * 60 * 60 * 1000;
445
450
 
446
- function buildRunnerRecentLocalInboundReceiptKey(chatID, messageID) {
447
- return `${String(chatID || "").trim()}:${intFromRawAllowZero(messageID, 0)}`;
448
- }
449
-
450
- function normalizeRunnerRecentLocalInboundReceipt(rawReceipt, fallbackKey = "") {
451
- const receipt = safeObject(rawReceipt);
452
- const chatID = String(receipt.chat_id || receipt.chatID || "").trim();
453
- const messageID = intFromRawAllowZero(receipt.message_id ?? receipt.messageID, 0);
454
- const receiptKey = String(fallbackKey || buildRunnerRecentLocalInboundReceiptKey(chatID, messageID)).trim();
455
- if (!receiptKey || !chatID || !(messageID > 0)) {
456
- return null;
457
- }
458
- const normalized = {
459
- chat_id: chatID,
460
- message_id: messageID,
461
- receipt_origin: firstNonEmptyString([
462
- receipt.receipt_origin,
463
- receipt.receiptOrigin,
464
- receipt.source_origin,
465
- receipt.sourceOrigin,
466
- "local_telegram_inbound",
467
- ]),
468
- receipt_route_key: firstNonEmptyString([
469
- receipt.receipt_route_key,
470
- receipt.receiptRouteKey,
471
- receipt.source_route_key,
472
- receipt.sourceRouteKey,
473
- ]),
474
- receipt_bot_username: normalizeMentionSelector(
475
- firstNonEmptyString([
476
- receipt.receipt_bot_username,
477
- receipt.receiptBotUsername,
478
- receipt.source_bot_username,
479
- receipt.sourceBotUsername,
480
- ]),
481
- ),
482
- kind: firstNonEmptyString([receipt.kind, "telegram_message"]),
483
- sender: firstNonEmptyString([receipt.sender, receipt.from_name, receipt.fromName]),
484
- sender_username: firstNonEmptyString([
485
- receipt.sender_username,
486
- receipt.senderUsername,
487
- receipt.from_username,
488
- receipt.fromUsername,
489
- ]),
490
- sender_is_bot: Boolean(receipt.sender_is_bot ?? receipt.senderIsBot ?? false),
491
- body: firstNonEmptyString([receipt.body, receipt.text]),
492
- occurred_at: firstNonEmptyString([receipt.occurred_at, receipt.occurredAt]),
493
- received_at: firstNonEmptyString([receipt.received_at, receipt.receivedAt, new Date().toISOString()]),
494
- };
495
- const senderID = firstNonEmptyString([receipt.sender_id, receipt.senderID, receipt.from_id, receipt.fromID]);
496
- if (senderID) {
497
- normalized.sender_id = senderID;
498
- }
499
- const updateID = intFromRawAllowZero(receipt.update_id ?? receipt.updateID, 0);
500
- if (updateID > 0) {
501
- normalized.update_id = updateID;
502
- }
503
- const messageThreadID = intFromRawAllowZero(receipt.message_thread_id ?? receipt.messageThreadID, 0);
504
- if (messageThreadID > 0) {
505
- normalized.message_thread_id = messageThreadID;
506
- }
507
- const replyToMessageID = intFromRawAllowZero(receipt.reply_to_message_id ?? receipt.replyToMessageID, 0);
508
- if (replyToMessageID > 0) {
509
- normalized.reply_to_message_id = replyToMessageID;
510
- }
511
- const replyToFromUsername = firstNonEmptyString([
512
- receipt.reply_to_from_username,
513
- receipt.replyToFromUsername,
514
- receipt.reply_to_username,
515
- receipt.replyToUsername,
516
- ]);
517
- if (replyToFromUsername) {
518
- normalized.reply_to_from_username = replyToFromUsername;
519
- }
520
- if (receipt.reply_to_from_is_bot === true || receipt.replyToFromIsBot === true) {
521
- normalized.reply_to_from_is_bot = true;
522
- }
523
- const chatType = firstNonEmptyString([receipt.chat_type, receipt.chatType]);
524
- if (chatType) {
525
- normalized.chat_type = chatType;
526
- }
527
- const chatTitle = firstNonEmptyString([receipt.chat_title, receipt.chatTitle]);
528
- if (chatTitle) {
529
- normalized.chat_title = chatTitle;
530
- }
531
- const canonicalHumanMessageKey = String(
532
- safeObject(normalizeTelegramMessageEnvelope({
533
- chat_id: normalized.chat_id,
534
- message_id: normalized.message_id,
535
- message_thread_id: normalized.message_thread_id,
536
- reply_to_message_id: normalized.reply_to_message_id,
537
- kind: normalized.kind,
538
- sender_id: normalized.sender_id,
539
- sender_username: normalized.sender_username,
540
- sender_is_bot: normalized.sender_is_bot === true,
541
- body: normalized.body,
542
- occurred_at: normalized.occurred_at,
543
- canonical_human_message_key: firstNonEmptyString([
544
- receipt.canonical_human_message_key,
545
- receipt.canonicalHumanMessageKey,
546
- ]),
547
- })).canonical_human_message_key || "",
548
- ).trim();
549
- if (canonicalHumanMessageKey) {
550
- normalized.canonical_human_message_key = canonicalHumanMessageKey;
551
- }
552
- return [receiptKey, normalized];
553
- }
554
-
555
451
  function buildRunnerLocalInboundEnvelopeFromReceipt(rawReceipt) {
556
452
  const receipt = safeObject(rawReceipt);
557
453
  return normalizeTelegramMessageEnvelope({
@@ -590,7 +486,7 @@ function buildRunnerLocalInboundArtifacts(updates, routeKey, route, bot, destina
590
486
  const chatID = String(update.chatID || "").trim();
591
487
  const messageID = intFromRawAllowZero(update.messageID, 0);
592
488
  const receiptKey = buildRunnerRecentLocalInboundReceiptKey(chatID, messageID);
593
- const receiptEntry = normalizeRunnerRecentLocalInboundReceipt({
489
+ const receiptEntry = normalizeRunnerRecentLocalInboundReceiptEntry({
594
490
  update_id: intFromRawAllowZero(update.updateID, 0),
595
491
  chat_id: chatID,
596
492
  chat_type: update.chatType,
@@ -664,9 +560,9 @@ function buildRunnerRecentLocalInboundReceipts(routeStateRaw, localInboundArtifa
664
560
  const routeState = safeObject(routeStateRaw);
665
561
  const merged = new Map();
666
562
  for (const [key, value] of Object.entries(safeObject(routeState.recent_local_inbound_receipts))) {
667
- const normalizedEntry = normalizeRunnerRecentLocalInboundReceipt(value, key);
668
- if (!normalizedEntry) continue;
669
- merged.set(normalizedEntry[0], normalizedEntry[1]);
563
+ const normalizedEntry = normalizeRunnerRecentLocalInboundReceiptEntry(value, key);
564
+ if (!normalizedEntry) continue;
565
+ merged.set(normalizedEntry[0], normalizedEntry[1]);
670
566
  }
671
567
  for (const artifact of ensureArray(localInboundArtifacts)) {
672
568
  const normalizedEntry = ensureArray(safeObject(artifact).receiptEntry);
@@ -93,6 +93,18 @@ import {
93
93
  buildRunnerRouteDuplicateStateFromComment,
94
94
  buildRunnerRouteStateFromComment,
95
95
  } from "./runner-helpers.mjs";
96
+ import {
97
+ validateRunnerConversationDecisionBundle,
98
+ } from "./runner-orchestration-decision-bundle.mjs";
99
+ import {
100
+ resolveRunnerLocalInboundArtifactOwners,
101
+ } from "./runner-runtime.mjs";
102
+ import {
103
+ buildRunnerLocalInboundReceiptKey,
104
+ findRecentTelegramInboundReceipt,
105
+ normalizeRunnerRecentLocalInboundReceiptEntry,
106
+ normalizeRunnerRecentLocalInboundReceiptMap,
107
+ } from "./runner-local-inbound-receipts.mjs";
96
108
  import {
97
109
  buildRunnerInheritedRootReferenceRecorderResult,
98
110
  buildRunnerRootThreadRecorderFailure,
@@ -22509,9 +22521,128 @@ export async function runSelftestRunnerScenarios(push, deps) {
22509
22521
  [{ id: "comment-3", body: botReplyArchiveComment }],
22510
22522
  { chatID: "-100123", messageID: 4321, parseArchivedChatComment },
22511
22523
  );
22512
- push(
22513
- "archive_bot_reply_dedupe_by_chat_and_message_id",
22514
- String(existingBotReplyRecord?.id || "") === "comment-3",
22515
- `comment_id=${String(existingBotReplyRecord?.id || "(none)")}`,
22516
- );
22517
- }
22524
+ push(
22525
+ "archive_bot_reply_dedupe_by_chat_and_message_id",
22526
+ String(existingBotReplyRecord?.id || "") === "comment-3",
22527
+ `comment_id=${String(existingBotReplyRecord?.id || "(none)")}`,
22528
+ );
22529
+
22530
+ try {
22531
+ const owners = resolveRunnerLocalInboundArtifactOwners({
22532
+ update: {
22533
+ fromIsBot: false,
22534
+ mentionUsernames: ["RyoAI_bot", "RyoAI2_bot", "RyoAI3_bot"],
22535
+ },
22536
+ routeKey: "telegram-monitor-ryoai-bot-2::project::telegram::monitor::dest::actor",
22537
+ route: {
22538
+ botName: "RyoAI_bot",
22539
+ },
22540
+ bot: {
22541
+ username: "RyoAI_bot",
22542
+ },
22543
+ managedConversationBots: [
22544
+ {
22545
+ username: "RyoAI2_bot",
22546
+ routeKey: "telegram-monitor-ryoai2-bot-2::project::telegram::monitor::dest::actor",
22547
+ },
22548
+ {
22549
+ username: "RyoAI3_bot",
22550
+ routeKey: "telegram-monitor-ryoai3-bot::project::telegram::monitor::dest::actor",
22551
+ },
22552
+ ],
22553
+ });
22554
+ push(
22555
+ "runner_runtime_fans_out_human_multi_mention_receipts_to_all_explicit_managed_routes",
22556
+ ensureArray(owners).length === 3
22557
+ && ensureArray(owners).map((item) => String(safeSelftestObject(item).botUsername || "").trim().toLowerCase()).join(",") === "ryoai_bot,ryoai2_bot,ryoai3_bot",
22558
+ `owners=${ensureArray(owners).map((item) => `${String(safeSelftestObject(item).botUsername || "(none)")}:${String(safeSelftestObject(item).routeKey || "(none)")}`).join("|")}`,
22559
+ );
22560
+ } catch (err) {
22561
+ push("runner_runtime_fans_out_human_multi_mention_receipts_to_all_explicit_managed_routes", false, String(err?.message || err));
22562
+ }
22563
+
22564
+ try {
22565
+ const normalizedEntry = normalizeRunnerRecentLocalInboundReceiptEntry({
22566
+ chat_id: "-100123",
22567
+ message_id: 321,
22568
+ kind: "telegram_message",
22569
+ sender_username: "tester",
22570
+ sender_is_bot: false,
22571
+ body: "@RyoAI_bot @RyoAI2_bot hi",
22572
+ occurred_at: "2026-04-06T00:00:00.000Z",
22573
+ receipt_bot_username: "RyoAI_bot",
22574
+ });
22575
+ const normalizedReceipt = safeSelftestObject(ensureArray(normalizedEntry)[1]);
22576
+ const canonicalKey = String(normalizedReceipt.canonical_human_message_key || "").trim();
22577
+ const receiptMap = normalizeRunnerRecentLocalInboundReceiptMap({
22578
+ "-100123:321": {
22579
+ chat_id: "-100123",
22580
+ message_id: 321,
22581
+ kind: "telegram_message",
22582
+ sender_username: "tester",
22583
+ sender_is_bot: false,
22584
+ body: "@RyoAI_bot @RyoAI2_bot hi",
22585
+ occurred_at: "2026-04-06T00:00:00.000Z",
22586
+ receipt_bot_username: "RyoAI_bot",
22587
+ },
22588
+ });
22589
+ const foundByCanonical = findRecentTelegramInboundReceipt(receiptMap, {
22590
+ chatID: "-100123",
22591
+ canonicalHumanMessageKey: canonicalKey,
22592
+ });
22593
+ const archiveReceiptKey = buildRunnerLocalInboundReceiptKey(normalizedReceipt, {
22594
+ resolveTargetSelector: () => "RyoAI_bot",
22595
+ });
22596
+ push(
22597
+ "runner_local_inbound_receipt_helpers_share_canonical_normalization",
22598
+ Boolean(normalizedEntry)
22599
+ && Boolean(canonicalKey)
22600
+ && String(safeSelftestObject(foundByCanonical).canonical_human_message_key || "") === canonicalKey
22601
+ && archiveReceiptKey === `human:${canonicalKey}`,
22602
+ `canonical=${canonicalKey || "(none)"} archive_key=${archiveReceiptKey || "(none)"} found=${String(safeSelftestObject(foundByCanonical).canonical_human_message_key || "(none)")}`,
22603
+ );
22604
+ } catch (err) {
22605
+ push("runner_local_inbound_receipt_helpers_share_canonical_normalization", false, String(err?.message || err));
22606
+ }
22607
+
22608
+ try {
22609
+ const contradictoryDirectValidation = validateRunnerConversationDecisionBundle({
22610
+ schema_version: "runner_conversation_decision.v1",
22611
+ decision_type: "reply_outcome",
22612
+ conversation_intent_mode: "multi_bot_direct",
22613
+ allowed_responders: ["ryoai_bot", "ryoai2_bot", "ryoai3_bot"],
22614
+ initial_responders: ["ryoai_bot", "ryoai2_bot", "ryoai3_bot"],
22615
+ selected_bot_usernames: ["ryoai_bot"],
22616
+ execution_contract_type: "direct_result",
22617
+ execution_contract_targets: ["ryoai_bot"],
22618
+ next_expected_responders: [],
22619
+ should_close_after_reply: true,
22620
+ turn_kind: "direct_reply",
22621
+ actionable_for_current_route: false,
22622
+ });
22623
+ const contradictoryDelegationValidation = validateRunnerConversationDecisionBundle({
22624
+ schema_version: "runner_conversation_decision.v1",
22625
+ decision_type: "reply_outcome",
22626
+ conversation_intent_mode: "multi_bot_direct",
22627
+ allowed_responders: ["ryoai_bot", "ryoai2_bot"],
22628
+ initial_responders: ["ryoai_bot", "ryoai2_bot"],
22629
+ selected_bot_usernames: ["ryoai_bot", "ryoai2_bot"],
22630
+ execution_contract_type: "delegation",
22631
+ execution_contract_targets: ["ryoai2_bot"],
22632
+ next_expected_responders: ["ryoai2_bot"],
22633
+ should_close_after_reply: false,
22634
+ turn_kind: "delegation_kickoff",
22635
+ actionable_for_current_route: true,
22636
+ });
22637
+ push(
22638
+ "runner_decision_bundle_validation_rejects_contradictory_multi_bot_opening_contracts",
22639
+ contradictoryDirectValidation.ok !== true
22640
+ && String(contradictoryDirectValidation.status || "") === "contradictory_multi_initial_close_state"
22641
+ && contradictoryDelegationValidation.ok !== true
22642
+ && String(contradictoryDelegationValidation.status || "") === "contradictory_delegation_intent_mode",
22643
+ `direct=${String(contradictoryDirectValidation.status || "(none)")}:${String(contradictoryDirectValidation.reason || "(none)")} delegation=${String(contradictoryDelegationValidation.status || "(none)")}:${String(contradictoryDelegationValidation.reason || "(none)")}`,
22644
+ );
22645
+ } catch (err) {
22646
+ push("runner_decision_bundle_validation_rejects_contradictory_multi_bot_opening_contracts", false, String(err?.message || err));
22647
+ }
22648
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.293",
3
+ "version": "0.2.294",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [