metheus-governance-mcp-cli 0.2.284 → 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
@@ -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);
@@ -18685,13 +18684,14 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
18685
18684
  workspaceDir: "",
18686
18685
  workspaceSignalTrusted: true,
18687
18686
  },
18688
- {
18689
- ...buildLocalProjectDispatchDeps(),
18690
- extractActorFromToken: () => ({ user_id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" }),
18691
- createProjectContextItem: async (input) => {
18692
- capturedCreateArgs = input;
18693
- return {
18694
- 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",
18695
18695
  project_id: input.projectID,
18696
18696
  title: input.title,
18697
18697
  body: input.body,
@@ -18715,11 +18715,81 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
18715
18715
  && String(safeObject(capturedCreateArgs).category || "").trim() === "bot_role",
18716
18716
  `status=${String(safeObject(suggestStructured.item).status || "").trim() || "(none)"} category=${String(safeObject(capturedCreateArgs).category || "").trim() || "(none)"}`,
18717
18717
  );
18718
- } catch (err) {
18719
- push("local_project_context_suggest_tool_dispatches", false, String(err?.message || err));
18720
- }
18721
-
18722
- 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;
18723
18793
  const previousRunnerBotName = process.env.METHEUS_RUNNER_BOT_NAME;
18724
18794
  try {
18725
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
+ }
@@ -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]),
@@ -520,7 +518,7 @@ function buildContextSpeakerLabel(parsed) {
520
518
  if (username) {
521
519
  return `@${String(username).replace(/^@+/, "")}`;
522
520
  }
523
- return firstNonEmptyString([parsed.sender, parsed.botName, parsed.replyToSender]);
521
+ return firstNonEmptyString([parsed.sender, parsed.botName]);
524
522
  }
525
523
 
526
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
  ) {
@@ -107,6 +107,9 @@ import {
107
107
  import {
108
108
  isInternalRuntimeArtifactPath,
109
109
  } from "./local-ai-adapters.mjs";
110
+ import {
111
+ findExactProjectContextDuplicate,
112
+ } from "./project-context-dedupe.mjs";
110
113
 
111
114
  function safeObject(value) {
112
115
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -266,11 +269,14 @@ export function buildRunnerInputPayload({
266
269
  .map((value) => String(value || "").trim().toLowerCase())
267
270
  .filter(Boolean),
268
271
  );
272
+ const parsedReplyTargetBot = parsed.replyToSenderIsBot === true
273
+ ? parsed.replyToUsername
274
+ : "";
269
275
  const normalizedReplyTargetBot = normalizeMentionSelector(
270
276
  directHumanIntent.replyTargetBotSelector
271
277
  || directHumanIntent.reply_target_bot
272
278
  || directHumanIntent.reply_to_bot_username
273
- || parsed.replyToUsername,
279
+ || parsedReplyTargetBot,
274
280
  );
275
281
  const normalizedIntentMode = String(
276
282
  directHumanIntent.intentMode || directHumanIntent.intent_mode || directHumanIntent.mode || "",
@@ -626,6 +632,11 @@ function projectContextImportanceRank(value) {
626
632
  function compareProjectContextItems(leftRaw, rightRaw) {
627
633
  const left = normalizeProjectContextItem(leftRaw);
628
634
  const right = normalizeProjectContextItem(rightRaw);
635
+ const leftStatus = String(left.status || "").trim().toLowerCase();
636
+ const rightStatus = String(right.status || "").trim().toLowerCase();
637
+ const leftStatusRank = leftStatus === "active" ? 0 : leftStatus === "suggested" ? 1 : 2;
638
+ const rightStatusRank = rightStatus === "active" ? 0 : rightStatus === "suggested" ? 1 : 2;
639
+ if (leftStatusRank !== rightStatusRank) return leftStatusRank - rightStatusRank;
629
640
  const importanceDelta = projectContextImportanceRank(right.importance) - projectContextImportanceRank(left.importance);
630
641
  if (importanceDelta !== 0) return importanceDelta;
631
642
  const leftUpdated = Date.parse(String(left.updated_at || left.created_at || "").trim());
@@ -660,13 +671,6 @@ function normalizeProjectContextImportance(value) {
660
671
  return "normal";
661
672
  }
662
673
 
663
- function normalizeProjectContextComparableText(value) {
664
- return String(value || "")
665
- .trim()
666
- .toLowerCase()
667
- .replace(/\s+/g, " ");
668
- }
669
-
670
674
  function looksLikeInterrogativeMessage(text) {
671
675
  const normalizedText = String(text || "").trim();
672
676
  if (!normalizedText) {
@@ -770,13 +774,9 @@ async function maybeCreateSuggestedProjectContext({
770
774
  actorUserID,
771
775
  }))
772
776
  : [];
773
- const duplicate = existingItems
774
- .map((item) => normalizeProjectContextItem(item))
775
- .find((item) => (
776
- ["active", "suggested"].includes(String(item.status || "").trim().toLowerCase())
777
- && normalizeProjectContextComparableText(item.title) === normalizeProjectContextComparableText(candidate.title)
778
- && normalizeProjectContextComparableText(item.body) === normalizeProjectContextComparableText(candidate.body)
779
- ));
777
+ const duplicate = findExactProjectContextDuplicate(existingItems, candidate, {
778
+ statuses: ["active", "suggested"],
779
+ });
780
780
  if (duplicate) {
781
781
  return {
782
782
  skipped: true,
@@ -817,7 +817,7 @@ async function maybeCreateSuggestedProjectContext({
817
817
  }
818
818
  }
819
819
 
820
- async function loadActiveProjectContextItems({ normalizedRoute, runtime, deps }) {
820
+ async function loadProjectContextItemsForPrompt({ normalizedRoute, runtime, deps }) {
821
821
  const listProjectContextItems = typeof deps?.listProjectContextItems === "function"
822
822
  ? deps.listProjectContextItems
823
823
  : null;
@@ -839,7 +839,7 @@ async function loadActiveProjectContextItems({ normalizedRoute, runtime, deps })
839
839
  actorUserID: safeObject(runtime.actor).user_id,
840
840
  }))
841
841
  .map((item) => normalizeProjectContextItem(item))
842
- .filter((item) => item.id && item.status.toLowerCase() === "active")
842
+ .filter((item) => item.id && ["active", "suggested"].includes(item.status.toLowerCase()))
843
843
  .sort(compareProjectContextItems)
844
844
  .slice(0, 8);
845
845
  } catch {
@@ -3308,7 +3308,7 @@ export async function processRunnerSelectedRecord({
3308
3308
  reduceContextWindowForHumanIntent,
3309
3309
  buildRunnerContextWindow,
3310
3310
  buildHumanIntentContextWindowOptions,
3311
- loadActiveProjectContextItems,
3311
+ loadProjectContextItemsForPrompt,
3312
3312
  isInformationalHumanIntentType,
3313
3313
  resolveInformationalQueryExecutionPlan,
3314
3314
  buildRunnerInputPayload,
@@ -89,6 +89,7 @@ import {
89
89
  resolveCodexRawTextProcessResult,
90
90
  } from "./local-ai-adapters.mjs";
91
91
  import {
92
+ buildTelegramMessageEnvelopeFromParsedArchive,
92
93
  buildRunnerRouteDuplicateStateFromComment,
93
94
  buildRunnerRouteStateFromComment,
94
95
  } from "./runner-helpers.mjs";
@@ -115,6 +116,9 @@ import {
115
116
  import {
116
117
  resolveRunnerAuthoritativeTriggerDecision,
117
118
  } from "./runner-trigger.mjs";
119
+ import {
120
+ findExactProjectContextDuplicate,
121
+ } from "./project-context-dedupe.mjs";
118
122
 
119
123
  function requireDependency(deps, key) {
120
124
  const candidate = deps?.[key];
@@ -7477,6 +7481,175 @@ export async function runSelftestRunnerScenarios(push, deps) {
7477
7481
  push("single_bot_direct_reply_target_prefixes_visible_target_when_ai_omits_mention", false, String(err?.message || err));
7478
7482
  }
7479
7483
 
7484
+ try {
7485
+ let aiCalls = 0;
7486
+ let plannerCalls = 0;
7487
+ let deliveryCalls = 0;
7488
+ let deliveredText = "";
7489
+ const processed = await processRunnerSelectedRecord({
7490
+ routeKey: "single-bot-human-direct-reply-with-human-reply-target-key",
7491
+ normalizedRoute: normalizeRunnerRoute({
7492
+ name: "telegram-monitor-single-bot-human-direct-reply-with-human-reply-target",
7493
+ project_id: selftestProjectID,
7494
+ provider: "telegram",
7495
+ role: "monitor",
7496
+ role_profile: "monitor",
7497
+ trigger_policy: {
7498
+ mentions_only: true,
7499
+ direct_messages: true,
7500
+ reply_to_bot_messages: true,
7501
+ },
7502
+ archive_policy: {
7503
+ mirror_replies: true,
7504
+ dedupe_inbound: true,
7505
+ dedupe_outbound: true,
7506
+ skip_bot_messages: true,
7507
+ },
7508
+ dry_run_delivery: true,
7509
+ }),
7510
+ selectedRecord: {
7511
+ id: "comment-single-bot-human-direct-reply-with-human-reply-target",
7512
+ createdAt: "2026-04-02T05:48:25.000Z",
7513
+ parsedArchive: {
7514
+ kind: "telegram_message",
7515
+ chatID: "-100123",
7516
+ chatType: "supergroup",
7517
+ senderIsBot: false,
7518
+ sender: "Human",
7519
+ username: "sopia19910",
7520
+ body: "@RyoAI2_bot 너 대답 안하지?",
7521
+ mentionUsernames: ["RyoAI2_bot"],
7522
+ messageID: 376,
7523
+ replyToMessageID: 375,
7524
+ replyToUsername: "sopia19910",
7525
+ replyToSenderIsBot: false,
7526
+ },
7527
+ },
7528
+ pendingOrdered: [],
7529
+ bot: {
7530
+ id: "bot-peer-1",
7531
+ name: "RyoAI2_bot",
7532
+ username: "RyoAI2_bot",
7533
+ role: "monitor",
7534
+ provider: "telegram",
7535
+ },
7536
+ destination: {
7537
+ id: "dest-1",
7538
+ label: "Main Room",
7539
+ provider: "telegram",
7540
+ chatID: "-100123",
7541
+ },
7542
+ archiveThread: {
7543
+ threadID: "thread-1",
7544
+ workItemID: "work-item-1",
7545
+ },
7546
+ executionPlan: {
7547
+ mode: "role_profile",
7548
+ roleProfileName: "monitor",
7549
+ roleProfile: {
7550
+ client: "sample",
7551
+ model: "",
7552
+ permissionMode: "read_only",
7553
+ reasoningEffort: "low",
7554
+ },
7555
+ workspaceDir: path.join(os.tmpdir(), "metheus-runner-selftest-single-bot-human-direct-reply-with-human-reply-target"),
7556
+ workspaceSource: "selftest",
7557
+ usedCommandFallback: false,
7558
+ },
7559
+ runtime: {
7560
+ baseURL: "https://example.test",
7561
+ token: "selftest-token",
7562
+ timeoutSeconds: 30,
7563
+ actor: { user_id: "user-1" },
7564
+ },
7565
+ deps: {
7566
+ saveRunnerRouteState: () => {},
7567
+ startRunnerTypingHeartbeat: () => ({ async stop() {} }),
7568
+ runRunnerAIExecution: async () => {
7569
+ aiCalls += 1;
7570
+ return {
7571
+ skip: false,
7572
+ reply: "대답합니다.",
7573
+ replyToMessageID: 0,
7574
+ };
7575
+ },
7576
+ performLocalBotDelivery: async ({ text }) => {
7577
+ deliveryCalls += 1;
7578
+ deliveredText = String(text || "").trim();
7579
+ return {
7580
+ delivery: { dryRun: true, body: {} },
7581
+ archive: {},
7582
+ };
7583
+ },
7584
+ serializeRunnerTriggerPolicy: (value) => value,
7585
+ serializeRunnerArchivePolicy: (value) => value,
7586
+ buildRunnerExecutionDeps: () => ({
7587
+ analyzeHumanConversationIntentWithAI: async () => ({
7588
+ mode: "single_bot",
7589
+ lead_bot: "ryoai2_bot",
7590
+ participants: ["ryoai2_bot"],
7591
+ initial_responders: ["ryoai2_bot"],
7592
+ allowed_responders: ["ryoai2_bot"],
7593
+ summary_bot: "",
7594
+ allow_bot_to_bot: false,
7595
+ reply_expectation: "informational",
7596
+ intent_type: "small_talk",
7597
+ }),
7598
+ planRoleExecutionWithAI: async () => {
7599
+ plannerCalls += 1;
7600
+ return {
7601
+ requiresExecution: true,
7602
+ summaryRole: "worker",
7603
+ steps: [{ role: "worker", goal: "unexpected", artifactsRequired: true }],
7604
+ };
7605
+ },
7606
+ }),
7607
+ buildRunnerDeliveryDeps: () => ({}),
7608
+ buildRunnerRuntimeDeps: () => ({}),
7609
+ resolveConversationPeerBots: () => [
7610
+ { id: "bot-peer-1", name: "RyoAI2_bot" },
7611
+ { id: "bot-peer-2", name: "RyoAI3_bot" },
7612
+ ],
7613
+ },
7614
+ });
7615
+ push(
7616
+ "single_bot_human_direct_reply_does_not_require_visible_human_target",
7617
+ processed.kind === "replied"
7618
+ && aiCalls === 1
7619
+ && plannerCalls === 0
7620
+ && deliveryCalls === 1
7621
+ && String(processed.result?.response_contract_validation_status || "") !== "reply_target_not_visible_in_reply"
7622
+ && deliveredText === "대답합니다.",
7623
+ `kind=${String(processed.kind || "(none)")} ai_calls=${aiCalls} planner_calls=${plannerCalls} delivery_calls=${deliveryCalls} validation=${String(processed.result?.response_contract_validation_status || "(none)")} delivered=${String(deliveredText || "(none)")}`,
7624
+ );
7625
+ } catch (err) {
7626
+ push("single_bot_human_direct_reply_does_not_require_visible_human_target", false, String(err?.message || err));
7627
+ }
7628
+
7629
+ try {
7630
+ const envelope = buildTelegramMessageEnvelopeFromParsedArchive({
7631
+ kind: "telegram_message",
7632
+ chatID: "-100123",
7633
+ messageID: 377,
7634
+ sender: "",
7635
+ username: "",
7636
+ senderIsBot: false,
7637
+ replyToSender: "Human Target",
7638
+ replyToUsername: "sopia19910",
7639
+ replyToSenderIsBot: false,
7640
+ body: "본문",
7641
+ occurredAt: "2026-04-02T05:48:35.000Z",
7642
+ });
7643
+ push(
7644
+ "telegram_message_envelope_does_not_promote_human_reply_target_to_sender_identity",
7645
+ String(envelope.sender_username || "") === ""
7646
+ && String(envelope.sender || "") === "",
7647
+ `sender=${String(envelope.sender || "(none)")} sender_username=${String(envelope.sender_username || "(none)")}`,
7648
+ );
7649
+ } catch (err) {
7650
+ push("telegram_message_envelope_does_not_promote_human_reply_target_to_sender_identity", false, String(err?.message || err));
7651
+ }
7652
+
7480
7653
  try {
7481
7654
  let aiCalls = 0;
7482
7655
  let deliveryCalls = 0;
@@ -12789,17 +12962,26 @@ export async function runSelftestRunnerScenarios(push, deps) {
12789
12962
  status: "archived",
12790
12963
  updated_at: "2026-03-17T00:00:00.000Z",
12791
12964
  },
12792
- {
12793
- id: "ctx-3",
12794
- title: "Workspace Binding",
12795
- body: "Workspace is C:/LUSH KOREA CLI.",
12796
- category: "workspace",
12797
- importance: "high",
12798
- status: "active",
12799
- updated_at: "2026-03-18T00:08:00.000Z",
12800
- },
12801
- ]),
12802
- }),
12965
+ {
12966
+ id: "ctx-3",
12967
+ title: "Workspace Binding",
12968
+ body: "Workspace is C:/LUSH KOREA CLI.",
12969
+ category: "workspace",
12970
+ importance: "high",
12971
+ status: "active",
12972
+ updated_at: "2026-03-18T00:08:00.000Z",
12973
+ },
12974
+ {
12975
+ id: "ctx-4",
12976
+ title: "Pending Risk",
12977
+ body: "BOT3 capacity failures should be tracked before closing a delegation turn.",
12978
+ category: "risk",
12979
+ importance: "normal",
12980
+ status: "suggested",
12981
+ updated_at: "2026-03-18T00:07:00.000Z",
12982
+ },
12983
+ ]),
12984
+ }),
12803
12985
  buildRunnerDeliveryDeps: () => ({}),
12804
12986
  buildRunnerRuntimeDeps: () => ({}),
12805
12987
  },
@@ -12807,26 +12989,63 @@ export async function runSelftestRunnerScenarios(push, deps) {
12807
12989
  const prompt = buildLocalBotPrompt(capturedInputPayload, { terse: true });
12808
12990
  const projectContexts = ensureArray(capturedInputPayload?.project_context_items);
12809
12991
  push(
12810
- "active_project_context_is_injected_into_runner_prompt",
12811
- processed.kind === "replied"
12812
- && aiCalls === 1
12813
- && projectContexts.length === 2
12814
- && String(projectContexts[0]?.title || "") === "Operating Rule"
12815
- && String(projectContexts[1]?.title || "") === "Workspace Binding"
12816
- && prompt.includes("Active project context")
12817
- && prompt.includes("Operating Rule")
12818
- && prompt.includes("Workspace Binding")
12819
- && !prompt.includes("Archived Note"),
12820
- `kind=${String(processed.kind || "(none)")} ai_calls=${aiCalls} contexts=${projectContexts.map((item) => item.title).join(",") || "(none)"} prompt_has_active_context=${String(prompt.includes("Active project context"))}`,
12821
- );
12822
- } catch (err) {
12823
- push("active_project_context_is_injected_into_runner_prompt", false, String(err?.message || err));
12824
- }
12825
-
12826
- try {
12827
- const prompt = buildLocalBotPrompt({
12828
- bot: {
12829
- name: "RyoAI_bot",
12992
+ "existing_project_context_is_injected_into_runner_prompt",
12993
+ processed.kind === "replied"
12994
+ && aiCalls === 1
12995
+ && projectContexts.length === 3
12996
+ && String(projectContexts[0]?.title || "") === "Operating Rule"
12997
+ && String(projectContexts[1]?.title || "") === "Workspace Binding"
12998
+ && String(projectContexts[2]?.title || "") === "Pending Risk"
12999
+ && prompt.includes("Existing project context memory")
13000
+ && prompt.includes("Operating Rule")
13001
+ && prompt.includes("Workspace Binding")
13002
+ && prompt.includes("Pending Risk")
13003
+ && !prompt.includes("Archived Note"),
13004
+ `kind=${String(processed.kind || "(none)")} ai_calls=${aiCalls} contexts=${projectContexts.map((item) => `${item.status}:${item.title}`).join(",") || "(none)"} prompt_has_context_memory=${String(prompt.includes("Existing project context memory"))}`,
13005
+ );
13006
+ } catch (err) {
13007
+ push("existing_project_context_is_injected_into_runner_prompt", false, String(err?.message || err));
13008
+ }
13009
+
13010
+ try {
13011
+ const duplicate = findExactProjectContextDuplicate([
13012
+ {
13013
+ id: "ctx-active-1",
13014
+ title: "Operating Rule",
13015
+ body: "Always answer with current project settings first.",
13016
+ status: "active",
13017
+ },
13018
+ {
13019
+ id: "ctx-suggested-1",
13020
+ title: " Pending Risk ",
13021
+ body: "BOT3 capacity failures should be tracked before closing a delegation turn.",
13022
+ status: "suggested",
13023
+ },
13024
+ {
13025
+ id: "ctx-archived-1",
13026
+ title: "Old Note",
13027
+ body: "old note",
13028
+ status: "archived",
13029
+ },
13030
+ ], {
13031
+ title: "pending risk",
13032
+ body: "BOT3 capacity failures should be tracked before closing a delegation turn.",
13033
+ }, {
13034
+ statuses: ["active", "suggested"],
13035
+ });
13036
+ push(
13037
+ "project_context_exact_duplicate_helper_matches_active_and_suggested_only",
13038
+ String(duplicate?.id || "") === "ctx-suggested-1",
13039
+ `duplicate_id=${String(duplicate?.id || "(none)")} duplicate_status=${String(duplicate?.status || "(none)")}`,
13040
+ );
13041
+ } catch (err) {
13042
+ push("project_context_exact_duplicate_helper_matches_active_and_suggested_only", false, String(err?.message || err));
13043
+ }
13044
+
13045
+ try {
13046
+ const prompt = buildLocalBotPrompt({
13047
+ bot: {
13048
+ name: "RyoAI_bot",
12830
13049
  username: "RyoAI_bot",
12831
13050
  role: "monitor",
12832
13051
  },
@@ -13096,14 +13315,148 @@ export async function runSelftestRunnerScenarios(push, deps) {
13096
13315
  && String(safeObject(lastSavedState).last_context_suggestion_id || "") === "ctx-suggest-1",
13097
13316
  `kind=${String(processed.kind || "(none)")} create_calls=${createCalls.length} status=${String(processed?.result?.context_suggestion_status || "(none)")} state_id=${String(safeObject(lastSavedState).last_context_suggestion_id || "(none)")}`,
13098
13317
  );
13099
- } catch (err) {
13100
- push("human_role_rule_creates_suggested_project_context", false, String(err?.message || err));
13101
- }
13102
-
13103
- try {
13104
- const createCalls = [];
13105
- const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-selftest-context-suggest-question-"));
13106
- const processed = await processRunnerSelectedRecord({
13318
+ } catch (err) {
13319
+ push("human_role_rule_creates_suggested_project_context", false, String(err?.message || err));
13320
+ }
13321
+
13322
+ try {
13323
+ const createCalls = [];
13324
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-selftest-context-suggest-duplicate-"));
13325
+ const processed = await processRunnerSelectedRecord({
13326
+ routeKey: "context-suggest-duplicate-key",
13327
+ normalizedRoute: normalizeRunnerRoute({
13328
+ name: "telegram-monitor-context-suggest-duplicate",
13329
+ project_id: selftestProjectID,
13330
+ provider: "telegram",
13331
+ role: "monitor",
13332
+ role_profile: "monitor",
13333
+ destination_id: "dest-1",
13334
+ destination_label: "Main Room",
13335
+ server_bot_name: "RyoAI_bot",
13336
+ server_bot_id: "bot-1",
13337
+ trigger_policy: {
13338
+ mentions_only: true,
13339
+ direct_messages: true,
13340
+ reply_to_bot_messages: true,
13341
+ },
13342
+ archive_policy: {
13343
+ mirror_replies: true,
13344
+ dedupe_inbound: true,
13345
+ dedupe_outbound: true,
13346
+ skip_bot_messages: true,
13347
+ },
13348
+ dry_run_delivery: false,
13349
+ }),
13350
+ selectedRecord: {
13351
+ id: "comment-context-suggest-duplicate",
13352
+ createdAt: "2026-03-19T01:04:00.000Z",
13353
+ parsedArchive: {
13354
+ kind: "telegram_message",
13355
+ chatID: "-100123",
13356
+ chatType: "supergroup",
13357
+ body: "@RyoAI_bot Room rule: RyoAI_bot handles coordination and RyoAI2_bot handles review in this room.",
13358
+ messageID: 301,
13359
+ sender: "human",
13360
+ senderIsBot: false,
13361
+ mentionUsernames: ["ryoai_bot", "ryoai2_bot"],
13362
+ },
13363
+ },
13364
+ pendingOrdered: [],
13365
+ bot: {
13366
+ id: "bot-1",
13367
+ name: "RyoAI_bot",
13368
+ username: "RyoAI_bot",
13369
+ role: "monitor",
13370
+ provider: "telegram",
13371
+ },
13372
+ destination: {
13373
+ id: "dest-1",
13374
+ label: "Main Room",
13375
+ provider: "telegram",
13376
+ chatID: "-100123",
13377
+ },
13378
+ archiveThread: {
13379
+ threadID: "thread-1",
13380
+ workItemID: "work-item-1",
13381
+ },
13382
+ executionPlan: {
13383
+ mode: "role_profile",
13384
+ roleProfileName: "monitor",
13385
+ roleProfile: {
13386
+ client: "sample",
13387
+ model: "",
13388
+ permissionMode: "read_only",
13389
+ reasoningEffort: "low",
13390
+ },
13391
+ workspaceDir,
13392
+ workspaceSource: "selftest",
13393
+ usedCommandFallback: false,
13394
+ },
13395
+ runtime: {
13396
+ baseURL: "https://example.test",
13397
+ token: "selftest-token",
13398
+ timeoutSeconds: 30,
13399
+ actor: { user_id: "user-1" },
13400
+ },
13401
+ deps: {
13402
+ saveRunnerRouteState: () => {},
13403
+ startRunnerTypingHeartbeat: () => ({ async stop() {} }),
13404
+ runRunnerAIExecution: async () => ({
13405
+ skip: false,
13406
+ reply: "역할 규칙으로 기억하겠습니다.",
13407
+ replyToMessageID: 301,
13408
+ raw: {
13409
+ context_suggestion: {
13410
+ should_store: true,
13411
+ title: "Bot collaboration roles",
13412
+ body: "RyoAI_bot handles coordination and RyoAI2_bot handles review in this room.",
13413
+ category: "bot_role",
13414
+ importance: "high",
13415
+ },
13416
+ },
13417
+ }),
13418
+ performLocalBotDelivery: async () => ({
13419
+ delivery: { dryRun: false, body: { result: { message_id: 9003 } } },
13420
+ archive: { ok: true },
13421
+ }),
13422
+ serializeRunnerTriggerPolicy: (value) => value,
13423
+ serializeRunnerArchivePolicy: (value) => value,
13424
+ buildRunnerExecutionDeps: () => ({
13425
+ listProjectContextItems: async () => ([
13426
+ {
13427
+ id: "ctx-suggest-existing",
13428
+ project_id: selftestProjectID,
13429
+ title: "Bot collaboration roles",
13430
+ body: "RyoAI_bot handles coordination and RyoAI2_bot handles review in this room.",
13431
+ category: "bot_role",
13432
+ importance: "high",
13433
+ status: "suggested",
13434
+ },
13435
+ ]),
13436
+ createProjectContextItem: async (input) => {
13437
+ createCalls.push(input);
13438
+ return { id: "ctx-should-not-create" };
13439
+ },
13440
+ }),
13441
+ buildRunnerDeliveryDeps: () => ({}),
13442
+ buildRunnerRuntimeDeps: () => ({}),
13443
+ },
13444
+ });
13445
+ push(
13446
+ "duplicate_suggested_project_context_is_not_created",
13447
+ processed.kind === "replied"
13448
+ && createCalls.length === 0
13449
+ && String(processed?.result?.context_suggestion_status || "") === "skipped:duplicate_suggested",
13450
+ `kind=${String(processed.kind || "(none)")} create_calls=${createCalls.length} status=${String(processed?.result?.context_suggestion_status || "(none)")}`,
13451
+ );
13452
+ } catch (err) {
13453
+ push("duplicate_suggested_project_context_is_not_created", false, String(err?.message || err));
13454
+ }
13455
+
13456
+ try {
13457
+ const createCalls = [];
13458
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-selftest-context-suggest-question-"));
13459
+ const processed = await processRunnerSelectedRecord({
13107
13460
  routeKey: "context-suggest-question-key",
13108
13461
  normalizedRoute: normalizeRunnerRoute({
13109
13462
  name: "telegram-monitor-context-suggest-question",
@@ -19962,7 +20315,7 @@ export async function runSelftestRunnerScenarios(push, deps) {
19962
20315
  reduceContextWindowForHumanIntent: (items) => items,
19963
20316
  buildRunnerContextWindow: () => [{ id: "ctx-1" }],
19964
20317
  buildHumanIntentContextWindowOptions: () => ({}),
19965
- loadActiveProjectContextItems: async () => [{ id: "pc-1" }],
20318
+ loadProjectContextItemsForPrompt: async () => [{ id: "pc-1" }],
19966
20319
  isInformationalHumanIntentType: (value) => String(value || "").trim().toLowerCase() === "status_query",
19967
20320
  resolveInformationalQueryExecutionPlan: (plan) => ({ ...plan, mode: "lookup_only" }),
19968
20321
  buildRunnerInputPayload: (payload) => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.284",
3
+ "version": "0.2.285",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [