metheus-governance-mcp-cli 0.2.283 → 0.2.285

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
@@ -145,10 +145,10 @@ import {
145
145
  findEarlierProcessableArchiveDuplicate,
146
146
  findRecentTelegramMessageEnvelope,
147
147
  isTelegramLocalInboundEnvelopeForRoute,
148
- isInboundArchiveKind,
149
- normalizeTelegramMessageEnvelope as normalizeRunnerTelegramMessageEnvelope,
150
- normalizeArchiveCommentRecord,
151
- selectPendingArchiveComments,
148
+ isInboundArchiveKind,
149
+ normalizeTelegramMessageEnvelope as normalizeRunnerTelegramMessageEnvelope,
150
+ normalizeArchiveCommentRecord,
151
+ selectPendingArchiveComments,
152
152
  printRunnerResult,
153
153
  } from "./lib/runner-helpers.mjs";
154
154
  import {
@@ -2812,25 +2812,23 @@ function buildRunnerReplyChainSpeakerType(parsedArchiveRaw) {
2812
2812
  return "human";
2813
2813
  }
2814
2814
 
2815
- function buildRunnerReplyChainSpeakerLabel(parsedArchiveRaw) {
2816
- const parsedArchive = safeObject(parsedArchiveRaw);
2817
- const username = normalizeTelegramMentionUsername(
2818
- parsedArchive.botUsername
2819
- || parsedArchive.username
2820
- || parsedArchive.replyToUsername
2821
- || parsedArchive.conversationTargetBotUsername,
2822
- );
2823
- if (username) {
2824
- return `@${username}`;
2825
- }
2826
- return firstNonEmptyString([
2827
- parsedArchive.sender,
2828
- parsedArchive.botName,
2829
- parsedArchive.replyToSender,
2830
- parsedArchive.chatTitle,
2831
- buildRunnerReplyChainSpeakerType(parsedArchive) === "bot" ? "bot" : "human",
2832
- ]);
2833
- }
2815
+ function buildRunnerReplyChainSpeakerLabel(parsedArchiveRaw) {
2816
+ const parsedArchive = safeObject(parsedArchiveRaw);
2817
+ const username = normalizeTelegramMentionUsername(
2818
+ parsedArchive.botUsername
2819
+ || parsedArchive.username
2820
+ || parsedArchive.conversationTargetBotUsername,
2821
+ );
2822
+ if (username) {
2823
+ return `@${username}`;
2824
+ }
2825
+ return firstNonEmptyString([
2826
+ parsedArchive.sender,
2827
+ parsedArchive.botName,
2828
+ parsedArchive.chatTitle,
2829
+ buildRunnerReplyChainSpeakerType(parsedArchive) === "bot" ? "bot" : "human",
2830
+ ]);
2831
+ }
2834
2832
 
2835
2833
  function normalizeRunnerReplyChainSnapshot(rawSnapshot) {
2836
2834
  const snapshot = safeObject(rawSnapshot);
@@ -4015,17 +4013,18 @@ function runnerRequestPreferredAuthoritySelectedBotUsernames(entryRaw) {
4015
4013
  );
4016
4014
  }
4017
4015
 
4018
- function extractRunnerExplicitSelectedBotUsernamesFromParsed(parsedArchiveRaw) {
4019
- const parsedArchive = safeObject(parsedArchiveRaw);
4020
- return uniqueOrderedStrings(
4021
- ensureArray(parsedArchive.mentionUsernames).length
4022
- ? parsedArchive.mentionUsernames
4023
- : String(parsedArchive.replyToUsername || "").trim()
4024
- ? [parsedArchive.replyToUsername]
4025
- : [],
4026
- normalizeTelegramMentionUsername,
4027
- );
4028
- }
4016
+ function extractRunnerExplicitSelectedBotUsernamesFromParsed(parsedArchiveRaw) {
4017
+ const parsedArchive = safeObject(parsedArchiveRaw);
4018
+ return uniqueOrderedStrings(
4019
+ ensureArray(parsedArchive.mentionUsernames).length
4020
+ ? parsedArchive.mentionUsernames
4021
+ : parsedArchive.replyToSenderIsBot === true
4022
+ && String(parsedArchive.replyToUsername || "").trim()
4023
+ ? [parsedArchive.replyToUsername]
4024
+ : [],
4025
+ normalizeTelegramMentionUsername,
4026
+ );
4027
+ }
4029
4028
 
4030
4029
  function runnerRequestPreferredAIReplyPreview(entryRaw) {
4031
4030
  const entry = safeObject(entryRaw);
@@ -6430,10 +6429,7 @@ function markRunnerRequestLifecycle({
6430
6429
  const authoritativeDecisionBundle = resolvedDecisionBundleValidation.ok === true
6431
6430
  ? safeObject(resolvedDecisionBundleValidation.bundle)
6432
6431
  : runnerRequestAuthoritativeDecisionBundle(existing);
6433
- const effectiveReplyToMessageID = intFromRawAllowZero(
6434
- replyToMessageID,
6435
- intFromRawAllowZero(existing.last_reply_to_message_id, 0),
6436
- );
6432
+ const effectiveReplyToMessageID = intFromRawAllowZero(replyToMessageID, 0);
6437
6433
  const lastReplyMessageEnvelope = buildTelegramBotReplyEnvelope({
6438
6434
  sourceEnvelope: sourceMessageEnvelope,
6439
6435
  chatID: existing.chat_id,
@@ -6454,14 +6450,14 @@ function markRunnerRequestLifecycle({
6454
6450
  senderUsername: normalizedCurrentBotSelector,
6455
6451
  body: aiReplyPreview,
6456
6452
  });
6457
- const shouldRefreshAttemptedDeliveryEnvelope = (
6458
- aiReplyGenerated === true
6459
- || String(aiReplyPreview || "").trim().length > 0
6460
- || String(deliveryStatus || "").trim().length > 0
6461
- || String(transportError || "").trim().length > 0
6462
- || intFromRawAllowZero(replyToMessageID, 0) > 0
6463
- || intFromRawAllowZero(lastReplyMessageThreadID, 0) > 0
6464
- );
6453
+ const shouldRefreshAttemptedDeliveryEnvelope = (
6454
+ aiReplyGenerated === true
6455
+ || String(aiReplyPreview || "").trim().length > 0
6456
+ || String(deliveryStatus || "").trim().length > 0
6457
+ || String(transportError || "").trim().length > 0
6458
+ || intFromRawAllowZero(replyToMessageID, 0) > 0
6459
+ || intFromRawAllowZero(lastReplyMessageThreadID, 0) > 0
6460
+ );
6465
6461
  const rootEffectiveExecutionContractTargets = uniqueOrderedStrings(
6466
6462
  [
6467
6463
  ...ensureArray(authoritativeDecisionBundle.execution_contract_targets),
@@ -6504,6 +6500,13 @@ function markRunnerRequestLifecycle({
6504
6500
  const normalizedOutcome = String(outcome || "").trim().toLowerCase();
6505
6501
  const normalizedFailureReplyClassification = String(failureReplyClassification || "").trim().toLowerCase();
6506
6502
  const normalizedFailureFacts = safeObject(failureFacts);
6503
+ const shouldPersistReplyAnchor = (
6504
+ aiReplyGenerated === true
6505
+ || intFromRawAllowZero(lastReplyMessageID, 0) > 0
6506
+ || ["delivered", "dry_run", "archive_error", "failed_transport"].includes(normalizedDeliveryStatus)
6507
+ || String(transportError || "").trim().length > 0
6508
+ || ["replied", "delivery_failed_after_generation"].includes(normalizedOutcome)
6509
+ );
6507
6510
  const shouldRemainRunningAfterReply = authoritativeDecisionBundle.should_close_after_reply === true
6508
6511
  ? false
6509
6512
  : authoritativeDecisionBundle.should_close_after_reply === false
@@ -6907,7 +6910,9 @@ function markRunnerRequestLifecycle({
6907
6910
  last_source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || existing.last_source_message_thread_id,
6908
6911
  last_reply_message_id: intFromRawAllowZero(lastReplyMessageID, 0) || existing.last_reply_message_id,
6909
6912
  last_reply_message_thread_id: intFromRawAllowZero(lastReplyMessageThreadID, 0) || existing.last_reply_message_thread_id,
6910
- last_reply_to_message_id: intFromRawAllowZero(replyToMessageID, 0) || existing.last_reply_to_message_id,
6913
+ last_reply_to_message_id: shouldPersistReplyAnchor
6914
+ ? effectiveReplyToMessageID
6915
+ : existing.last_reply_to_message_id,
6911
6916
  last_reply_message_envelope: persistSuccessfulReplyEnvelope
6912
6917
  ? lastReplyMessageEnvelope
6913
6918
  : safeObject(existing.last_reply_message_envelope),
@@ -18679,13 +18684,14 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
18679
18684
  workspaceDir: "",
18680
18685
  workspaceSignalTrusted: true,
18681
18686
  },
18682
- {
18683
- ...buildLocalProjectDispatchDeps(),
18684
- extractActorFromToken: () => ({ user_id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" }),
18685
- createProjectContextItem: async (input) => {
18686
- capturedCreateArgs = input;
18687
- return {
18688
- id: "99999999-8888-4777-8666-555555555555",
18687
+ {
18688
+ ...buildLocalProjectDispatchDeps(),
18689
+ extractActorFromToken: () => ({ user_id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" }),
18690
+ listProjectContextItems: async () => ([]),
18691
+ createProjectContextItem: async (input) => {
18692
+ capturedCreateArgs = input;
18693
+ return {
18694
+ id: "99999999-8888-4777-8666-555555555555",
18689
18695
  project_id: input.projectID,
18690
18696
  title: input.title,
18691
18697
  body: input.body,
@@ -18709,11 +18715,81 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
18709
18715
  && String(safeObject(capturedCreateArgs).category || "").trim() === "bot_role",
18710
18716
  `status=${String(safeObject(suggestStructured.item).status || "").trim() || "(none)"} category=${String(safeObject(capturedCreateArgs).category || "").trim() || "(none)"}`,
18711
18717
  );
18712
- } catch (err) {
18713
- push("local_project_context_suggest_tool_dispatches", false, String(err?.message || err));
18714
- }
18715
-
18716
- const previousRunnerBotID = process.env.METHEUS_RUNNER_BOT_ID;
18718
+ } catch (err) {
18719
+ push("local_project_context_suggest_tool_dispatches", false, String(err?.message || err));
18720
+ }
18721
+
18722
+ try {
18723
+ let duplicateCreateCalls = 0;
18724
+ const duplicateSuggestResponse = await handleLocalProjectToolDispatchImpl(
18725
+ {
18726
+ requestObj: {
18727
+ jsonrpc: "2.0",
18728
+ id: 5,
18729
+ method: "tools/call",
18730
+ params: {
18731
+ name: "project.context.suggest",
18732
+ arguments: {
18733
+ project_id: selftestProjectID,
18734
+ title: "Bot role split",
18735
+ body: "RyoAI2_bot handles deep review when explicitly asked.",
18736
+ category: "bot_role",
18737
+ importance: "high",
18738
+ },
18739
+ },
18740
+ },
18741
+ toolName: "project.context.suggest",
18742
+ toolArgs: {
18743
+ project_id: selftestProjectID,
18744
+ title: "Bot role split",
18745
+ body: "RyoAI2_bot handles deep review when explicitly asked.",
18746
+ category: "bot_role",
18747
+ importance: "high",
18748
+ },
18749
+ args: {
18750
+ baseURL: DEFAULT_SITE_URL,
18751
+ timeoutSeconds: 30,
18752
+ },
18753
+ token: "selftest-token",
18754
+ workspaceDir: "",
18755
+ workspaceSignalTrusted: true,
18756
+ },
18757
+ {
18758
+ ...buildLocalProjectDispatchDeps(),
18759
+ extractActorFromToken: () => ({ user_id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" }),
18760
+ listProjectContextItems: async () => ([
18761
+ {
18762
+ id: "ctx-duplicate-suggested",
18763
+ project_id: selftestProjectID,
18764
+ title: "Bot role split",
18765
+ body: "RyoAI2_bot handles deep review when explicitly asked.",
18766
+ category: "bot_role",
18767
+ importance: "high",
18768
+ status: "suggested",
18769
+ },
18770
+ ]),
18771
+ createProjectContextItem: async () => {
18772
+ duplicateCreateCalls += 1;
18773
+ return {};
18774
+ },
18775
+ },
18776
+ );
18777
+ const duplicateSuggestStructured = safeObject(safeObject(duplicateSuggestResponse).result)?.structuredContent || {};
18778
+ push(
18779
+ "local_project_context_suggest_tool_skips_exact_duplicate",
18780
+ duplicateSuggestStructured.ok === true
18781
+ && duplicateSuggestStructured.created === false
18782
+ && duplicateSuggestStructured.duplicate === true
18783
+ && String(duplicateSuggestStructured.reason || "").trim() === "duplicate_suggested"
18784
+ && duplicateCreateCalls === 0
18785
+ && String(safeObject(duplicateSuggestStructured.item).id || "").trim() === "ctx-duplicate-suggested",
18786
+ `created=${String(duplicateSuggestStructured.created)} duplicate=${String(duplicateSuggestStructured.duplicate)} reason=${String(duplicateSuggestStructured.reason || "").trim() || "(none)"} create_calls=${duplicateCreateCalls}`,
18787
+ );
18788
+ } catch (err) {
18789
+ push("local_project_context_suggest_tool_skips_exact_duplicate", false, String(err?.message || err));
18790
+ }
18791
+
18792
+ const previousRunnerBotID = process.env.METHEUS_RUNNER_BOT_ID;
18717
18793
  const previousRunnerBotName = process.env.METHEUS_RUNNER_BOT_NAME;
18718
18794
  try {
18719
18795
  process.env.METHEUS_RUNNER_BOT_ID = "bot-selftest-123";
@@ -1772,11 +1772,12 @@ function importanceRankForProjectContext(value) {
1772
1772
 
1773
1773
  function formatProjectContextLine(item) {
1774
1774
  const contextItem = safeObject(item);
1775
+ const status = String(contextItem.status || "active").trim() || "active";
1775
1776
  const category = String(contextItem.category || "general").trim() || "general";
1776
1777
  const importance = String(contextItem.importance || "normal").trim() || "normal";
1777
1778
  const title = truncatePromptLineText(contextItem.title || "-", 100) || "-";
1778
1779
  const body = truncatePromptLineText(contextItem.body || "(empty)", 240) || "(empty)";
1779
- return `- [${category}/${importance}] ${title} | ${body}`;
1780
+ return `- [${status}/${category}/${importance}] ${title} | ${body}`;
1780
1781
  }
1781
1782
 
1782
1783
  function inferCurrentTurnPurpose({ trigger, conversation, selfBotUsername, otherMentionedBots }) {
@@ -1815,8 +1816,15 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1815
1816
  const context = Array.isArray(safePayload.context_comments) ? safePayload.context_comments : [];
1816
1817
  const projectContextItems = ensureArray(safePayload.project_context_items)
1817
1818
  .map((item) => safeObject(item))
1818
- .filter((item) => String(item.status || "active").trim().toLowerCase() === "active")
1819
+ .filter((item) => ["active", "suggested"].includes(String(item.status || "active").trim().toLowerCase()))
1819
1820
  .sort((left, right) => {
1821
+ const leftStatus = String(left.status || "active").trim().toLowerCase();
1822
+ const rightStatus = String(right.status || "active").trim().toLowerCase();
1823
+ const leftStatusRank = leftStatus === "active" ? 0 : 1;
1824
+ const rightStatusRank = rightStatus === "active" ? 0 : 1;
1825
+ if (leftStatusRank !== rightStatusRank) {
1826
+ return leftStatusRank - rightStatusRank;
1827
+ }
1820
1828
  const importanceDelta = importanceRankForProjectContext(right.importance) - importanceRankForProjectContext(left.importance);
1821
1829
  if (importanceDelta !== 0) return importanceDelta;
1822
1830
  const leftUpdated = Date.parse(firstNonEmptyString([left.updated_at, left.updatedAt, left.created_at, left.createdAt]));
@@ -1994,11 +2002,13 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1994
2002
  `- Workspace: ${workspaceDir || "-"}`,
1995
2003
  `- Destination: ${destinationLabel || "-"} (${firstNonEmptyString([safePayload.destination?.chat_id, safePayload.destination?.chatID, process.env.METHEUS_RUNNER_CHAT_ID]) || "-"})`,
1996
2004
  "",
1997
- "Active project context (stable facts and operating rules):",
2005
+ "Existing project context memory (active and suggested):",
1998
2006
  projectContextItems.length
1999
2007
  ? projectContextItems.map(formatProjectContextLine).join("\n")
2000
2008
  : "- none",
2001
- "Prefer these active project context items over stale room chatter when they conflict.",
2009
+ "Active items are authoritative shared memory.",
2010
+ "Suggested items are pending shared-memory candidates and must not be duplicated if they already capture the same durable fact, rule, focus, or risk.",
2011
+ "Prefer active items over stale room chatter when they conflict.",
2002
2012
  "",
2003
2013
  "Recent human messages (prioritized context):",
2004
2014
  recentHumanMessages.length ? recentHumanMessages.map(formatContextCommentLine).join("\n") : "- none",
@@ -2164,6 +2174,7 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
2164
2174
  "- Only include context_suggestion when the human explicitly establishes a durable shared project rule, bot role assignment, project fact, current focus, or risk that should be remembered beyond this chat.",
2165
2175
  "- Never include context_suggestion for greetings, questions, status checks, file-location questions, workspace paths, or temporary/personal requests.",
2166
2176
  "- Do not use category \"workspace\" for chat-room context memory. Workspace paths must be answered from tools, not stored as shared chat context.",
2177
+ "- If an existing active or suggested project context item already captures the same durable meaning, do not emit a new context_suggestion with should_store=true.",
2167
2178
  "- When you include context_suggestion, set should_store=true and keep title/body concise and durable.",
2168
2179
  "",
2169
2180
  isInternalExecutionStep
@@ -1,5 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import {
4
+ findExactProjectContextDuplicate,
5
+ } from "./project-context-dedupe.mjs";
3
6
 
4
7
  function requireDependency(deps, key) {
5
8
  const candidate = deps?.[key];
@@ -112,6 +115,22 @@ function buildProjectContextListText(projectID, items, statusFilter = "") {
112
115
  return lines.join("\n");
113
116
  }
114
117
 
118
+ function buildProjectContextDuplicateText(projectID, duplicate, attemptedStatus) {
119
+ const item = normalizeProjectContextRecord(duplicate);
120
+ return [
121
+ `project_id: ${projectID}`,
122
+ "created: false",
123
+ "duplicate: true",
124
+ `reason: duplicate_${String(item.status || "existing").trim().toLowerCase() || "existing"}`,
125
+ `existing_context_id: ${item.id || "-"}`,
126
+ `existing_status: ${item.status || "-"}`,
127
+ `attempted_status: ${String(attemptedStatus || "").trim() || "-"}`,
128
+ `title: ${item.title || "-"}`,
129
+ "",
130
+ item.body || "",
131
+ ].join("\n").trim();
132
+ }
133
+
115
134
  function resolveProjectID(toolArgs, args, workspaceDir, deps) {
116
135
  const resolveProjectIDForRequest = requireDependency(deps, "resolveProjectIDForRequest");
117
136
  return String(
@@ -478,6 +497,35 @@ export async function handleLocalProjectToolDispatch(
478
497
  if (!body) {
479
498
  return jsonRpcError(requestObj, -32001, "body is required");
480
499
  }
500
+ const requestedStatus = normalizeProjectContextStatus(
501
+ toolArgs.status,
502
+ toolName === "project.context.suggest" ? "suggested" : "active",
503
+ );
504
+ const existingItems = ensureArray(await listProjectContextItems({
505
+ siteBaseURL,
506
+ projectID,
507
+ token,
508
+ timeoutSeconds,
509
+ actorUserID,
510
+ })).map((item) => normalizeProjectContextRecord(item));
511
+ const duplicate = findExactProjectContextDuplicate(existingItems, {
512
+ title,
513
+ body,
514
+ }, {
515
+ statuses: ["active", "suggested"],
516
+ });
517
+ if (duplicate) {
518
+ return jsonRpcResult(requestObj, {
519
+ content: [{ type: "text", text: buildProjectContextDuplicateText(projectID, duplicate, requestedStatus) }],
520
+ structuredContent: {
521
+ ok: true,
522
+ created: false,
523
+ duplicate: true,
524
+ reason: `duplicate_${String(duplicate.status || "").trim().toLowerCase() || "existing"}`,
525
+ item: normalizeProjectContextRecord(duplicate),
526
+ },
527
+ });
528
+ }
481
529
  const created = normalizeProjectContextRecord(await createProjectContextItem({
482
530
  siteBaseURL,
483
531
  token,
@@ -488,7 +536,7 @@ export async function handleLocalProjectToolDispatch(
488
536
  body,
489
537
  category: normalizeProjectContextCategory(toolArgs.category),
490
538
  importance: normalizeProjectContextImportance(toolArgs.importance),
491
- status: normalizeProjectContextStatus(toolArgs.status, toolName === "project.context.suggest" ? "suggested" : "active"),
539
+ status: requestedStatus,
492
540
  }));
493
541
  return jsonRpcResult(requestObj, {
494
542
  content: [{ type: "text", text: buildProjectContextText(created) }],
@@ -0,0 +1,48 @@
1
+ function safeObject(value) {
2
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3
+ return {};
4
+ }
5
+ return value;
6
+ }
7
+
8
+ function ensureArray(value) {
9
+ return Array.isArray(value) ? value : [];
10
+ }
11
+
12
+ export function normalizeProjectContextComparableText(value) {
13
+ return String(value || "")
14
+ .trim()
15
+ .toLowerCase()
16
+ .replace(/\s+/g, " ");
17
+ }
18
+
19
+ export function normalizeProjectContextDuplicateRecord(rawItem) {
20
+ const item = safeObject(rawItem);
21
+ return {
22
+ id: String(item.id || item.context_id || "").trim(),
23
+ title: String(item.title || "").trim(),
24
+ body: String(item.body || "").trim(),
25
+ status: String(item.status || "").trim().toLowerCase(),
26
+ };
27
+ }
28
+
29
+ export function findExactProjectContextDuplicate(rawItems, rawCandidate, options = {}) {
30
+ const allowedStatuses = new Set(
31
+ ensureArray(options.statuses && options.statuses.length ? options.statuses : ["active", "suggested"])
32
+ .map((value) => String(value || "").trim().toLowerCase())
33
+ .filter(Boolean),
34
+ );
35
+ const candidate = normalizeProjectContextDuplicateRecord(rawCandidate);
36
+ if (!candidate.title || !candidate.body) {
37
+ return null;
38
+ }
39
+ const candidateTitle = normalizeProjectContextComparableText(candidate.title);
40
+ const candidateBody = normalizeProjectContextComparableText(candidate.body);
41
+ return ensureArray(rawItems)
42
+ .map((item) => normalizeProjectContextDuplicateRecord(item))
43
+ .find((item) => (
44
+ allowedStatuses.has(item.status)
45
+ && normalizeProjectContextComparableText(item.title) === candidateTitle
46
+ && normalizeProjectContextComparableText(item.body) === candidateBody
47
+ )) || null;
48
+ }
@@ -13,6 +13,14 @@ function intFromRawAllowZero(value, fallback = 0) {
13
13
  return Number.isFinite(parsed) ? parsed : fallback;
14
14
  }
15
15
 
16
+ function buildReplyAnchorMismatchError(expectedReplyToMessageID, observedReplyToMessageID) {
17
+ return `reply anchor mismatch: expected ${String(expectedReplyToMessageID || 0)}, observed ${String(observedReplyToMessageID || 0)}`;
18
+ }
19
+
20
+ function buildMessageThreadMismatchError(expectedMessageThreadID, observedMessageThreadID) {
21
+ return `message thread mismatch: expected ${String(expectedMessageThreadID || 0)}, observed ${String(observedMessageThreadID || 0)}`;
22
+ }
23
+
16
24
  function requireArchiveDependency(deps, key) {
17
25
  const candidate = deps?.[key];
18
26
  if (typeof candidate !== "function") {
@@ -83,11 +91,41 @@ export async function finalizeLocalBotDeliveryArchive({
83
91
  deliveredResult.message_id ?? deliveredBody.message_id ?? deliveredBody.ts,
84
92
  0,
85
93
  );
86
- const deliveredMessageThreadID = intFromRawAllowZero(
94
+ const observedMessageThreadID = intFromRawAllowZero(
87
95
  deliveredResult.message_thread_id ?? deliveredBody.message_thread_id ?? delivery.effectiveMessageThreadID,
88
96
  intFromRawAllowZero(messageThreadID, 0),
89
97
  );
90
- const archiveReplyToMessageID = intFromRawAllowZero(delivery.effectiveReplyToMessageID, replyToMessageID);
98
+ const expectedMessageThreadID = intFromRawAllowZero(messageThreadID, 0);
99
+ const expectedReplyToMessageID = intFromRawAllowZero(replyToMessageID, 0);
100
+ const observedReplyToMessageID = intFromRawAllowZero(delivery.effectiveReplyToMessageID, 0);
101
+ if (
102
+ (expectedReplyToMessageID > 0 || observedReplyToMessageID > 0)
103
+ && observedReplyToMessageID !== expectedReplyToMessageID
104
+ ) {
105
+ return {
106
+ ok: false,
107
+ error: buildReplyAnchorMismatchError(expectedReplyToMessageID, observedReplyToMessageID),
108
+ reply_anchor_mismatch: true,
109
+ expected_reply_to_message_id: expectedReplyToMessageID,
110
+ observed_reply_to_message_id: observedReplyToMessageID,
111
+ thread_id: thread.threadID,
112
+ work_item_id: thread.workItemID,
113
+ };
114
+ }
115
+ if (
116
+ (expectedMessageThreadID > 0 || observedMessageThreadID > 0)
117
+ && observedMessageThreadID !== expectedMessageThreadID
118
+ ) {
119
+ return {
120
+ ok: false,
121
+ error: buildMessageThreadMismatchError(expectedMessageThreadID, observedMessageThreadID),
122
+ message_thread_mismatch: true,
123
+ expected_message_thread_id: expectedMessageThreadID,
124
+ observed_message_thread_id: observedMessageThreadID,
125
+ thread_id: thread.threadID,
126
+ work_item_id: thread.workItemID,
127
+ };
128
+ }
91
129
  if (archiveDedupeOutbound && deliveredMessageID > 0) {
92
130
  const existingComments = await listThreadCommentsTail({
93
131
  siteBaseURL,
@@ -119,8 +157,8 @@ export async function finalizeLocalBotDeliveryArchive({
119
157
  destination,
120
158
  replyText: text,
121
159
  messageID: deliveredMessageID,
122
- messageThreadID: deliveredMessageThreadID,
123
- replyToMessageID: archiveReplyToMessageID,
160
+ messageThreadID: expectedMessageThreadID,
161
+ replyToMessageID: expectedReplyToMessageID,
124
162
  conversation: archiveConversation,
125
163
  });
126
164
  const createdComment = await createThreadComment({
@@ -275,13 +275,11 @@ export function buildTelegramMessageEnvelopeFromParsedArchive(parsedArchiveRaw,
275
275
  overrides.sender,
276
276
  parsedArchive.sender,
277
277
  parsedArchive.botName,
278
- parsedArchive.replyToSender,
279
278
  ]),
280
279
  sender_username: firstNonEmptyString([
281
280
  overrides.sender_username,
282
281
  parsedArchive.botUsername,
283
282
  parsedArchive.username,
284
- parsedArchive.replyToUsername,
285
283
  ]),
286
284
  sender_is_bot: overrides.sender_is_bot ?? parsedArchive.senderIsBot,
287
285
  body: firstNonEmptyString([overrides.body, parsedArchive.body]),
@@ -314,6 +312,27 @@ export function buildTelegramMessageEnvelopeFromParsedArchive(parsedArchiveRaw,
314
312
  });
315
313
  }
316
314
 
315
+ export function resolveTelegramReplyAnchorMessageID({
316
+ replyToMessageID = 0,
317
+ sourceEnvelope: sourceEnvelopeRaw = {},
318
+ fallbackReplyToMessageID = 0,
319
+ } = {}) {
320
+ const explicitReplyToMessageID = intFromRawAllowZero(replyToMessageID, 0);
321
+ if (explicitReplyToMessageID > 0) {
322
+ return explicitReplyToMessageID;
323
+ }
324
+ const sourceEnvelope = normalizeTelegramMessageEnvelope(sourceEnvelopeRaw);
325
+ const sourceMessageID = intFromRawAllowZero(sourceEnvelope.message_id, 0);
326
+ if (sourceMessageID > 0) {
327
+ return sourceMessageID;
328
+ }
329
+ const sourceReplyToMessageID = intFromRawAllowZero(sourceEnvelope.reply_to_message_id, 0);
330
+ if (sourceReplyToMessageID > 0) {
331
+ return sourceReplyToMessageID;
332
+ }
333
+ return intFromRawAllowZero(fallbackReplyToMessageID, 0);
334
+ }
335
+
317
336
  export function buildTelegramBotReplyEnvelope({
318
337
  sourceEnvelope: sourceEnvelopeRaw = {},
319
338
  chatID = "",
@@ -499,7 +518,7 @@ function buildContextSpeakerLabel(parsed) {
499
518
  if (username) {
500
519
  return `@${String(username).replace(/^@+/, "")}`;
501
520
  }
502
- return firstNonEmptyString([parsed.sender, parsed.botName, parsed.replyToSender]);
521
+ return firstNonEmptyString([parsed.sender, parsed.botName]);
503
522
  }
504
523
 
505
524
  function normalizeContextWindowOptions(rawOptions) {
@@ -561,7 +561,9 @@ function summarizeStartupLoopFacts({
561
561
  execution_contract_targets: ensureArray(parsed.executionContractAssignments)
562
562
  .map((item) => normalizeMentionSelector(safeObject(item).targetBot))
563
563
  .filter(Boolean),
564
- reply_to_bot_username: normalizeMentionSelector(parsed.replyToUsername),
564
+ reply_to_bot_username: parsed.replyToSenderIsBot === true
565
+ ? normalizeMentionSelector(parsed.replyToUsername)
566
+ : "",
565
567
  newer_human_message_count: newerHumanMessages.length,
566
568
  has_newer_human_message: newerHumanMessages.length > 0,
567
569
  bot_reply_count_since_last_human: botReplysSinceLastHuman.length + 1,
@@ -52,7 +52,7 @@ export async function prepareRunnerSelectedRecordContext({
52
52
  reduceContextWindowForHumanIntent,
53
53
  buildRunnerContextWindow,
54
54
  buildHumanIntentContextWindowOptions,
55
- loadActiveProjectContextItems,
55
+ loadProjectContextItemsForPrompt,
56
56
  isInformationalHumanIntentType,
57
57
  resolveInformationalQueryExecutionPlan,
58
58
  buildRunnerInputPayload,
@@ -76,7 +76,7 @@ export async function prepareRunnerSelectedRecordContext({
76
76
  || typeof reduceContextWindowForHumanIntent !== "function"
77
77
  || typeof buildRunnerContextWindow !== "function"
78
78
  || typeof buildHumanIntentContextWindowOptions !== "function"
79
- || typeof loadActiveProjectContextItems !== "function"
79
+ || typeof loadProjectContextItemsForPrompt !== "function"
80
80
  || typeof isInformationalHumanIntentType !== "function"
81
81
  || typeof resolveInformationalQueryExecutionPlan !== "function"
82
82
  || typeof buildRunnerInputPayload !== "function"
@@ -372,7 +372,7 @@ export async function prepareRunnerSelectedRecordContext({
372
372
  ),
373
373
  directHumanResponseContract.intentType,
374
374
  );
375
- const projectContextItems = await loadActiveProjectContextItems({
375
+ const projectContextItems = await loadProjectContextItemsForPrompt({
376
376
  normalizedRoute,
377
377
  runtime,
378
378
  deps: executionDeps,
@@ -84,6 +84,11 @@ export async function prepareRunnerSelectedRecordDeliveryContext({
84
84
  const normalizedConversationMode = String(effectiveConversationContext?.mode || "").trim().toLowerCase();
85
85
  const normalizedConversationStage = String(effectiveConversationContext?.stage || "").trim().toLowerCase();
86
86
  const normalizedGuideVisibleReplyTarget = normalizeMentionSelector(safeObject(guidePacket).visible_reply_target);
87
+ const managedPeerSelectors = new Set(
88
+ Array.from(directHumanPeerMap instanceof Map ? directHumanPeerMap.keys() : [])
89
+ .map((item) => normalizeMentionSelector(item))
90
+ .filter(Boolean),
91
+ );
87
92
  const visibleDelegationTargets = uniqueOrdered(
88
93
  normalizedExecutionContractType === "delegation"
89
94
  ? [
@@ -122,6 +127,7 @@ export async function prepareRunnerSelectedRecordDeliveryContext({
122
127
  effectiveResponseContractValidation.ok
123
128
  && normalizedReplyTargetBotSelector
124
129
  && normalizedReplyTargetBotSelector !== currentBotSelector
130
+ && managedPeerSelectors.has(normalizedReplyTargetBotSelector)
125
131
  && normalizedGuideVisibleReplyTarget
126
132
  && normalizedGuideVisibleReplyTarget === normalizedReplyTargetBotSelector
127
133
  ) {
@@ -1,3 +1,7 @@
1
+ import {
2
+ resolveTelegramReplyAnchorMessageID,
3
+ } from "./runner-helpers.mjs";
4
+
1
5
  function safeObject(value) {
2
6
  if (!value || typeof value !== "object" || Array.isArray(value)) {
3
7
  return {};
@@ -154,9 +158,20 @@ export function prepareRunnerSelectedRecordIngress({
154
158
  currentBotSelector,
155
159
  });
156
160
  const replyMessageThreadID = intFromRawAllowZero(sourceMessageEnvelope.message_thread_id, 0);
157
- const replyToMessageID = intFromRawAllowZero(sourceMessageEnvelope.message_id, 0);
161
+ const sourceMessageID = intFromRawAllowZero(sourceMessageEnvelope.message_id, 0);
162
+ const sourceReplyToMessageID = intFromRawAllowZero(sourceMessageEnvelope.reply_to_message_id, 0);
163
+ const replyToMessageID = resolveTelegramReplyAnchorMessageID({
164
+ replyToMessageID: sourceMessageID,
165
+ sourceEnvelope: sourceMessageEnvelope,
166
+ });
158
167
  const replyAnchorSource = String(sourceMessageEnvelope.source_origin || "").trim()
159
- || (replyToMessageID > 0 ? "source_message_envelope" : "");
168
+ || (replyToMessageID > 0
169
+ ? sourceMessageID > 0
170
+ ? "source_message_envelope"
171
+ : sourceReplyToMessageID > 0
172
+ ? "source_message_envelope_reply_to"
173
+ : ""
174
+ : "");
160
175
 
161
176
  return {
162
177
  handledResult: null,