negotium 0.2.16 → 0.2.18

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.
Files changed (47) hide show
  1. package/dist/agent-helpers.js +83 -38
  2. package/dist/agent-helpers.js.map +13 -13
  3. package/dist/background-bash.js.map +1 -1
  4. package/dist/browser-runtime.js.map +1 -1
  5. package/dist/{chunk-5cpdbbeh.js → chunk-fc58cqry.js} +3 -1
  6. package/dist/{chunk-5cpdbbeh.js.map → chunk-fc58cqry.js.map} +4 -4
  7. package/dist/hosted-agent.js +5 -2
  8. package/dist/hosted-agent.js.map +6 -6
  9. package/dist/main.js +1228 -486
  10. package/dist/main.js.map +23 -22
  11. package/dist/mcp-factories.js +701 -97
  12. package/dist/mcp-factories.js.map +14 -14
  13. package/dist/prompts.js +8 -8
  14. package/dist/prompts.js.map +4 -4
  15. package/dist/query-runtime.js.map +1 -1
  16. package/dist/registry.js +3 -3
  17. package/dist/registry.js.map +2 -2
  18. package/dist/rollout.js +1 -1
  19. package/dist/runtime/src/agents/compaction-support.ts +1 -1
  20. package/dist/runtime/src/agents/idle-archiver.ts +3 -2
  21. package/dist/runtime/src/agents/maestro-provider.ts +3 -0
  22. package/dist/runtime/src/index.ts +1 -0
  23. package/dist/runtime/src/mcp/canonical-proxy-server.ts +18 -1
  24. package/dist/runtime/src/mcp/wiki-server.ts +801 -59
  25. package/dist/runtime/src/platform/mcp-config.ts +1 -0
  26. package/dist/runtime/src/prompts/agents/wiki-archiver.md +16 -6
  27. package/dist/runtime/src/prompts/builders.ts +17 -34
  28. package/dist/runtime/src/runtime/turn-event-stream.ts +26 -0
  29. package/dist/runtime/src/runtime/turn-runner.ts +34 -26
  30. package/dist/runtime/src/storage/api-topics.ts +13 -4
  31. package/dist/runtime/src/topics/lifecycle.ts +3 -2
  32. package/dist/runtime/src/types/api.ts +6 -0
  33. package/dist/runtime/src/types.ts +6 -0
  34. package/dist/runtime/src/version.ts +1 -1
  35. package/dist/runtime-helpers.js.map +1 -1
  36. package/dist/storage.js +12 -6
  37. package/dist/storage.js.map +4 -4
  38. package/dist/types/packages/core/src/agents/compaction-support.d.ts +1 -0
  39. package/dist/types/packages/core/src/mcp/wiki-server.d.ts +4 -0
  40. package/dist/types/packages/core/src/prompts/builders.d.ts +2 -4
  41. package/dist/types/packages/core/src/runtime/turn-runner.d.ts +4 -0
  42. package/dist/types/packages/core/src/storage/api-topics.d.ts +1 -0
  43. package/dist/types/packages/core/src/types/api.d.ts +6 -0
  44. package/dist/types/packages/core/src/types.d.ts +6 -0
  45. package/dist/types/packages/core/src/version.d.ts +1 -1
  46. package/dist/vault.js.map +1 -1
  47. package/package.json +2 -2
package/dist/main.js CHANGED
@@ -1010,6 +1010,70 @@ var init_codex_tree_kill = __esm(() => {
1010
1010
  killOwnedCodexTreesForShutdown = manager.killOwnedTreesForShutdown;
1011
1011
  });
1012
1012
 
1013
+ // ../../packages/core/src/agents/compaction-support.ts
1014
+ function isCjkCodePoint(code) {
1015
+ return code >= 12288 && code <= 19903 || code >= 19968 && code <= 40959 || code >= 4352 && code <= 4607 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65280 && code <= 65519 || code >= 131072 && code <= 262143;
1016
+ }
1017
+ function estimateTextTokens(text) {
1018
+ let cjkChars = 0;
1019
+ let cjkUnits = 0;
1020
+ for (let index = 0;index < text.length; index++) {
1021
+ const code = text.codePointAt(index) ?? 0;
1022
+ const wide = code > 65535;
1023
+ if (wide)
1024
+ index++;
1025
+ if (isCjkCodePoint(code)) {
1026
+ cjkChars++;
1027
+ cjkUnits += wide ? 2 : 1;
1028
+ }
1029
+ }
1030
+ const latinUnits = text.length - cjkUnits;
1031
+ return Math.ceil(cjkChars / CHARS_PER_CJK_TOKEN + latinUnits / CHARS_PER_TEXT_TOKEN);
1032
+ }
1033
+ function estimateConversationTokens(messages) {
1034
+ return messages.reduce((sum, message) => sum + 4 + estimateTextTokens(message.content), 0);
1035
+ }
1036
+ var ACTIVE_TASK_TEMPLATE = `You are compressing a long agent conversation so the main agent can continue
1037
+ without losing context. Produce a single concise summary using EXACTLY these
1038
+ section headers, in this order:
1039
+
1040
+ ## Active Task
1041
+ One sentence: what is the agent currently working on?
1042
+
1043
+ ## Goal
1044
+ One or two sentences: the user's overall objective in this session.
1045
+
1046
+ ## Constraints
1047
+ Bulleted list of durable requirements, user preferences, technical limits, or
1048
+ process rules that should continue to govern the work. Skip if none.
1049
+
1050
+ ## Key Decisions
1051
+ Bulleted list of decisions already made that should not be reopened unless the
1052
+ user asks. Include the rationale when it is short and important. Skip if none.
1053
+
1054
+ ## Pending
1055
+ Bulleted list of unresolved items, decisions to make, or work explicitly
1056
+ deferred. Use "(blocked: <reason>)" when applicable.
1057
+
1058
+ ## Next Steps
1059
+ Bulleted list of the concrete next actions the main agent should take after
1060
+ compaction, in likely execution order. Skip if none.
1061
+
1062
+ ## Files
1063
+ Bulleted list of \`absolute/paths\` touched or referenced (read, written,
1064
+ inspected). Skip if none.
1065
+
1066
+ ## Recent context
1067
+ 3-5 bullets capturing the most recent tool calls + their salient outputs.
1068
+ Prefer specifics (paths, line numbers, exit codes, key values) over generic
1069
+ recaps. Skip details that have no bearing on the next step.
1070
+
1071
+ RULES:
1072
+ - Output ONLY the eight sections above, with no preamble or postscript.
1073
+ - Do NOT echo the user's words verbatim - paraphrase tightly.
1074
+ - Do NOT invent file paths or facts not present in the transcript.
1075
+ - Keep the entire summary under 1500 words.`, CHARS_PER_TEXT_TOKEN = 3.5, CHARS_PER_CJK_TOKEN = 0.9;
1076
+
1013
1077
  // ../../packages/core/src/runtime/user-turn-envelope.ts
1014
1078
  function legacyUserTurnEnvelope(prompt, attachments) {
1015
1079
  return attachments?.length ? { prompt, attachments } : { prompt };
@@ -1572,7 +1636,7 @@ var exports_version = {};
1572
1636
  __export(exports_version, {
1573
1637
  NEGOTIUM_VERSION: () => NEGOTIUM_VERSION
1574
1638
  });
1575
- var NEGOTIUM_VERSION = "0.2.16";
1639
+ var NEGOTIUM_VERSION = "0.2.18";
1576
1640
 
1577
1641
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
1578
1642
  import { spawn } from "child_process";
@@ -2879,6 +2943,8 @@ var init_mcp_config = __esm(() => {
2879
2943
  return env ? buildStdioMcpServer(agent, CANONICAL_MCP_PROXY_SERVER, ["--surface=wiki"], env) : null;
2880
2944
  }
2881
2945
  const args = [`--user-id=${userId}`];
2946
+ if (topicId)
2947
+ args.push(`--current-topic-id=${topicId}`);
2882
2948
  const resolvedWikiTopicId = wikiTopicId ?? topicId ?? (session !== "dm" ? session : undefined);
2883
2949
  if (resolvedWikiTopicId)
2884
2950
  args.push(`--topic-id=${resolvedWikiTopicId}`);
@@ -6427,6 +6493,7 @@ function maestroProvider(opts) {
6427
6493
  ...opts,
6428
6494
  enableToolSearch: !opts.toolPolicy && opts.enableToolSearch !== false,
6429
6495
  toolResultTruncation: buildMaestroToolResultTruncation(opts),
6496
+ ephemeralSystemPrompt: opts.ephemeralSystemPrompt,
6430
6497
  apiKeyOverrides: resolveMaestroApiKeyOverrides(userId),
6431
6498
  agent: "maestro",
6432
6499
  disallowedTools: buildMaestroDisallowedTools(callerDisallowedTools, opts.toolPolicy),
@@ -7567,6 +7634,7 @@ function initializeApiTopicsSchema() {
7567
7634
  last_message_at TEXT,
7568
7635
  parent_topic_id TEXT,
7569
7636
  memory_topic_id TEXT,
7637
+ memory_key TEXT,
7570
7638
  is_fork INTEGER NOT NULL DEFAULT 0 CHECK (is_fork IN (0,1)),
7571
7639
  is_subagent INTEGER NOT NULL DEFAULT 0 CHECK (is_subagent IN (0,1)),
7572
7640
  visibility TEXT NOT NULL DEFAULT 'visible' CHECK (visibility IN ('visible','hidden')),
@@ -7616,8 +7684,8 @@ function initializeApiTopicsSchema() {
7616
7684
  const legacyBaseEffort = row.base_effort ?? row.default_effort;
7617
7685
  db.query(`INSERT INTO api_topics_next
7618
7686
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
7619
- created_at,last_message_at,parent_topic_id,memory_topic_id,is_fork,is_subagent,visibility,access_mode,session_id)
7620
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(String(row.id), String(row.title), normalized.kind, typeof row.description === "string" ? row.description : null, normalized.agent ?? null, typeof legacyBaseModel === "string" ? legacyBaseModel : null, typeof legacyBaseEffort === "string" ? legacyBaseEffort : null, normalized.aiMode, String(row.created_at), typeof row.last_message_at === "string" ? row.last_message_at : null, typeof row.parent_topic_id === "string" ? row.parent_topic_id : null, typeof row.memory_topic_id === "string" ? row.memory_topic_id : null, Number(row.is_fork ?? 0) !== 0 ? 1 : 0, Number(row.is_subagent ?? 0) !== 0 ? 1 : 0, row.visibility === "hidden" ? "hidden" : "visible", row.access_mode === "shared" ? "shared" : "private", typeof row.session_id === "string" ? row.session_id : null);
7687
+ created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,access_mode,session_id)
7688
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(String(row.id), String(row.title), normalized.kind, typeof row.description === "string" ? row.description : null, normalized.agent ?? null, typeof legacyBaseModel === "string" ? legacyBaseModel : null, typeof legacyBaseEffort === "string" ? legacyBaseEffort : null, normalized.aiMode, String(row.created_at), typeof row.last_message_at === "string" ? row.last_message_at : null, typeof row.parent_topic_id === "string" ? row.parent_topic_id : null, typeof row.memory_topic_id === "string" ? row.memory_topic_id : null, typeof row.memory_key === "string" ? row.memory_key : null, Number(row.is_fork ?? 0) !== 0 ? 1 : 0, Number(row.is_subagent ?? 0) !== 0 ? 1 : 0, row.visibility === "hidden" ? "hidden" : "visible", row.access_mode === "shared" ? "shared" : "private", typeof row.session_id === "string" ? row.session_id : null);
7621
7689
  }
7622
7690
  db.exec("DROP TABLE IF EXISTS topic_members");
7623
7691
  db.exec("DROP TABLE api_topics");
@@ -7686,6 +7754,9 @@ function initializeApiTopicsSchema() {
7686
7754
  if (!tableColumns2("api_topics").has("memory_topic_id")) {
7687
7755
  db.exec("ALTER TABLE api_topics ADD COLUMN memory_topic_id TEXT");
7688
7756
  }
7757
+ if (!tableColumns2("api_topics").has("memory_key")) {
7758
+ db.exec("ALTER TABLE api_topics ADD COLUMN memory_key TEXT");
7759
+ }
7689
7760
  db.exec(`
7690
7761
  UPDATE api_topics
7691
7762
  SET browser_profile_owner = (
@@ -7746,6 +7817,7 @@ function rowToDto2(r, participants = getTopicParticipants(r.id), tellTargets) {
7746
7817
  lastMessageAt: r.last_message_at ?? new Date().toISOString(),
7747
7818
  parentTopicId: r.parent_topic_id ?? undefined,
7748
7819
  memoryTopicId: r.memory_topic_id ?? undefined,
7820
+ memoryKey: r.memory_key ?? undefined,
7749
7821
  isFork: r.is_fork !== 0,
7750
7822
  ...r.is_subagent !== 0 ? { isSubagent: true } : {},
7751
7823
  ...r.is_subagent !== 0 ? {
@@ -7840,9 +7912,9 @@ function upsertTopic(t) {
7840
7912
  db.transaction(() => {
7841
7913
  db.query(`INSERT INTO api_topics
7842
7914
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
7843
- created_at,last_message_at,parent_topic_id,memory_topic_id,is_fork,is_subagent,visibility,access_mode,
7915
+ created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,access_mode,
7844
7916
  subagent_report_mode)
7845
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
7917
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
7846
7918
  ON CONFLICT(id) DO UPDATE SET
7847
7919
  title = excluded.title,
7848
7920
  kind = excluded.kind,
@@ -7855,11 +7927,12 @@ function upsertTopic(t) {
7855
7927
  last_message_at = excluded.last_message_at,
7856
7928
  parent_topic_id = excluded.parent_topic_id,
7857
7929
  memory_topic_id = excluded.memory_topic_id,
7930
+ memory_key = excluded.memory_key,
7858
7931
  is_fork = excluded.is_fork,
7859
7932
  is_subagent = excluded.is_subagent,
7860
7933
  visibility = excluded.visibility,
7861
7934
  access_mode = excluded.access_mode,
7862
- subagent_report_mode = excluded.subagent_report_mode`).run(t.id, t.title, normalized.kind, t.description ?? null, normalized.agent ?? null, t.defaultModel ?? null, t.defaultEffort ?? null, normalized.aiMode, t.createdAt, t.lastMessageAt ?? null, t.parentTopicId ?? null, t.memoryTopicId ?? null, t.isFork ? 1 : 0, t.isSubagent ? 1 : 0, normalizeTopicVisibility(t.visibility), normalizeTopicAccessMode(t.accessMode), t.subagentReportMode ?? "auto");
7935
+ subagent_report_mode = excluded.subagent_report_mode`).run(t.id, t.title, normalized.kind, t.description ?? null, normalized.agent ?? null, t.defaultModel ?? null, t.defaultEffort ?? null, normalized.aiMode, t.createdAt, t.lastMessageAt ?? null, t.parentTopicId ?? null, t.memoryTopicId ?? null, t.memoryKey ?? null, t.isFork ? 1 : 0, t.isSubagent ? 1 : 0, normalizeTopicVisibility(t.visibility), normalizeTopicAccessMode(t.accessMode), t.subagentReportMode ?? "auto");
7863
7936
  db.query("DELETE FROM topic_members WHERE topic_id = ?").run(t.id);
7864
7937
  for (const participant of t.participants) {
7865
7938
  db.query("INSERT INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)").run(t.id, participant.userId, participant.role);
@@ -11321,70 +11394,6 @@ var init_personal_general = __esm(async () => {
11321
11394
  ]);
11322
11395
  });
11323
11396
 
11324
- // ../../packages/core/src/agents/compaction-support.ts
11325
- function isCjkCodePoint(code) {
11326
- return code >= 12288 && code <= 19903 || code >= 19968 && code <= 40959 || code >= 4352 && code <= 4607 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65280 && code <= 65519 || code >= 131072 && code <= 262143;
11327
- }
11328
- function estimateTextTokens(text) {
11329
- let cjkChars = 0;
11330
- let cjkUnits = 0;
11331
- for (let index = 0;index < text.length; index++) {
11332
- const code = text.codePointAt(index) ?? 0;
11333
- const wide = code > 65535;
11334
- if (wide)
11335
- index++;
11336
- if (isCjkCodePoint(code)) {
11337
- cjkChars++;
11338
- cjkUnits += wide ? 2 : 1;
11339
- }
11340
- }
11341
- const latinUnits = text.length - cjkUnits;
11342
- return Math.ceil(cjkChars / CHARS_PER_CJK_TOKEN + latinUnits / CHARS_PER_TEXT_TOKEN);
11343
- }
11344
- function estimateConversationTokens(messages) {
11345
- return messages.reduce((sum, message) => sum + 4 + estimateTextTokens(message.content), 0);
11346
- }
11347
- var ACTIVE_TASK_TEMPLATE = `You are compressing a long agent conversation so the main agent can continue
11348
- without losing context. Produce a single concise summary using EXACTLY these
11349
- section headers, in this order:
11350
-
11351
- ## Active Task
11352
- One sentence: what is the agent currently working on?
11353
-
11354
- ## Goal
11355
- One or two sentences: the user's overall objective in this session.
11356
-
11357
- ## Constraints
11358
- Bulleted list of durable requirements, user preferences, technical limits, or
11359
- process rules that should continue to govern the work. Skip if none.
11360
-
11361
- ## Key Decisions
11362
- Bulleted list of decisions already made that should not be reopened unless the
11363
- user asks. Include the rationale when it is short and important. Skip if none.
11364
-
11365
- ## Pending
11366
- Bulleted list of unresolved items, decisions to make, or work explicitly
11367
- deferred. Use "(blocked: <reason>)" when applicable.
11368
-
11369
- ## Next Steps
11370
- Bulleted list of the concrete next actions the main agent should take after
11371
- compaction, in likely execution order. Skip if none.
11372
-
11373
- ## Files
11374
- Bulleted list of \`absolute/paths\` touched or referenced (read, written,
11375
- inspected). Skip if none.
11376
-
11377
- ## Recent context
11378
- 3-5 bullets capturing the most recent tool calls + their salient outputs.
11379
- Prefer specifics (paths, line numbers, exit codes, key values) over generic
11380
- recaps. Skip details that have no bearing on the next step.
11381
-
11382
- RULES:
11383
- - Output ONLY the eight sections above, with no preamble or postscript.
11384
- - Do NOT echo the user's words verbatim - paraphrase tightly.
11385
- - Do NOT invent file paths or facts not present in the transcript.
11386
- - Keep the entire summary under 1500 words.`, CHARS_PER_TEXT_TOKEN = 3.5, CHARS_PER_CJK_TOKEN = 0.9;
11387
-
11388
11397
  // ../../packages/core/src/prompts/builders.ts
11389
11398
  import { readFileSync as readFileSync13 } from "fs";
11390
11399
  import { resolve as resolve13 } from "path";
@@ -11707,14 +11716,14 @@ function buildMemoryPromptSection(opts) {
11707
11716
  const parts = [`
11708
11717
 
11709
11718
  ## Memory`];
11710
- parts.push(`Memory directory: ${opts.wikiDir}/topic`);
11711
- parts.push(`Files: ${opts.hasFiles ? opts.briefFile : "(none)"}`);
11712
- if (opts.latestSummaryFile) {
11713
- parts.push(`Latest summary: ${opts.latestSummaryFile}`);
11714
- }
11715
- parts.push("", opts.isManager ? opts.hasFiles ? "Above is this workspace's memory-hub brief (accumulated across all archived topics). For past context or other topics, consult it first, then use the `wiki_query` MCP tool for deeper recall." : "If you need past context while handling delegated work, use the `wiki_query` MCP tool." : opts.hasFiles ? "The file above is this topic's wiki brief. Read it first for past context; use the `wiki_query` MCP tool for deeper recall." : opts.latestSummaryFile ? "There is no wiki brief file yet. For past context, read the Latest summary first; use the `wiki_query` MCP tool for deeper recall." : "There is no wiki brief file yet. Use the `wiki_query` MCP tool for past context.");
11716
- parts.push("", opts.isManager ? "When answering about past workspace decisions or other topics, weave in memory naturally, but confirm with `wiki_query` when unsure." : opts.hasFiles || opts.latestSummaryFile ? "On your first reply in the topic, read the wiki brief" + (opts.latestSummaryFile ? " and Latest summary" : "") + " and mention a one-line context recap so the user can quickly confirm it is on track." : "On your first reply in the topic, check prior conversation context if needed and mention it in one natural line.");
11717
- parts.push("", opts.hasFiles ? "If the user says your memory is wrong, find the file in the Memory directory with Read and fix it directly with Edit." : "If the user says your memory is wrong, check the relevant wiki brief and fix it directly when possible, or use `wiki_query` to find the entry and reconcile.");
11719
+ if (opts.isManager) {
11720
+ parts.push("Use `wiki_query` for past workspace decisions or cross-topic context, then `wiki_read` only for the relevant result.");
11721
+ } else if (opts.memoryKey) {
11722
+ parts.push(`This topic uses the canonical memory key \`${opts.memoryKey}\`. At the start of a new session, read it with \`wiki_read(kind: "topic", key: "${opts.memoryKey}")\`.`);
11723
+ } else {
11724
+ parts.push(`At the start of a new session, search topic memories with \`wiki_query(question: "${opts.topicTitle}", kind: "topic", limit: 5)\` and read the most relevant candidate with \`wiki_read\`.`, "If a candidate is clearly the same continuing topic/persona, read it with `adopt: true`; otherwise keep this topic's own name as its memory key. Do not merge weak or ambiguous matches.");
11725
+ }
11726
+ parts.push("Keep the lookup quiet. Mention prior context naturally in one short line only when it helps the user.", 'Use `wiki_query(kind: "article")` for reusable knowledge and `wiki_query(kind: "summary")` for historical session details.');
11718
11727
  if (opts.hasArchive) {
11719
11728
  parts.push("", "If you need the actual conversation from an earlier session, use the `wiki_last_conversation` MCP tool.");
11720
11729
  }
@@ -12801,11 +12810,12 @@ function archiveActiveTopicForMemory(topicId, userId, options) {
12801
12810
  }, "topic-memory-archiver: raw snapshot preserved below exchange threshold");
12802
12811
  return "below-threshold";
12803
12812
  }
12804
- const memoryTopic = getTopicMemoryOrigin(topicId) ?? topic;
12813
+ const memoryTopic = topic.memoryKey ? topic : getTopicMemoryOrigin(topicId) ?? topic;
12814
+ const memoryTitle = memoryTopic.memoryKey?.trim() || memoryTopic.title;
12805
12815
  const launched = (options.launchArchiver ?? runArchiverTurn)({
12806
12816
  userId,
12807
12817
  topicId: memoryTopic.id,
12808
- topicTitle: memoryTopic.title,
12818
+ topicTitle: memoryTitle,
12809
12819
  archivePath: job.archivePath,
12810
12820
  ...options.rawArchivePaths ? { rawArchivePaths: options.rawArchivePaths } : {},
12811
12821
  messageCount: job.messageCount,
@@ -16181,11 +16191,12 @@ async function deleteTopicCascadeImpl(topic, userId, options, deletingAncestorId
16181
16191
  }
16182
16192
  if (archived && meetsMemoryArchiveExchangeThreshold(archived.exchangeCount, MIN_MEMORY_ARCHIVE_EXCHANGES, topic)) {
16183
16193
  try {
16184
- const memoryTopic = getTopicMemoryOrigin(topicId) ?? topic;
16194
+ const memoryTopic = topic.memoryKey ? topic : getTopicMemoryOrigin(topicId) ?? topic;
16195
+ const memoryTitle = memoryTopic.memoryKey?.trim() || memoryTopic.title;
16185
16196
  runArchiverTurn({
16186
16197
  userId,
16187
16198
  ...memoryTopic.id !== topicId && !deletingTopicIds.has(memoryTopic.id) ? { topicId: memoryTopic.id } : {},
16188
- topicTitle: memoryTopic.title,
16199
+ topicTitle: memoryTitle,
16189
16200
  archivePath: archived.path,
16190
16201
  ...rawArchives.length > 0 ? { rawArchivePaths: rawArchives } : {},
16191
16202
  messageCount: archived.messageCount
@@ -17209,6 +17220,9 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17209
17220
  let pendingSawDelta = false;
17210
17221
  let accumulatedText = "";
17211
17222
  let pendingText = "";
17223
+ let assistantContextTokens = 0;
17224
+ let toolContextTokens = 0;
17225
+ let lastContextProgressAt = 0;
17212
17226
  let lastVisibleMessageId = null;
17213
17227
  const visibleMessageIds = [];
17214
17228
  let lastTaskPanelText = null;
@@ -17227,6 +17241,20 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17227
17241
  model
17228
17242
  });
17229
17243
  };
17244
+ const broadcastContextProgress = (force = false) => {
17245
+ if (silent)
17246
+ return;
17247
+ const now = Date.now();
17248
+ if (!force && lastContextProgressAt > 0 && now - lastContextProgressAt < 250)
17249
+ return;
17250
+ lastContextProgressAt = now;
17251
+ hub.broadcastAiStatus(topicId, {
17252
+ kind: "context_progress",
17253
+ queryId,
17254
+ assistantTokens: assistantContextTokens,
17255
+ toolTokens: toolContextTokens
17256
+ });
17257
+ };
17230
17258
  const nextSyntheticToolUseId = () => `tool-${queryId}-${++syntheticToolCounter}`;
17231
17259
  const bindToolUseId = (providerToolUseId, fallback) => {
17232
17260
  const providerId = normalizeToolUseId(providerToolUseId);
@@ -17336,6 +17364,8 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17336
17364
  const incoming = event.content;
17337
17365
  accumulatedText += incoming;
17338
17366
  pendingText += incoming;
17367
+ assistantContextTokens = estimateTextTokens(accumulatedText);
17368
+ broadcastContextProgress();
17339
17369
  break;
17340
17370
  }
17341
17371
  case "text":
@@ -17343,10 +17373,15 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17343
17373
  const incoming = event.content;
17344
17374
  accumulatedText += incoming;
17345
17375
  pendingText += incoming;
17376
+ assistantContextTokens = estimateTextTokens(accumulatedText);
17377
+ broadcastContextProgress();
17346
17378
  }
17347
17379
  break;
17348
17380
  case "tool_use":
17349
17381
  emitPendingAssistantMessage();
17382
+ toolContextTokens += estimateTextTokens(`${event.name}
17383
+ ${JSON.stringify(event.input ?? {})}`);
17384
+ broadcastContextProgress(true);
17350
17385
  if (isVisualsShowHtmlTool(event.name)) {
17351
17386
  const input = event.input;
17352
17387
  if (typeof input.html !== "string" || input.html.trim().length === 0) {
@@ -17500,6 +17535,8 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17500
17535
  }
17501
17536
  break;
17502
17537
  case "tool_result":
17538
+ toolContextTokens += estimateTextTokens(event.content);
17539
+ broadcastContextProgress(true);
17503
17540
  if (isVisualToolResultHandled(event.toolUseId))
17504
17541
  break;
17505
17542
  if (!silent) {
@@ -17900,6 +17937,7 @@ __export(exports_turn_runner, {
17900
17937
  channelTranscriptSpeaker: () => channelTranscriptSpeaker,
17901
17938
  canonicalModelId: () => canonicalModelId,
17902
17939
  buildVideoHtml: () => buildVideoHtml,
17940
+ buildTurnReminderQuery: () => buildTurnReminderQuery,
17903
17941
  buildMermaidHtml: () => buildMermaidHtml,
17904
17942
  buildMentionOnlyChannelPrompt: () => buildMentionOnlyChannelPrompt,
17905
17943
  buildImageHtml: () => buildImageHtml,
@@ -17925,6 +17963,18 @@ function withDefaultPlaywright(configuredMcp, isManager) {
17925
17963
  enabled.add("background-bash");
17926
17964
  return [...enabled];
17927
17965
  }
17966
+ function buildTurnReminderQuery(agent, prompt, reminders) {
17967
+ const reminderPrompt = reminders.join(`
17968
+
17969
+ `);
17970
+ if (!reminderPrompt)
17971
+ return { prompt };
17972
+ if (agent === "maestro")
17973
+ return { prompt, ephemeralSystemPrompt: reminderPrompt };
17974
+ return { prompt: `${prompt}
17975
+
17976
+ ${reminderPrompt}` };
17977
+ }
17928
17978
  function appendSystemMessage(topicId, text2) {
17929
17979
  const message = {
17930
17980
  id: randomUUID16(),
@@ -18740,25 +18790,24 @@ function startAiTurn(params) {
18740
18790
  const isManager = topicRecord?.kind === "manager";
18741
18791
  const isMentionOnlyChannel = topic.aiMode === "mention" && !isManager;
18742
18792
  let systemPrompt = isManager ? buildManagerSystemPrompt(systemPromptOpts) : isMentionOnlyChannel ? buildChannelSystemPrompt(systemPromptOpts) : buildTopicSystemPrompt(systemPromptOpts);
18743
- const memoryTopic = isMentionOnlyChannel || isManager ? topic : getTopicMemoryOrigin(topicId) ?? topic;
18793
+ const memoryTopic = isMentionOnlyChannel || isManager || topic.memoryKey ? topic : getTopicMemoryOrigin(topicId) ?? topic;
18794
+ const memoryTitle = memoryTopic.memoryKey?.trim() || memoryTopic.title;
18744
18795
  if (!isMentionOnlyChannel) {
18796
+ let hasMemoryArchive = false;
18745
18797
  try {
18746
- const resolvedBrief = resolveTopicBrief(memoryTopic.id, memoryTopic.title);
18798
+ const resolvedBrief = resolveTopicBrief(memoryTopic.id, memoryTitle);
18747
18799
  const wikiDir = getSharedWikiDir();
18748
- const { briefFile, hasBriefFile, latestSummaryFile } = resolveWikiMemoryMirror(wikiDir, memoryTopic.id, memoryTopic.title);
18749
- if (resolvedBrief || hasBriefFile || latestSummaryFile) {
18750
- systemPrompt += buildMemoryPromptSection({
18751
- briefFile,
18752
- wikiDir,
18753
- hasFiles: hasBriefFile,
18754
- latestSummaryFile,
18755
- hasArchive: Boolean(resolvedBrief?.brief.latestSummaryMd || latestSummaryFile),
18756
- isManager
18757
- });
18758
- }
18800
+ const { latestSummaryFile } = resolveWikiMemoryMirror(wikiDir, memoryTopic.id, memoryTitle);
18801
+ hasMemoryArchive = Boolean(resolvedBrief?.brief.latestSummaryMd || latestSummaryFile);
18759
18802
  } catch (err2) {
18760
18803
  logger.warn({ topicId, err: err2 }, "ai: failed to inject topic brief");
18761
18804
  }
18805
+ systemPrompt += buildMemoryPromptSection({
18806
+ topicTitle: topic.title,
18807
+ ...memoryTopic.memoryKey ? { memoryKey: memoryTopic.memoryKey } : {},
18808
+ hasArchive: hasMemoryArchive,
18809
+ isManager
18810
+ });
18762
18811
  }
18763
18812
  const activeVisual = visualTools ? getActiveVisualForPrompt(topicId, userId) : null;
18764
18813
  if (activeVisual) {
@@ -18872,15 +18921,11 @@ function startAiTurn(params) {
18872
18921
  if (consumePlaywrightUnavailable(userId, topic.title)) {
18873
18922
  turnReminders.push("<system-reminder>Playwright browser tools are UNAVAILABLE this turn. The `mcp__playwright__*` tools have been removed from this turn's catalog because the long-lived browser MCP could not be prepared. Do not attempt to call browser tools. If browser interaction is required, ask the user to retry shortly or use a non-browser alternative.</system-reminder>");
18874
18923
  }
18875
- const effectivePrompt = turnReminders.length > 0 ? `${agentPrompt}
18876
-
18877
- ${turnReminders.join(`
18878
-
18879
- `)}` : agentPrompt;
18924
+ const reminderQuery = buildTurnReminderQuery(agentKind, agentPrompt, turnReminders);
18880
18925
  try {
18881
18926
  yield* runAgent({
18882
18927
  agent: agentKind,
18883
- prompt: effectivePrompt,
18928
+ ...reminderQuery,
18884
18929
  attachments: promptAttachments,
18885
18930
  cwd: workspaceCwd,
18886
18931
  systemPrompt,
@@ -22461,6 +22506,7 @@ __export(exports_src, {
22461
22506
  extractFileEvents: () => extractFileEvents,
22462
22507
  executeVaultCommand: () => executeVaultCommand,
22463
22508
  executeExternalUserTurn: () => executeExternalUserTurn,
22509
+ estimateTextTokens: () => estimateTextTokens,
22464
22510
  errorResult: () => errorResult,
22465
22511
  errMsg: () => errMsg,
22466
22512
  ensurePersonalGeneral: () => ensurePersonalGeneral,
@@ -24894,7 +24940,7 @@ import {
24894
24940
  unlinkSync as unlinkSync18,
24895
24941
  writeFileSync as writeFileSync17
24896
24942
  } from "fs";
24897
- import { basename as basename7, dirname as dirname16, join as join36, resolve as resolve17 } from "path";
24943
+ import { basename as basename7, dirname as dirname16, join as join36, relative as relative2, resolve as resolve17 } from "path";
24898
24944
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
24899
24945
  import { StdioServerTransport as StdioServerTransport2 } from "@modelcontextprotocol/sdk/server/stdio.js";
24900
24946
  import {
@@ -25033,9 +25079,243 @@ function topicNameFrom(topicIdStr) {
25033
25079
  function slugify(topic) {
25034
25080
  return wikiSummarySlug(topic);
25035
25081
  }
25082
+ function normalizeSearchText(value) {
25083
+ return value.toLowerCase().replaceAll(/[^a-z0-9\uAC00-\uD7A3]+/g, " ").trim().replaceAll(/\s+/g, " ");
25084
+ }
25085
+ function compactSearchText(value) {
25086
+ return normalizeSearchText(value).replaceAll(" ", "");
25087
+ }
25088
+ function trigrams(value) {
25089
+ const compact = compactSearchText(value);
25090
+ if (compact.length < 3)
25091
+ return new Set(compact ? [compact] : []);
25092
+ const result = new Set;
25093
+ for (let i = 0;i <= compact.length - 3; i += 1)
25094
+ result.add(compact.slice(i, i + 3));
25095
+ return result;
25096
+ }
25097
+ function fuzzySearchScore(query2, candidate, maximum) {
25098
+ const compactQuery = compactSearchText(query2);
25099
+ const compactCandidate = compactSearchText(candidate);
25100
+ if (compactQuery.length < 3 || compactCandidate.length < 3)
25101
+ return 0;
25102
+ const similarity = trigramSimilarity(compactQuery, compactCandidate);
25103
+ const minimumSimilarity = compactQuery.length <= 4 ? 0.6 : 0.45;
25104
+ return similarity >= minimumSimilarity ? similarity * maximum : 0;
25105
+ }
25106
+ function meaningfulSearchTokens(value) {
25107
+ return normalizeSearchText(value).split(" ").filter((token) => (token.length >= 2 || /^\d+$/.test(token)) && !SEARCH_STOP_WORDS.has(token));
25108
+ }
25109
+ function canonicalSearchToken(token) {
25110
+ return token.length > 4 && token.endsWith("s") ? token.slice(0, -1) : token;
25111
+ }
25112
+ function canonicalSearchTokens(value) {
25113
+ return meaningfulSearchTokens(value).map(canonicalSearchToken);
25114
+ }
25115
+ function countToken(tokens, token) {
25116
+ let count = 0;
25117
+ for (const candidate of tokens)
25118
+ if (candidate === token)
25119
+ count += 1;
25120
+ return count;
25121
+ }
25122
+ function countPhrase(text2, phrase) {
25123
+ if (!phrase)
25124
+ return 0;
25125
+ let count = 0;
25126
+ let offset = 0;
25127
+ let match = text2.indexOf(phrase, offset);
25128
+ while (match >= 0) {
25129
+ count += 1;
25130
+ offset = match + phrase.length;
25131
+ match = text2.indexOf(phrase, offset);
25132
+ }
25133
+ return count;
25134
+ }
25135
+ function summaryTopicIdentity(key) {
25136
+ return normalizeSearchText(key.replace(/^\d{4}-\d{2}-\d{2}-/, ""));
25137
+ }
25138
+ function scoreSummaryTime(query2, key, title) {
25139
+ const date = key.match(/^(\d{4})-(\d{2})-(\d{2})/);
25140
+ if (!date)
25141
+ return 0;
25142
+ const tokens = meaningfulSearchTokens(query2);
25143
+ const documentDate = `${date[1]}-${date[2]}-${date[3]}`;
25144
+ const requestedDateMatch = query2.match(/\b(\d{4}) (\d{2}) (\d{2})\b/);
25145
+ const requestedDate = requestedDateMatch ? `${requestedDateMatch[1]}-${requestedDateMatch[2]}-${requestedDateMatch[3]}` : undefined;
25146
+ const beforeDate = /\bbefore\b/.test(query2);
25147
+ const afterDate = /\bafter\b/.test(query2);
25148
+ if (requestedDate && beforeDate && documentDate >= requestedDate)
25149
+ return null;
25150
+ if (requestedDate && afterDate && documentDate <= requestedDate)
25151
+ return null;
25152
+ const queryYear = tokens.find((token) => /^\d{4}(?:\uB144)?$/.test(token))?.replace(/\uB144$/, "");
25153
+ const queryMonth = tokens.map((token) => SEARCH_MONTHS.get(token)).find(Boolean);
25154
+ const hasRecencyTerm = tokens.some((token) => SEARCH_RECENCY_TERMS.has(token));
25155
+ const yearMatches = !queryYear || queryYear === date[1];
25156
+ const monthMatches = !queryMonth || queryMonth === date[2];
25157
+ if (!queryYear && !queryMonth && !hasRecencyTerm || !yearMatches || !monthMatches)
25158
+ return 0;
25159
+ const normalizedMetadata = `${normalizeSearchText(key)} ${normalizeSearchText(title)}`;
25160
+ const contentTokens = tokens.filter((token) => !/^\d{4}(?:\uB144)?$/.test(token) && !SEARCH_MONTHS.has(token) && !SEARCH_RECENCY_TERMS.has(token));
25161
+ const contentMatches = contentTokens.filter((token) => normalizedMetadata.includes(token)).length;
25162
+ const relationScore = requestedDate && (beforeDate || afterDate) ? 60 : 0;
25163
+ const exactDateScore = requestedDate === documentDate ? 90 : 0;
25164
+ return 30 + relationScore + exactDateScore + contentMatches * 15;
25165
+ }
25166
+ function trigramSimilarity(left, right) {
25167
+ const a = trigrams(left);
25168
+ const b = trigrams(right);
25169
+ if (a.size === 0 || b.size === 0)
25170
+ return 0;
25171
+ let overlap = 0;
25172
+ for (const gram of a)
25173
+ if (b.has(gram))
25174
+ overlap += 1;
25175
+ return 2 * overlap / (a.size + b.size);
25176
+ }
25177
+ function parseWikiIndex(path) {
25178
+ let text2;
25179
+ try {
25180
+ text2 = readFileSync22(path, "utf-8");
25181
+ } catch {
25182
+ return [];
25183
+ }
25184
+ const candidates = [];
25185
+ const entryPattern = /^\s*-\s*\[\[(topic|articles|summaries)\/([^\]]+)\]\]\s*(.*)$/;
25186
+ for (const line of text2.split(`
25187
+ `)) {
25188
+ const match = line.match(entryPattern);
25189
+ if (!match)
25190
+ continue;
25191
+ const namespace = match[1];
25192
+ const key = match[2].replace(/\.md$/i, "");
25193
+ const tail = match[3].trim();
25194
+ const dateMatch = tail.match(/\((\d{4}-\d{2}-\d{2})(?:[^)]*)\)\s*$/);
25195
+ const description = dateMatch ? tail.slice(0, dateMatch.index).trim() : tail;
25196
+ candidates.push({
25197
+ kind: namespace === "topic" ? "topic" : namespace === "articles" ? "article" : "summary",
25198
+ key,
25199
+ description,
25200
+ ...dateMatch ? { date: dateMatch[1] } : {}
25201
+ });
25202
+ }
25203
+ return candidates;
25204
+ }
25205
+ function indexCandidateExists(candidate) {
25206
+ if (candidate.kind === "topic")
25207
+ return true;
25208
+ const root = candidate.kind === "article" ? runtime().articlesDir : runtime().summariesDir;
25209
+ const filePath = resolve17(root, `${candidate.key}.md`);
25210
+ const relativePath = relative2(root, filePath);
25211
+ if (relativePath.startsWith("..") || relativePath.startsWith("/"))
25212
+ return false;
25213
+ try {
25214
+ return statSync15(filePath).isFile();
25215
+ } catch {
25216
+ return false;
25217
+ }
25218
+ }
25219
+ function scoreIndexKey(query2, key) {
25220
+ const normalizedQuery = normalizeSearchText(query2);
25221
+ const compactQuery = compactSearchText(normalizedQuery);
25222
+ const normalizedKey = normalizeSearchText(key);
25223
+ const compactKey = compactSearchText(normalizedKey);
25224
+ const tokens = meaningfulSearchTokens(normalizedQuery);
25225
+ let score = 0;
25226
+ if (normalizedKey === normalizedQuery)
25227
+ score += 120;
25228
+ else if (compactKey === compactQuery)
25229
+ score += 110;
25230
+ else {
25231
+ if (compactQuery.length >= 2 && (compactKey.startsWith(compactQuery) || compactQuery.startsWith(compactKey))) {
25232
+ score += 65;
25233
+ }
25234
+ if (tokens.length > 0 && compactQuery.length >= 3 && compactKey.includes(compactQuery)) {
25235
+ score += 45;
25236
+ }
25237
+ score += fuzzySearchScore(compactQuery, compactKey, 55);
25238
+ }
25239
+ for (const token of tokens) {
25240
+ if (normalizedKey.includes(token))
25241
+ score += 10;
25242
+ }
25243
+ return score;
25244
+ }
25245
+ function scoreIndexCandidate(query2, candidate) {
25246
+ const normalizedQuery = normalizeSearchText(query2);
25247
+ if (!normalizedQuery)
25248
+ return 0;
25249
+ const normalizedDescription = normalizeSearchText(candidate.description);
25250
+ const tokens = canonicalSearchTokens(normalizedQuery);
25251
+ const descriptionTokens = new Set(canonicalSearchTokens(normalizedDescription));
25252
+ let score = scoreIndexKey(normalizedQuery, candidate.key);
25253
+ if (tokens.length > 0 && normalizedDescription.includes(normalizedQuery))
25254
+ score += 30;
25255
+ let descriptionMatches = 0;
25256
+ for (const token of tokens) {
25257
+ if (descriptionTokens.has(token)) {
25258
+ score += 5;
25259
+ descriptionMatches += 1;
25260
+ }
25261
+ }
25262
+ if (tokens.length > 1 && descriptionMatches === tokens.length)
25263
+ score += 25;
25264
+ if (candidate.kind === "topic") {
25265
+ const requestsCurrent = /\b(?:current|active|ongoing|open)\b|\uD604\uC7AC|\uC9C4\uD589 \uC911/.test(normalizedQuery);
25266
+ const archivedCandidate = /\b(?:archive|archived|historical|closed)\b|\uBCF4\uAD00|\uC885\uB8CC/.test(`${normalizeSearchText(candidate.key)} ${normalizedDescription}`);
25267
+ if (requestsCurrent && archivedCandidate)
25268
+ score -= 40;
25269
+ }
25270
+ return score;
25271
+ }
25036
25272
  function wikiQuery(args) {
25037
- const question = safeExt(args, ["question", "query", "q", "text"], "");
25038
- const query2 = question.toLowerCase();
25273
+ const question = String(safeExt(args, ["question", "query", "q", "text"], "")).trim();
25274
+ const requestedKind = String(safeExt(args, ["kind", "type"], "all"));
25275
+ const kind = requestedKind === "topic" || requestedKind === "article" || requestedKind === "summary" ? requestedKind : "all";
25276
+ const requestedLimit = Number(safeExt(args, ["limit", "maxResults"], 8));
25277
+ const limit = Math.min(Math.max(Number.isFinite(requestedLimit) ? requestedLimit : 8, 1), 20);
25278
+ if (!question) {
25279
+ return {
25280
+ content: [{ type: "text", text: "A non-empty wiki query is required." }],
25281
+ isError: true
25282
+ };
25283
+ }
25284
+ const normalizedQuery = normalizeSearchText(question);
25285
+ if (!normalizedQuery) {
25286
+ return { content: [{ type: "text", text: "No matching wiki articles found." }] };
25287
+ }
25288
+ const canReadTopicMemory = runtime().host.canReadTopicMemory;
25289
+ const temporalDirection = /\b(?:latest|newest|recent)\b|\uCD5C\uADFC|\uCD5C\uC2E0/.test(normalizedQuery) ? "newest" : /\b(?:earliest|oldest)\b|\uAC00\uC7A5 \uC624\uB798\uB41C|\uCD5C\uCD08/.test(normalizedQuery) ? "oldest" : undefined;
25290
+ const visibleIndexCandidates = [
25291
+ ...parseWikiIndex(resolve17(runtime().wikiDir, "topic-index.md")),
25292
+ ...parseWikiIndex(resolve17(runtime().wikiDir, "article-index.md"))
25293
+ ].filter((candidate) => kind === "all" || candidate.kind === kind).filter((candidate) => candidate.kind !== "topic" || !canReadTopicMemory || canReadTopicMemory(candidate.key, runtime().userId));
25294
+ const hasStaleExactIndexMatch = visibleIndexCandidates.some((candidate) => !indexCandidateExists(candidate) && scoreIndexKey(question, candidate.key) >= 100);
25295
+ const indexCandidates = visibleIndexCandidates.filter(indexCandidateExists).map((candidate) => ({
25296
+ candidate,
25297
+ keyScore: scoreIndexKey(question, candidate.key),
25298
+ score: scoreIndexCandidate(question, candidate)
25299
+ })).filter(({ keyScore, score }) => keyScore >= 25 || score >= 40).sort((left, right) => right.score - left.score || left.candidate.key.localeCompare(right.candidate.key));
25300
+ const strongestIndex = indexCandidates[0];
25301
+ if (kind === "topic" && strongestIndex && (strongestIndex.keyScore >= 25 || strongestIndex.score >= 40)) {
25302
+ const runnerUp = indexCandidates[1];
25303
+ const ambiguous = strongestIndex.keyScore < 100 && runnerUp !== undefined && runnerUp.score >= strongestIndex.score * 0.9;
25304
+ if (ambiguous) {
25305
+ const lines2 = ["Found 2 ambiguous wiki candidates:", ""];
25306
+ for (const { candidate, score } of [strongestIndex, runnerUp]) {
25307
+ lines2.push(`- kind: ${candidate.kind}`, ` key: ${candidate.key}`, ` score: ${score.toFixed(2)}`, ` description: ${candidate.description || "(none)"}`, ...candidate.date ? [` date: ${candidate.date}`] : []);
25308
+ }
25309
+ return { content: [{ type: "text", text: lines2.join(`
25310
+ `) }] };
25311
+ }
25312
+ const lines = ["Found 1 wiki candidate:", ""];
25313
+ for (const { candidate, score } of [strongestIndex]) {
25314
+ lines.push(`- kind: ${candidate.kind}`, ` key: ${candidate.key}`, ` score: ${score.toFixed(2)}`, ` description: ${candidate.description || "(none)"}`, ...candidate.date ? [` date: ${candidate.date}`] : []);
25315
+ }
25316
+ return { content: [{ type: "text", text: lines.join(`
25317
+ `) }] };
25318
+ }
25039
25319
  const results = [];
25040
25320
  const scored = [];
25041
25321
  function scan(dir, label) {
@@ -25052,67 +25332,234 @@ function wikiQuery(args) {
25052
25332
  const fp = join36(sub, f.name);
25053
25333
  try {
25054
25334
  const text2 = readFileSync22(fp, "utf-8");
25335
+ const key = `${entry.name}/${f.name.replace(/\.md$/i, "")}`;
25055
25336
  scored.push({
25056
- score: scoreMatch(text2),
25337
+ kind: label === "articles" ? "article" : "skill",
25338
+ key,
25339
+ keyScore: scoreIndexKey(question, key),
25340
+ score: scoreMatch(text2, key, label === "articles" ? "article" : "skill"),
25057
25341
  path: `${label}/${entry.name}/${f.name}`,
25058
- text: text2
25342
+ text: text2,
25343
+ title: titleFromDocument(text2, key)
25059
25344
  });
25060
25345
  } catch {}
25061
25346
  }
25062
25347
  } catch {}
25063
25348
  }
25064
25349
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
25350
+ if (label === "topic" && canReadTopicMemory && !canReadTopicMemory(entry.name.replace(/\.md$/i, ""), runtime().userId)) {
25351
+ continue;
25352
+ }
25065
25353
  const fp = join36(dir, entry.name);
25066
25354
  try {
25067
25355
  const text2 = readFileSync22(fp, "utf-8");
25356
+ const key = entry.name.replace(/\.md$/i, "");
25357
+ const resultKind = label === "articles" ? "article" : label === "topic" ? "topic" : label === "summaries" ? "summary" : "skill";
25068
25358
  scored.push({
25069
- score: scoreMatch(text2),
25359
+ kind: resultKind,
25360
+ key,
25361
+ keyScore: scoreIndexKey(question, key),
25362
+ score: scoreMatch(text2, key, resultKind),
25070
25363
  path: `${label}/${entry.name}`,
25071
- text: text2
25364
+ text: text2,
25365
+ title: titleFromDocument(text2, key),
25366
+ ...label === "summaries" && /^\d{4}-\d{2}-\d{2}/.test(key) ? { date: key.slice(0, 10) } : {}
25072
25367
  });
25073
25368
  } catch {}
25074
25369
  }
25075
25370
  }
25076
25371
  } catch {}
25077
25372
  }
25078
- function scoreMatch(text2) {
25079
- const lower = text2.toLowerCase();
25373
+ function titleFromDocument(text2, key) {
25374
+ const titleLine = text2.split(`
25375
+ `).find((line) => /^#+\s*/.test(line));
25376
+ return titleLine ? titleLine.replace(/^#+\s*/, "") : basename7(key);
25377
+ }
25378
+ function scoreMatch(text2, key, resultKind) {
25379
+ const titleLine = text2.split(`
25380
+ `).find((line) => /^#+\s*/.test(line)) ?? "";
25381
+ const title = titleLine.replace(/^#+\s*/, "");
25382
+ const normalizedKey = normalizeSearchText(key);
25383
+ const normalizedTitle2 = normalizeSearchText(title);
25384
+ const normalizedBody = normalizeSearchText(text2.replace(titleLine, ""));
25385
+ const compactQuery = compactSearchText(normalizedQuery);
25386
+ const compactKey = compactSearchText(normalizedKey);
25387
+ const compactTitle = compactSearchText(normalizedTitle2);
25388
+ const tokens = canonicalSearchTokens(normalizedQuery);
25389
+ const keyTokens = new Set(canonicalSearchTokens(normalizedKey));
25390
+ const titleTokens = new Set(canonicalSearchTokens(normalizedTitle2));
25391
+ const bodyTokens = canonicalSearchTokens(normalizedBody);
25080
25392
  let score = 0;
25081
- const words = query2.split(/\s+/).filter(Boolean);
25082
- for (const w of words) {
25083
- if (lower.includes(w)) {
25084
- score += w.length >= 3 ? 3 : 1;
25085
- const firstLine = lower.split(`
25086
- `)[0];
25087
- if (firstLine.startsWith("#") && firstLine.includes(w))
25088
- score += 5;
25089
- }
25090
- }
25393
+ let strongMetadataEvidence = false;
25394
+ if (normalizedKey === normalizedQuery) {
25395
+ score += 120;
25396
+ strongMetadataEvidence = true;
25397
+ } else if (compactKey === compactQuery) {
25398
+ score += 110;
25399
+ strongMetadataEvidence = true;
25400
+ } else if (compactQuery.length >= 2 && (compactKey.startsWith(compactQuery) || compactQuery.startsWith(compactKey))) {
25401
+ score += 65;
25402
+ strongMetadataEvidence = true;
25403
+ }
25404
+ if (tokens.length > 0 && compactQuery.length >= 3 && compactKey.includes(compactQuery)) {
25405
+ score += 45;
25406
+ strongMetadataEvidence = true;
25407
+ }
25408
+ const fuzzyKeyScore = fuzzySearchScore(compactQuery, compactKey, 55);
25409
+ score += fuzzyKeyScore;
25410
+ if (fuzzyKeyScore > 0)
25411
+ strongMetadataEvidence = true;
25412
+ if (normalizedTitle2 === normalizedQuery) {
25413
+ score += 115;
25414
+ strongMetadataEvidence = true;
25415
+ } else if (compactTitle === compactQuery) {
25416
+ score += 105;
25417
+ strongMetadataEvidence = true;
25418
+ } else if (compactQuery.length >= 2 && compactTitle.startsWith(compactQuery)) {
25419
+ score += 60;
25420
+ strongMetadataEvidence = true;
25421
+ }
25422
+ const fuzzyTitleScore = fuzzySearchScore(compactQuery, compactTitle, 50);
25423
+ score += fuzzyTitleScore;
25424
+ if (fuzzyTitleScore > 0)
25425
+ strongMetadataEvidence = true;
25426
+ if (resultKind === "summary") {
25427
+ const temporalScore = scoreSummaryTime(normalizedQuery, key, title);
25428
+ if (temporalScore === null)
25429
+ return 0;
25430
+ score += temporalScore;
25431
+ if (temporalScore > 0)
25432
+ strongMetadataEvidence = true;
25433
+ }
25434
+ const isMultiToken = tokens.length > 1;
25435
+ if (tokens.length > 0 && normalizedTitle2.includes(normalizedQuery)) {
25436
+ score += 35;
25437
+ strongMetadataEvidence = true;
25438
+ }
25439
+ const phraseMatches = isMultiToken ? countPhrase(normalizedBody, normalizedQuery) : 0;
25440
+ if (phraseMatches > 0)
25441
+ score += phraseMatches * 12;
25442
+ else if (tokens.length === 1 && tokens[0].length >= 5 && normalizedBody.includes(tokens[0])) {
25443
+ score += 16;
25444
+ }
25445
+ let bodyMatches = 0;
25446
+ let minimumBodyFrequency = Number.POSITIVE_INFINITY;
25447
+ for (const token of tokens) {
25448
+ if (keyTokens.has(token))
25449
+ score += 10;
25450
+ if (titleTokens.has(token))
25451
+ score += 12;
25452
+ const bodyFrequency = countToken(bodyTokens, token);
25453
+ if (bodyFrequency > 0) {
25454
+ score += Math.min(bodyFrequency, 3) * 4;
25455
+ bodyMatches += 1;
25456
+ minimumBodyFrequency = Math.min(minimumBodyFrequency, bodyFrequency);
25457
+ }
25458
+ }
25459
+ if (isMultiToken && bodyMatches === tokens.length) {
25460
+ score += 8 * Math.min(minimumBodyFrequency, 3);
25461
+ }
25462
+ const minimumBodyTokens = hasStaleExactIndexMatch ? 3 : 2;
25463
+ const partialArticleEvidence = resultKind === "article" && bodyMatches >= 3 && bodyMatches / tokens.length >= 0.6;
25464
+ const strongBodyEvidence = tokens.length >= minimumBodyTokens && (bodyMatches === tokens.length && (phraseMatches > 0 || minimumBodyFrequency >= 1) || partialArticleEvidence);
25465
+ if (!strongMetadataEvidence && !strongBodyEvidence)
25466
+ return 0;
25091
25467
  return score;
25092
25468
  }
25093
- scan(runtime().articlesDir, "articles");
25094
- if (runtime().surface === "all")
25469
+ if (kind === "all" || kind === "article")
25470
+ scan(runtime().articlesDir, "articles");
25471
+ if (kind === "all" && runtime().surface === "all")
25095
25472
  scan(runtime().skillsDir, "skills");
25096
- scan(runtime().topicsDir, "topic");
25097
- scan(runtime().summariesDir, "summaries");
25098
- scored.sort((a, b) => b.score - a.score);
25099
- const top = scored.slice(0, 8);
25100
- if (top.length === 0) {
25473
+ if (kind === "all" || kind === "topic")
25474
+ scan(runtime().topicsDir, "topic");
25475
+ if (kind === "all" || kind === "summary")
25476
+ scan(runtime().summariesDir, "summaries");
25477
+ const merged = new Map;
25478
+ for (const { candidate, keyScore, score } of indexCandidates) {
25479
+ merged.set(`${candidate.kind}:${candidate.key}`, {
25480
+ kind: candidate.kind,
25481
+ key: candidate.key,
25482
+ keyScore,
25483
+ score,
25484
+ description: candidate.description,
25485
+ date: candidate.date
25486
+ });
25487
+ }
25488
+ const documentThreshold = kind === "topic" ? 24 : 16;
25489
+ for (const document of scored) {
25490
+ if (document.score < documentThreshold)
25491
+ continue;
25492
+ const id = `${document.kind}:${document.key}`;
25493
+ const indexed = merged.get(id);
25494
+ merged.set(id, indexed ? { ...indexed, ...document, score: Math.max(indexed.score, document.score) } : document);
25495
+ }
25496
+ let ranked = [...merged.values()].sort((left, right) => right.score - left.score || (left.kind === "summary" && right.kind === "summary" ? temporalDirection === "oldest" ? (left.date ?? "").localeCompare(right.date ?? "") : (right.date ?? "").localeCompare(left.date ?? "") : 0) || left.key.localeCompare(right.key));
25497
+ if (kind === "summary") {
25498
+ const sameTopic = ranked.filter((result) => {
25499
+ const identity = summaryTopicIdentity(result.key);
25500
+ return identity.length >= 3 && normalizedQuery.includes(identity);
25501
+ });
25502
+ if (sameTopic.length > 0)
25503
+ ranked = sameTopic;
25504
+ const requestedDateMatch = normalizedQuery.match(/\b(\d{4}) (\d{2}) (\d{2})\b/);
25505
+ const requestedDate = requestedDateMatch ? `${requestedDateMatch[1]}-${requestedDateMatch[2]}-${requestedDateMatch[3]}` : undefined;
25506
+ const beforeDate = /\bbefore\b/.test(normalizedQuery);
25507
+ const afterDate = /\bafter\b/.test(normalizedQuery);
25508
+ if (requestedDate && beforeDate) {
25509
+ ranked = ranked.filter((result) => !result.date || result.date < requestedDate);
25510
+ } else if (requestedDate && afterDate) {
25511
+ ranked = ranked.filter((result) => !result.date || result.date > requestedDate);
25512
+ }
25513
+ ranked.sort((left, right) => {
25514
+ if (beforeDate) {
25515
+ return (right.date ?? "").localeCompare(left.date ?? "") || right.score - left.score;
25516
+ }
25517
+ if (afterDate) {
25518
+ return (left.date ?? "").localeCompare(right.date ?? "") || right.score - left.score;
25519
+ }
25520
+ if (requestedDate) {
25521
+ const dateDifference = Number(right.date === requestedDate) - Number(left.date === requestedDate);
25522
+ if (dateDifference !== 0)
25523
+ return dateDifference;
25524
+ }
25525
+ if (temporalDirection) {
25526
+ const dateDifference = temporalDirection === "oldest" ? (left.date ?? "").localeCompare(right.date ?? "") : (right.date ?? "").localeCompare(left.date ?? "");
25527
+ if (dateDifference !== 0)
25528
+ return dateDifference;
25529
+ }
25530
+ return right.score - left.score || (right.date ?? "").localeCompare(left.date ?? "") || left.key.localeCompare(right.key);
25531
+ });
25532
+ }
25533
+ if (kind === "topic" && ranked[0]) {
25534
+ const runnerUp = ranked[1];
25535
+ const ambiguous = ranked[0].keyScore < 100 && runnerUp !== undefined && runnerUp.score >= ranked[0].score * 0.9;
25536
+ if (ambiguous) {
25537
+ ranked = ranked.slice(0, 2);
25538
+ } else {
25539
+ ranked = ranked.slice(0, 1);
25540
+ }
25541
+ } else {
25542
+ ranked = ranked.slice(0, limit);
25543
+ }
25544
+ if (ranked.length === 0) {
25101
25545
  results.push("No matching wiki articles found.");
25102
25546
  } else {
25103
- results.push(`Found ${top.length} matching result(s):
25547
+ results.push(`Found ${ranked.length} matching result(s):
25104
25548
  `);
25105
- for (const { path, text: text2 } of top) {
25106
- const titleLine = text2.split(`
25107
- `).find((l) => l.startsWith("#"));
25108
- const title = titleLine ? titleLine.replace(/^#+\s*/, "") : basename7(path, ".md");
25109
- const body = text2.replace(/^---[\s\S]*?---\n?/, "").replace(/^#.*$/m, "").trim().slice(0, 400);
25110
- results.push(`## ${title}
25111
- Path: ${path}
25112
-
25113
- ${body}${text2.length > 400 ? `
25549
+ for (const result of ranked) {
25550
+ if (result.kind === "skill") {
25551
+ const body = (result.text ?? "").trim().slice(0, 400);
25552
+ results.push(`## ${result.title ?? result.key}
25553
+ Path: ${result.path}
25554
+
25555
+ ${body}${(result.text?.length ?? 0) > 400 ? `
25114
25556
  ...(truncated)` : ""}
25115
25557
  `);
25558
+ continue;
25559
+ }
25560
+ results.push(`- kind: ${result.kind}`, ` key: ${result.key}`, ` score: ${result.score.toFixed(2)}`, ...result.title ? [` title: ${result.title}`] : [], ` description: ${result.description || "(none)"}`, ...result.date ? [` date: ${result.date}`] : [], ...result.path ? [` path: ${result.path}`, ` Path: ${result.path}`] : [], ...result.text ? [
25561
+ ` excerpt: ${result.text.replace(/^---[\s\S]*?---\n?/, "").replace(/^#.*$/m, "").trim().slice(0, 400)}`
25562
+ ] : []);
25116
25563
  }
25117
25564
  }
25118
25565
  return { content: [{ type: "text", text: results.join(`
@@ -25131,36 +25578,94 @@ function wikiTopicBrief(args) {
25131
25578
  };
25132
25579
  }
25133
25580
  const { getTopicBrief: getTopicBrief2, resolveTopicBrief: resolveTopicBrief2 } = runtime().host;
25134
- if (runtime().topicId && (getTopicBrief2 || resolveTopicBrief2)) {
25581
+ const hasAuthorizedBridge = Boolean(runtime().topicId && (getTopicBrief2 || resolveTopicBrief2));
25582
+ if (hasAuthorizedBridge) {
25135
25583
  try {
25136
25584
  const brief = resolveTopicBrief2 ? resolveTopicBrief2(rawTopic, runtime().userId) : getTopicBrief2(rawTopic);
25137
- if (!brief) {
25138
- return {
25139
- content: [{ type: "text", text: `No brief found for topic: ${rawTopic}` }]
25140
- };
25141
- }
25142
- const lines = [
25143
- `# Topic Brief: ${topicNameFrom(rawTopic)}`,
25144
- "",
25145
- brief.briefMd || "(empty brief)"
25146
- ];
25147
- if (brief.latestSummaryMd) {
25148
- lines.push("", "## Latest Summary", "", brief.latestSummaryMd);
25149
- }
25150
- if (brief.summaryDate) {
25151
- lines.push("", `Summary date: ${brief.summaryDate}`);
25152
- }
25153
- return { content: [{ type: "text", text: lines.join(`
25585
+ if (brief) {
25586
+ const lines = [
25587
+ `# Topic Brief: ${topicNameFrom(rawTopic)}`,
25588
+ "",
25589
+ brief.briefMd || "(empty brief)"
25590
+ ];
25591
+ if (brief.latestSummaryMd) {
25592
+ lines.push("", "## Latest Summary", "", brief.latestSummaryMd);
25593
+ }
25594
+ if (brief.summaryDate) {
25595
+ lines.push("", `Summary date: ${brief.summaryDate}`);
25596
+ }
25597
+ return { content: [{ type: "text", text: lines.join(`
25154
25598
  `) }] };
25155
- } catch {
25599
+ }
25600
+ } catch {}
25601
+ return {
25602
+ content: [{ type: "text", text: `No brief found for topic: ${rawTopic}` }]
25603
+ };
25604
+ }
25605
+ const topicKey = wikiSummarySlug(rawTopic.replace(/^topic\//, "").replace(/\.md$/i, ""));
25606
+ const filePath = resolve17(runtime().topicsDir, `${topicKey}.md`);
25607
+ try {
25608
+ const content = readFileSync22(filePath, "utf-8");
25609
+ return {
25610
+ content: [{ type: "text", text: `# Topic Brief: ${topicKey}
25611
+
25612
+ ${content}` }]
25613
+ };
25614
+ } catch {}
25615
+ return {
25616
+ content: [{ type: "text", text: `No brief found for topic: ${rawTopic}` }]
25617
+ };
25618
+ }
25619
+ function wikiRead(args) {
25620
+ const rawKind = String(safeExt(args, ["kind", "type"], "article"));
25621
+ const kind = rawKind === "topic" || rawKind === "summary" ? rawKind : "article";
25622
+ const rawKey = String(safeExt(args, ["key", "slug", "topic"], "")).trim();
25623
+ if (!rawKey) {
25624
+ return { content: [{ type: "text", text: "A wiki document key is required." }], isError: true };
25625
+ }
25626
+ if (kind === "topic") {
25627
+ const result = wikiTopicBrief({ topic: rawKey });
25628
+ const resultText = result.content.map((entry) => entry.type === "text" ? entry.text : "").join(`
25629
+ `);
25630
+ const adopt = safeExt(args, ["adopt", "use", "select"], false);
25631
+ if (!adopt || resultText.startsWith("No brief found for topic"))
25632
+ return result;
25633
+ const currentTopicId = runtime().currentTopicId;
25634
+ const memoryKey = wikiSummarySlug(rawKey.replace(/^topic\//, "").replace(/\.md$/i, ""));
25635
+ if (!currentTopicId || !runtime().host.adoptTopicMemory?.(currentTopicId, runtime().userId, memoryKey)) {
25156
25636
  return {
25157
- content: [{ type: "text", text: `Could not read topic brief for: ${rawTopic}` }]
25637
+ content: [
25638
+ ...result.content,
25639
+ { type: "text", text: `
25640
+ Memory was read but could not be adopted by this topic.` }
25641
+ ]
25158
25642
  };
25159
25643
  }
25644
+ return {
25645
+ content: [...result.content, { type: "text", text: `
25646
+ Adopted topic memory: ${memoryKey}` }]
25647
+ };
25648
+ }
25649
+ if (rawKey.includes("\\") || rawKey.split("/").some((part) => part === "..") || rawKey.startsWith("/")) {
25650
+ return { content: [{ type: "text", text: "Invalid wiki document key." }], isError: true };
25651
+ }
25652
+ const root = kind === "summary" ? runtime().summariesDir : runtime().articlesDir;
25653
+ const key = rawKey.replace(new RegExp(`^(?:${kind === "summary" ? "summaries" : "articles"})/`), "").replace(/\.md$/i, "");
25654
+ const filePath = resolve17(root, `${key}.md`);
25655
+ const relativePath = relative2(root, filePath);
25656
+ if (relativePath.startsWith("..") || relativePath.startsWith("/")) {
25657
+ return { content: [{ type: "text", text: "Invalid wiki document key." }], isError: true };
25658
+ }
25659
+ try {
25660
+ const content = readFileSync22(filePath, "utf-8");
25661
+ return {
25662
+ content: [{ type: "text", text: `# Wiki ${kind}: ${key}
25663
+
25664
+ ${content}` }]
25665
+ };
25666
+ } catch {
25667
+ return { content: [{ type: "text", text: `No ${kind} found for: ${rawKey}` }] };
25160
25668
  }
25161
- return {
25162
- content: [{ type: "text", text: `No topic brief found for: ${rawTopic}` }]
25163
- };
25164
25669
  }
25165
25670
  function wikiLastConversation(args) {
25166
25671
  const rawTopic = safeExt(args, ["topic", "topicName", "topicId"], runtime().topicId);
@@ -25529,6 +26034,7 @@ function createWikiMcpServer(context, host2) {
25529
26034
  const wikiDir = resolve17(host2.wikiRoot);
25530
26035
  const current3 = {
25531
26036
  userId: context.userId,
26037
+ currentTopicId: context.currentTopicId ?? context.topicId,
25532
26038
  topicId: context.topicId,
25533
26039
  surface,
25534
26040
  host: host2,
@@ -25558,6 +26064,7 @@ function createWikiMcpServer(context, host2) {
25558
26064
  const { name, arguments: args } = request.params;
25559
26065
  const handlers = {
25560
26066
  wiki_query: wikiQuery,
26067
+ wiki_read: wikiRead,
25561
26068
  wiki_topic_brief: wikiTopicBrief,
25562
26069
  wiki_last_conversation: wikiLastConversation,
25563
26070
  skill_query: skillQuery,
@@ -25572,16 +26079,87 @@ function createWikiMcpServer(context, host2) {
25572
26079
  });
25573
26080
  return server;
25574
26081
  }
25575
- var wikiRuntime, FILE_LOCK_WAIT_MS = 10, FILE_LOCK_TIMEOUT_MS = 5000, FILE_LOCK_STALE_MS = 30000, lockWaitArray, WIKI_TOOLS, SKILL_TOOLS;
26082
+ var wikiRuntime, FILE_LOCK_WAIT_MS = 10, FILE_LOCK_TIMEOUT_MS = 5000, FILE_LOCK_STALE_MS = 30000, lockWaitArray, SEARCH_STOP_WORDS, SEARCH_MONTHS, SEARCH_RECENCY_TERMS, WIKI_TOOLS, SKILL_TOOLS;
25576
26083
  var init_wiki_server = __esm(() => {
25577
26084
  init_config();
25578
26085
  init_wiki_summary_names();
25579
26086
  wikiRuntime = new AsyncLocalStorage3;
25580
26087
  lockWaitArray = new Int32Array(new SharedArrayBuffer(4));
26088
+ SEARCH_STOP_WORDS = new Set([
26089
+ "a",
26090
+ "an",
26091
+ "and",
26092
+ "are",
26093
+ "as",
26094
+ "at",
26095
+ "be",
26096
+ "by",
26097
+ "for",
26098
+ "from",
26099
+ "in",
26100
+ "is",
26101
+ "it",
26102
+ "of",
26103
+ "on",
26104
+ "or",
26105
+ "that",
26106
+ "the",
26107
+ "to",
26108
+ "with"
26109
+ ]);
26110
+ SEARCH_MONTHS = new Map([
26111
+ ["january", "01"],
26112
+ ["jan", "01"],
26113
+ ["1\uC6D4", "01"],
26114
+ ["february", "02"],
26115
+ ["feb", "02"],
26116
+ ["2\uC6D4", "02"],
26117
+ ["march", "03"],
26118
+ ["mar", "03"],
26119
+ ["3\uC6D4", "03"],
26120
+ ["april", "04"],
26121
+ ["apr", "04"],
26122
+ ["4\uC6D4", "04"],
26123
+ ["may", "05"],
26124
+ ["5\uC6D4", "05"],
26125
+ ["june", "06"],
26126
+ ["jun", "06"],
26127
+ ["6\uC6D4", "06"],
26128
+ ["july", "07"],
26129
+ ["jul", "07"],
26130
+ ["7\uC6D4", "07"],
26131
+ ["august", "08"],
26132
+ ["aug", "08"],
26133
+ ["8\uC6D4", "08"],
26134
+ ["september", "09"],
26135
+ ["sep", "09"],
26136
+ ["sept", "09"],
26137
+ ["9\uC6D4", "09"],
26138
+ ["october", "10"],
26139
+ ["oct", "10"],
26140
+ ["10\uC6D4", "10"],
26141
+ ["november", "11"],
26142
+ ["nov", "11"],
26143
+ ["11\uC6D4", "11"],
26144
+ ["december", "12"],
26145
+ ["dec", "12"],
26146
+ ["12\uC6D4", "12"]
26147
+ ]);
26148
+ SEARCH_RECENCY_TERMS = new Set([
26149
+ "latest",
26150
+ "newest",
26151
+ "recent",
26152
+ "earliest",
26153
+ "oldest",
26154
+ "\uCD5C\uADFC",
26155
+ "\uCD5C\uC2E0",
26156
+ "\uCD5C\uCD08",
26157
+ "\uC624\uB798\uB41C"
26158
+ ]);
25581
26159
  WIKI_TOOLS = [
25582
26160
  {
25583
26161
  name: "wiki_query",
25584
- description: "Search the wiki knowledge base and return relevant articles.",
26162
+ description: "Search topic, article, or summary indexes and return relevant candidates.",
25585
26163
  inputSchema: {
25586
26164
  type: "object",
25587
26165
  properties: {
@@ -25592,11 +26170,38 @@ var init_wiki_server = __esm(() => {
25592
26170
  topic: {
25593
26171
  type: "string",
25594
26172
  description: "Optional topic name to narrow the search"
26173
+ },
26174
+ kind: {
26175
+ type: "string",
26176
+ enum: ["all", "topic", "article", "summary"],
26177
+ description: "Document kind to search; defaults to all"
26178
+ },
26179
+ limit: {
26180
+ type: "number",
26181
+ minimum: 1,
26182
+ maximum: 20,
26183
+ description: "Maximum number of candidates; defaults to 8"
25595
26184
  }
25596
26185
  },
25597
26186
  required: ["question"]
25598
26187
  }
25599
26188
  },
26189
+ {
26190
+ name: "wiki_read",
26191
+ description: "Read one topic, article, or summary returned by wiki_query. For a clearly matching continuing persona, set adopt=true when reading a topic.",
26192
+ inputSchema: {
26193
+ type: "object",
26194
+ properties: {
26195
+ kind: { type: "string", enum: ["topic", "article", "summary"] },
26196
+ key: { type: "string", description: "Canonical key returned by wiki_query" },
26197
+ adopt: {
26198
+ type: "boolean",
26199
+ description: "For kind=topic, persist this canonical memory key for the current room"
26200
+ }
26201
+ },
26202
+ required: ["kind", "key"]
26203
+ }
26204
+ },
25600
26205
  {
25601
26206
  name: "wiki_topic_brief",
25602
26207
  description: "Get the lightweight topic brief for a specific topic.",
@@ -30914,6 +31519,7 @@ var init_commands = __esm(() => {
30914
31519
  { name: "new", usage: "/new", description: "reset the current session" },
30915
31520
  { name: "compact", usage: "/compact", description: "summarize and shrink context" },
30916
31521
  { name: "status", usage: "/status", description: "show model and token usage" },
31522
+ { name: "context", usage: "/context", description: "show live context breakdown" },
30917
31523
  { name: "model", usage: "/model", description: "choose the model" },
30918
31524
  { name: "effort", usage: "/effort", description: "choose reasoning effort" },
30919
31525
  { name: "topics", usage: "/topics", description: "open topic picker" },
@@ -30942,297 +31548,6 @@ var init_commands = __esm(() => {
30942
31548
  ];
30943
31549
  });
30944
31550
 
30945
- // ../../adapters/terminal/src/path-suggest.ts
30946
- import { execFile as execFile3 } from "child_process";
30947
- import { existsSync as existsSync33, readdirSync as readdirSync14, statSync as statSync19 } from "fs";
30948
- import { homedir as homedir10 } from "os";
30949
- import { basename as basename11, dirname as dirname19, join as join40, sep as sep3 } from "path";
30950
- import { promisify as promisify3 } from "util";
30951
- function indexRecursivePaths(files) {
30952
- const paths = [];
30953
- const directories = new Set;
30954
- for (const relPath of files) {
30955
- const segments = relPath.split("/");
30956
- if (segments.some((segment) => segment.startsWith(".")))
30957
- continue;
30958
- for (let index = 0;index < segments.length - 1; index += 1) {
30959
- const directory = segments.slice(0, index + 1).join("/");
30960
- if (directories.has(directory))
30961
- continue;
30962
- directories.add(directory);
30963
- paths.push({
30964
- relPath: directory,
30965
- isDir: true,
30966
- lowerName: (segments[index] ?? "").toLowerCase()
30967
- });
30968
- }
30969
- paths.push({ relPath, isDir: false, lowerName: (segments.at(-1) ?? "").toLowerCase() });
30970
- }
30971
- return paths;
30972
- }
30973
- async function listFilesWithRipgrep(dir) {
30974
- const args = ["--files", "--max-depth", String(RECURSIVE_MAX_DEPTH)];
30975
- for (const excluded of RECURSIVE_EXCLUDES)
30976
- args.push("-g", `!${excluded}`);
30977
- args.push(".");
30978
- let out = "";
30979
- try {
30980
- const result = await execFileAsync3("rg", args, {
30981
- cwd: dir,
30982
- encoding: "utf8",
30983
- timeout: RECURSIVE_TIMEOUT_MS,
30984
- maxBuffer: 32 * 1024 * 1024
30985
- });
30986
- out = result.stdout;
30987
- } catch (err2) {
30988
- const stdout = err2?.stdout;
30989
- out = typeof stdout === "string" ? stdout : "";
30990
- }
30991
- return out.split(`
30992
- `).filter(Boolean).map((path) => path.replaceAll(sep3, "/").replace(/^\.\//, ""));
30993
- }
30994
- function cacheRecursivePaths(dir, files) {
30995
- const now = Date.now();
30996
- for (const [key, entry] of recursiveCache) {
30997
- if (now - entry.loadedAt >= RECURSIVE_CACHE_TTL_MS)
30998
- recursiveCache.delete(key);
30999
- }
31000
- recursiveCache.delete(dir);
31001
- while (recursiveCache.size >= MAX_RECURSIVE_CACHE_ENTRIES) {
31002
- const oldest = recursiveCache.keys().next().value;
31003
- if (oldest === undefined)
31004
- break;
31005
- recursiveCache.delete(oldest);
31006
- }
31007
- recursiveCache.set(dir, {
31008
- loadedAt: now,
31009
- paths: indexRecursivePaths(files),
31010
- matches: new Map
31011
- });
31012
- }
31013
- function loadFilesRecursive(dir, loader) {
31014
- const cached = recursiveCache.get(dir);
31015
- if (cached && Date.now() - cached.loadedAt < RECURSIVE_CACHE_TTL_MS) {
31016
- recursiveCache.delete(dir);
31017
- recursiveCache.set(dir, cached);
31018
- return Promise.resolve(false);
31019
- }
31020
- const active2 = recursiveSearches.get(dir);
31021
- if (active2)
31022
- return active2;
31023
- const search = (async () => {
31024
- try {
31025
- cacheRecursivePaths(dir, await loader(dir));
31026
- } catch {
31027
- cacheRecursivePaths(dir, []);
31028
- }
31029
- return true;
31030
- })().finally(() => recursiveSearches.delete(dir));
31031
- recursiveSearches.set(dir, search);
31032
- return search;
31033
- }
31034
- function activeAtToken(lineText, col) {
31035
- const upto = lineText.slice(0, col);
31036
- const match = /(?:^|\s)@([^\s]*)$/.exec(upto);
31037
- if (!match)
31038
- return null;
31039
- const frag = match[1] ?? "";
31040
- return { start: col - frag.length - 1, frag };
31041
- }
31042
- function resolveFragment(frag) {
31043
- const home = homedir10();
31044
- let path;
31045
- if (frag === "" || frag === "~")
31046
- path = home;
31047
- else if (frag === "~/")
31048
- path = home;
31049
- else if (frag.startsWith("~/"))
31050
- path = join40(home, frag.slice(2));
31051
- else if (frag.startsWith("/"))
31052
- path = frag;
31053
- else
31054
- path = join40(home, frag);
31055
- if (frag.endsWith("/") || frag === "" || frag === "~") {
31056
- return { dir: path, prefix: "" };
31057
- }
31058
- return { dir: dirname19(path), prefix: basename11(path) };
31059
- }
31060
- function toToken(fullPath, isDir) {
31061
- const home = homedir10();
31062
- let shown = fullPath;
31063
- if (fullPath === home)
31064
- shown = "~";
31065
- else if (fullPath.startsWith(`${home}/`))
31066
- shown = `~/${fullPath.slice(home.length + 1)}`;
31067
- return `@${shown}${isDir ? "/" : ""}`;
31068
- }
31069
- function rankAndSlice(dir, candidates) {
31070
- candidates.sort((a, b) => {
31071
- if (a.isPrefixMatch !== b.isPrefixMatch)
31072
- return a.isPrefixMatch ? -1 : 1;
31073
- if (a.isDir !== b.isDir)
31074
- return a.isDir ? -1 : 1;
31075
- const depthA = a.relPath.split("/").length;
31076
- const depthB = b.relPath.split("/").length;
31077
- if (depthA !== depthB)
31078
- return depthA - depthB;
31079
- return a.relPath.localeCompare(b.relPath);
31080
- });
31081
- return candidates.slice(0, MAX_SUGGESTIONS).map(({ relPath, isDir }) => ({
31082
- label: `${relPath}${isDir ? "/" : ""}`,
31083
- value: toToken(join40(dir, relPath), isDir),
31084
- isDir
31085
- }));
31086
- }
31087
- function recursiveCandidates(dir, lowerPrefix, seen) {
31088
- const entry = recursiveCache.get(dir);
31089
- if (!entry)
31090
- return [];
31091
- let matches = entry.matches.get(lowerPrefix);
31092
- if (!matches) {
31093
- matches = entry.paths.filter((path) => path.lowerName.includes(lowerPrefix)).map((path) => ({
31094
- relPath: path.relPath,
31095
- isDir: path.isDir,
31096
- isPrefixMatch: path.lowerName.startsWith(lowerPrefix)
31097
- }));
31098
- if (entry.matches.size >= MAX_MATCH_CACHE_ENTRIES) {
31099
- const oldest = entry.matches.keys().next().value;
31100
- if (oldest !== undefined)
31101
- entry.matches.delete(oldest);
31102
- }
31103
- entry.matches.set(lowerPrefix, matches);
31104
- } else {
31105
- entry.matches.delete(lowerPrefix);
31106
- entry.matches.set(lowerPrefix, matches);
31107
- }
31108
- return matches.filter((candidate) => {
31109
- if (seen.has(candidate.relPath))
31110
- return false;
31111
- seen.add(candidate.relPath);
31112
- return true;
31113
- });
31114
- }
31115
- function isRecursivePathQuery(lineText, col) {
31116
- const token = activeAtToken(lineText, col);
31117
- if (!token)
31118
- return false;
31119
- const { prefix } = resolveFragment(token.frag);
31120
- return !prefix.startsWith(".") && prefix.length >= RECURSIVE_MIN_PREFIX_LEN;
31121
- }
31122
- async function warmPathSuggestions(lineText, col, loader = listFilesWithRipgrep) {
31123
- if (!isRecursivePathQuery(lineText, col))
31124
- return false;
31125
- const token = activeAtToken(lineText, col);
31126
- if (!token)
31127
- return false;
31128
- const { dir } = resolveFragment(token.frag);
31129
- try {
31130
- if (!statSync19(dir).isDirectory())
31131
- return false;
31132
- } catch {
31133
- return false;
31134
- }
31135
- return loadFilesRecursive(dir, loader);
31136
- }
31137
- function pathSuggestions(lineText, col) {
31138
- const token = activeAtToken(lineText, col);
31139
- if (!token)
31140
- return null;
31141
- const { dir, prefix } = resolveFragment(token.frag);
31142
- let entries;
31143
- try {
31144
- entries = readdirSync14(dir, { withFileTypes: true });
31145
- } catch {
31146
- return { start: token.start, end: col, items: [], truncated: 0, searching: false };
31147
- }
31148
- const lowerPrefix = prefix.toLowerCase();
31149
- const showHidden = prefix.startsWith(".");
31150
- const candidates = entries.filter((entry) => {
31151
- const name = entry.name;
31152
- if (!showHidden && name.startsWith("."))
31153
- return false;
31154
- if (lowerPrefix === "")
31155
- return true;
31156
- const lowerName = name.toLowerCase();
31157
- return showHidden ? lowerName.startsWith(lowerPrefix) : lowerName.includes(lowerPrefix);
31158
- }).map((entry) => {
31159
- let isDir = entry.isDirectory();
31160
- if (!isDir && entry.isSymbolicLink()) {
31161
- try {
31162
- isDir = statSync19(join40(dir, entry.name)).isDirectory();
31163
- } catch {}
31164
- }
31165
- return {
31166
- relPath: entry.name,
31167
- isDir,
31168
- isPrefixMatch: entry.name.toLowerCase().startsWith(lowerPrefix)
31169
- };
31170
- });
31171
- if (!showHidden && lowerPrefix.length >= RECURSIVE_MIN_PREFIX_LEN) {
31172
- const seen = new Set(candidates.map((c) => c.relPath));
31173
- candidates.push(...recursiveCandidates(dir, lowerPrefix, seen));
31174
- }
31175
- const items = rankAndSlice(dir, candidates);
31176
- return {
31177
- start: token.start,
31178
- end: col,
31179
- items,
31180
- truncated: Math.max(0, candidates.length - items.length),
31181
- searching: recursiveSearches.has(dir)
31182
- };
31183
- }
31184
- function fragmentToAbsolutePath(frag) {
31185
- const home = homedir10();
31186
- if (frag === "~" || frag === "~/")
31187
- return home;
31188
- if (frag.startsWith("~/"))
31189
- return join40(home, frag.slice(2));
31190
- if (frag.startsWith("/"))
31191
- return frag;
31192
- return join40(home, frag);
31193
- }
31194
- function fragmentResolves(frag) {
31195
- try {
31196
- return existsSync33(fragmentToAbsolutePath(frag));
31197
- } catch {
31198
- return false;
31199
- }
31200
- }
31201
- function stripResolvedPathTriggers(text2) {
31202
- return text2.replaceAll(AT_PATH_TOKEN, (whole, lead, frag) => {
31203
- if (fragmentResolves(frag))
31204
- return `${lead}${frag}`;
31205
- const pathPart = frag.replace(TRAILING_PUNCT, "");
31206
- if (pathPart && pathPart !== frag && fragmentResolves(pathPart)) {
31207
- return `${lead}${pathPart}${frag.slice(pathPart.length)}`;
31208
- }
31209
- return whole;
31210
- });
31211
- }
31212
- function completePathToken(lineText, col, suggestion, options = {}) {
31213
- const token = activeAtToken(lineText, col);
31214
- if (!token)
31215
- return null;
31216
- let tokenEnd = col;
31217
- while (tokenEnd < lineText.length && !/\s/.test(lineText[tokenEnd] ?? " "))
31218
- tokenEnd += 1;
31219
- const trailingText = lineText.slice(col, tokenEnd);
31220
- const pathTail = trailingText.replace(TRAILING_PUNCT, "");
31221
- const end = col + pathTail.length;
31222
- const replacement = options.keepTrigger ? suggestion.value : suggestion.value.slice(1);
31223
- const line = lineText.slice(0, token.start) + replacement + lineText.slice(end);
31224
- return { line, col: token.start + replacement.length };
31225
- }
31226
- var MAX_SUGGESTIONS = 8, RECURSIVE_MIN_PREFIX_LEN = 4, RECURSIVE_MAX_DEPTH = 10, RECURSIVE_TIMEOUT_MS = 2000, RECURSIVE_CACHE_TTL_MS = 30000, MAX_RECURSIVE_CACHE_ENTRIES = 3, MAX_MATCH_CACHE_ENTRIES = 24, RECURSIVE_EXCLUDES, recursiveCache, recursiveSearches, execFileAsync3, AT_PATH_TOKEN, TRAILING_PUNCT;
31227
- var init_path_suggest = __esm(() => {
31228
- RECURSIVE_EXCLUDES = ["node_modules", ".git", ".cache", "dist", "build", "Library", ".Trash"];
31229
- recursiveCache = new Map;
31230
- recursiveSearches = new Map;
31231
- execFileAsync3 = promisify3(execFile3);
31232
- AT_PATH_TOKEN = /(^|[\s([{<])@([^\s]+)/g;
31233
- TRAILING_PUNCT = /[.,;:!?)\]}>"'`]+$/;
31234
- });
31235
-
31236
31551
  // ../../adapters/terminal/src/subagent-graph.ts
31237
31552
  import {
31238
31553
  layoutTerminalGraph,
@@ -31867,6 +32182,19 @@ function applyAiStatus(state, topicId, raw, createdAt) {
31867
32182
  tools: []
31868
32183
  });
31869
32184
  }
32185
+ if (kind === "context_progress") {
32186
+ if (isStaleTerminalStatus(current3, status))
32187
+ return state;
32188
+ return setActivity(state, topicId, {
32189
+ ...liveCurrent,
32190
+ running: true,
32191
+ queryId: typeof status.queryId === "string" ? status.queryId : current3.queryId,
32192
+ contextProgress: {
32193
+ assistantTokens: Math.max(0, Number(status.assistantTokens) || 0),
32194
+ toolTokens: Math.max(0, Number(status.toolTokens) || 0)
32195
+ }
32196
+ });
32197
+ }
31870
32198
  if (kind === "ai_done") {
31871
32199
  if (isStaleTerminalStatus(current3, status))
31872
32200
  return state;
@@ -31980,6 +32308,356 @@ var init_state2 = __esm(async () => {
31980
32308
  await init_src();
31981
32309
  });
31982
32310
 
32311
+ // ../../adapters/terminal/src/context-usage.ts
32312
+ function latestUsageMessage(messages) {
32313
+ for (let index = messages.length - 1;index >= 0; index--) {
32314
+ const usage = messages[index]?.authorId === "ai" ? messages[index]?.usage : undefined;
32315
+ if (usage?.context !== undefined && usage.contextWindow)
32316
+ return { index, usage };
32317
+ }
32318
+ return;
32319
+ }
32320
+ function estimateMessages(messages, predicate) {
32321
+ return messages.reduce((sum, message) => predicate(message) ? sum + 4 + estimateTextTokens(message.text) : sum, 0);
32322
+ }
32323
+ function activeContextBreakdown(state) {
32324
+ const topic = activeTopic(state);
32325
+ if (!topic)
32326
+ return;
32327
+ const messages = activeMessages(state);
32328
+ const latest = latestUsageMessage(messages);
32329
+ const stored = state.topicUsage[topic.id]?.currentSession;
32330
+ const confirmed = latest?.usage.context ?? stored?.contextTokens;
32331
+ const contextWindow = latest?.usage.contextWindow ?? stored?.contextWindow;
32332
+ if (confirmed === undefined || !contextWindow)
32333
+ return;
32334
+ const activity = state.activity[topic.id];
32335
+ if (!activity?.running) {
32336
+ return {
32337
+ context: confirmed,
32338
+ contextWindow,
32339
+ confirmed,
32340
+ user: 0,
32341
+ assistant: 0,
32342
+ tools: 0,
32343
+ free: Math.max(0, contextWindow - confirmed),
32344
+ estimated: false
32345
+ };
32346
+ }
32347
+ const afterBaseline = latest ? messages.slice(latest.index + 1) : stored?.timestamp ? messages.filter((message) => message.createdAt > stored.timestamp) : messages;
32348
+ const user = estimateMessages(afterBaseline, (message) => message.authorId !== "ai" && message.kind !== "system");
32349
+ const assistant = activity.contextProgress?.assistantTokens ?? estimateMessages(afterBaseline, (message) => message.authorId === "ai" && message.kind !== "tool");
32350
+ const tools = activity.contextProgress?.toolTokens ?? estimateMessages(afterBaseline, (message) => message.kind === "tool");
32351
+ const context = confirmed + user + assistant + tools;
32352
+ return {
32353
+ context,
32354
+ contextWindow,
32355
+ confirmed,
32356
+ user,
32357
+ assistant,
32358
+ tools,
32359
+ free: Math.max(0, contextWindow - context),
32360
+ estimated: true
32361
+ };
32362
+ }
32363
+ var init_context_usage = __esm(async () => {
32364
+ await __promiseAll([
32365
+ init_src(),
32366
+ init_state2()
32367
+ ]);
32368
+ });
32369
+
32370
+ // ../../adapters/terminal/src/path-suggest.ts
32371
+ import { execFile as execFile3 } from "child_process";
32372
+ import { existsSync as existsSync33, readdirSync as readdirSync14, statSync as statSync19 } from "fs";
32373
+ import { homedir as homedir10 } from "os";
32374
+ import { basename as basename11, dirname as dirname19, join as join40, sep as sep3 } from "path";
32375
+ import { promisify as promisify3 } from "util";
32376
+ function indexRecursivePaths(files) {
32377
+ const paths = [];
32378
+ const directories = new Set;
32379
+ for (const relPath of files) {
32380
+ const segments = relPath.split("/");
32381
+ if (segments.some((segment) => segment.startsWith(".")))
32382
+ continue;
32383
+ for (let index = 0;index < segments.length - 1; index += 1) {
32384
+ const directory = segments.slice(0, index + 1).join("/");
32385
+ if (directories.has(directory))
32386
+ continue;
32387
+ directories.add(directory);
32388
+ paths.push({
32389
+ relPath: directory,
32390
+ isDir: true,
32391
+ lowerName: (segments[index] ?? "").toLowerCase()
32392
+ });
32393
+ }
32394
+ paths.push({ relPath, isDir: false, lowerName: (segments.at(-1) ?? "").toLowerCase() });
32395
+ }
32396
+ return paths;
32397
+ }
32398
+ async function listFilesWithRipgrep(dir) {
32399
+ const args = ["--files", "--max-depth", String(RECURSIVE_MAX_DEPTH)];
32400
+ for (const excluded of RECURSIVE_EXCLUDES)
32401
+ args.push("-g", `!${excluded}`);
32402
+ args.push(".");
32403
+ let out = "";
32404
+ try {
32405
+ const result = await execFileAsync3("rg", args, {
32406
+ cwd: dir,
32407
+ encoding: "utf8",
32408
+ timeout: RECURSIVE_TIMEOUT_MS,
32409
+ maxBuffer: 32 * 1024 * 1024
32410
+ });
32411
+ out = result.stdout;
32412
+ } catch (err2) {
32413
+ const stdout = err2?.stdout;
32414
+ out = typeof stdout === "string" ? stdout : "";
32415
+ }
32416
+ return out.split(`
32417
+ `).filter(Boolean).map((path) => path.replaceAll(sep3, "/").replace(/^\.\//, ""));
32418
+ }
32419
+ function cacheRecursivePaths(dir, files) {
32420
+ const now = Date.now();
32421
+ for (const [key, entry] of recursiveCache) {
32422
+ if (now - entry.loadedAt >= RECURSIVE_CACHE_TTL_MS)
32423
+ recursiveCache.delete(key);
32424
+ }
32425
+ recursiveCache.delete(dir);
32426
+ while (recursiveCache.size >= MAX_RECURSIVE_CACHE_ENTRIES) {
32427
+ const oldest = recursiveCache.keys().next().value;
32428
+ if (oldest === undefined)
32429
+ break;
32430
+ recursiveCache.delete(oldest);
32431
+ }
32432
+ recursiveCache.set(dir, {
32433
+ loadedAt: now,
32434
+ paths: indexRecursivePaths(files),
32435
+ matches: new Map
32436
+ });
32437
+ }
32438
+ function loadFilesRecursive(dir, loader) {
32439
+ const cached = recursiveCache.get(dir);
32440
+ if (cached && Date.now() - cached.loadedAt < RECURSIVE_CACHE_TTL_MS) {
32441
+ recursiveCache.delete(dir);
32442
+ recursiveCache.set(dir, cached);
32443
+ return Promise.resolve(false);
32444
+ }
32445
+ const active2 = recursiveSearches.get(dir);
32446
+ if (active2)
32447
+ return active2;
32448
+ const search = (async () => {
32449
+ try {
32450
+ cacheRecursivePaths(dir, await loader(dir));
32451
+ } catch {
32452
+ cacheRecursivePaths(dir, []);
32453
+ }
32454
+ return true;
32455
+ })().finally(() => recursiveSearches.delete(dir));
32456
+ recursiveSearches.set(dir, search);
32457
+ return search;
32458
+ }
32459
+ function activeAtToken(lineText, col) {
32460
+ const upto = lineText.slice(0, col);
32461
+ const match = /(?:^|\s)@([^\s]*)$/.exec(upto);
32462
+ if (!match)
32463
+ return null;
32464
+ const frag = match[1] ?? "";
32465
+ return { start: col - frag.length - 1, frag };
32466
+ }
32467
+ function resolveFragment(frag) {
32468
+ const home = homedir10();
32469
+ let path;
32470
+ if (frag === "" || frag === "~")
32471
+ path = home;
32472
+ else if (frag === "~/")
32473
+ path = home;
32474
+ else if (frag.startsWith("~/"))
32475
+ path = join40(home, frag.slice(2));
32476
+ else if (frag.startsWith("/"))
32477
+ path = frag;
32478
+ else
32479
+ path = join40(home, frag);
32480
+ if (frag.endsWith("/") || frag === "" || frag === "~") {
32481
+ return { dir: path, prefix: "" };
32482
+ }
32483
+ return { dir: dirname19(path), prefix: basename11(path) };
32484
+ }
32485
+ function toToken(fullPath, isDir) {
32486
+ const home = homedir10();
32487
+ let shown = fullPath;
32488
+ if (fullPath === home)
32489
+ shown = "~";
32490
+ else if (fullPath.startsWith(`${home}/`))
32491
+ shown = `~/${fullPath.slice(home.length + 1)}`;
32492
+ return `@${shown}${isDir ? "/" : ""}`;
32493
+ }
32494
+ function rankAndSlice(dir, candidates) {
32495
+ candidates.sort((a, b) => {
32496
+ if (a.isPrefixMatch !== b.isPrefixMatch)
32497
+ return a.isPrefixMatch ? -1 : 1;
32498
+ if (a.isDir !== b.isDir)
32499
+ return a.isDir ? -1 : 1;
32500
+ const depthA = a.relPath.split("/").length;
32501
+ const depthB = b.relPath.split("/").length;
32502
+ if (depthA !== depthB)
32503
+ return depthA - depthB;
32504
+ return a.relPath.localeCompare(b.relPath);
32505
+ });
32506
+ return candidates.slice(0, MAX_SUGGESTIONS).map(({ relPath, isDir }) => ({
32507
+ label: `${relPath}${isDir ? "/" : ""}`,
32508
+ value: toToken(join40(dir, relPath), isDir),
32509
+ isDir
32510
+ }));
32511
+ }
32512
+ function recursiveCandidates(dir, lowerPrefix, seen) {
32513
+ const entry = recursiveCache.get(dir);
32514
+ if (!entry)
32515
+ return [];
32516
+ let matches = entry.matches.get(lowerPrefix);
32517
+ if (!matches) {
32518
+ matches = entry.paths.filter((path) => path.lowerName.includes(lowerPrefix)).map((path) => ({
32519
+ relPath: path.relPath,
32520
+ isDir: path.isDir,
32521
+ isPrefixMatch: path.lowerName.startsWith(lowerPrefix)
32522
+ }));
32523
+ if (entry.matches.size >= MAX_MATCH_CACHE_ENTRIES) {
32524
+ const oldest = entry.matches.keys().next().value;
32525
+ if (oldest !== undefined)
32526
+ entry.matches.delete(oldest);
32527
+ }
32528
+ entry.matches.set(lowerPrefix, matches);
32529
+ } else {
32530
+ entry.matches.delete(lowerPrefix);
32531
+ entry.matches.set(lowerPrefix, matches);
32532
+ }
32533
+ return matches.filter((candidate) => {
32534
+ if (seen.has(candidate.relPath))
32535
+ return false;
32536
+ seen.add(candidate.relPath);
32537
+ return true;
32538
+ });
32539
+ }
32540
+ function isRecursivePathQuery(lineText, col) {
32541
+ const token = activeAtToken(lineText, col);
32542
+ if (!token)
32543
+ return false;
32544
+ const { prefix } = resolveFragment(token.frag);
32545
+ return !prefix.startsWith(".") && prefix.length >= RECURSIVE_MIN_PREFIX_LEN;
32546
+ }
32547
+ async function warmPathSuggestions(lineText, col, loader = listFilesWithRipgrep) {
32548
+ if (!isRecursivePathQuery(lineText, col))
32549
+ return false;
32550
+ const token = activeAtToken(lineText, col);
32551
+ if (!token)
32552
+ return false;
32553
+ const { dir } = resolveFragment(token.frag);
32554
+ try {
32555
+ if (!statSync19(dir).isDirectory())
32556
+ return false;
32557
+ } catch {
32558
+ return false;
32559
+ }
32560
+ return loadFilesRecursive(dir, loader);
32561
+ }
32562
+ function pathSuggestions(lineText, col) {
32563
+ const token = activeAtToken(lineText, col);
32564
+ if (!token)
32565
+ return null;
32566
+ const { dir, prefix } = resolveFragment(token.frag);
32567
+ let entries;
32568
+ try {
32569
+ entries = readdirSync14(dir, { withFileTypes: true });
32570
+ } catch {
32571
+ return { start: token.start, end: col, items: [], truncated: 0, searching: false };
32572
+ }
32573
+ const lowerPrefix = prefix.toLowerCase();
32574
+ const showHidden = prefix.startsWith(".");
32575
+ const candidates = entries.filter((entry) => {
32576
+ const name = entry.name;
32577
+ if (!showHidden && name.startsWith("."))
32578
+ return false;
32579
+ if (lowerPrefix === "")
32580
+ return true;
32581
+ const lowerName = name.toLowerCase();
32582
+ return showHidden ? lowerName.startsWith(lowerPrefix) : lowerName.includes(lowerPrefix);
32583
+ }).map((entry) => {
32584
+ let isDir = entry.isDirectory();
32585
+ if (!isDir && entry.isSymbolicLink()) {
32586
+ try {
32587
+ isDir = statSync19(join40(dir, entry.name)).isDirectory();
32588
+ } catch {}
32589
+ }
32590
+ return {
32591
+ relPath: entry.name,
32592
+ isDir,
32593
+ isPrefixMatch: entry.name.toLowerCase().startsWith(lowerPrefix)
32594
+ };
32595
+ });
32596
+ if (!showHidden && lowerPrefix.length >= RECURSIVE_MIN_PREFIX_LEN) {
32597
+ const seen = new Set(candidates.map((c) => c.relPath));
32598
+ candidates.push(...recursiveCandidates(dir, lowerPrefix, seen));
32599
+ }
32600
+ const items = rankAndSlice(dir, candidates);
32601
+ return {
32602
+ start: token.start,
32603
+ end: col,
32604
+ items,
32605
+ truncated: Math.max(0, candidates.length - items.length),
32606
+ searching: recursiveSearches.has(dir)
32607
+ };
32608
+ }
32609
+ function fragmentToAbsolutePath(frag) {
32610
+ const home = homedir10();
32611
+ if (frag === "~" || frag === "~/")
32612
+ return home;
32613
+ if (frag.startsWith("~/"))
32614
+ return join40(home, frag.slice(2));
32615
+ if (frag.startsWith("/"))
32616
+ return frag;
32617
+ return join40(home, frag);
32618
+ }
32619
+ function fragmentResolves(frag) {
32620
+ try {
32621
+ return existsSync33(fragmentToAbsolutePath(frag));
32622
+ } catch {
32623
+ return false;
32624
+ }
32625
+ }
32626
+ function stripResolvedPathTriggers(text2) {
32627
+ return text2.replaceAll(AT_PATH_TOKEN, (whole, lead, frag) => {
32628
+ if (fragmentResolves(frag))
32629
+ return `${lead}${frag}`;
32630
+ const pathPart = frag.replace(TRAILING_PUNCT, "");
32631
+ if (pathPart && pathPart !== frag && fragmentResolves(pathPart)) {
32632
+ return `${lead}${pathPart}${frag.slice(pathPart.length)}`;
32633
+ }
32634
+ return whole;
32635
+ });
32636
+ }
32637
+ function completePathToken(lineText, col, suggestion, options = {}) {
32638
+ const token = activeAtToken(lineText, col);
32639
+ if (!token)
32640
+ return null;
32641
+ let tokenEnd = col;
32642
+ while (tokenEnd < lineText.length && !/\s/.test(lineText[tokenEnd] ?? " "))
32643
+ tokenEnd += 1;
32644
+ const trailingText = lineText.slice(col, tokenEnd);
32645
+ const pathTail = trailingText.replace(TRAILING_PUNCT, "");
32646
+ const end = col + pathTail.length;
32647
+ const replacement = options.keepTrigger ? suggestion.value : suggestion.value.slice(1);
32648
+ const line = lineText.slice(0, token.start) + replacement + lineText.slice(end);
32649
+ return { line, col: token.start + replacement.length };
32650
+ }
32651
+ var MAX_SUGGESTIONS = 8, RECURSIVE_MIN_PREFIX_LEN = 4, RECURSIVE_MAX_DEPTH = 10, RECURSIVE_TIMEOUT_MS = 2000, RECURSIVE_CACHE_TTL_MS = 30000, MAX_RECURSIVE_CACHE_ENTRIES = 3, MAX_MATCH_CACHE_ENTRIES = 24, RECURSIVE_EXCLUDES, recursiveCache, recursiveSearches, execFileAsync3, AT_PATH_TOKEN, TRAILING_PUNCT;
32652
+ var init_path_suggest = __esm(() => {
32653
+ RECURSIVE_EXCLUDES = ["node_modules", ".git", ".cache", "dist", "build", "Library", ".Trash"];
32654
+ recursiveCache = new Map;
32655
+ recursiveSearches = new Map;
32656
+ execFileAsync3 = promisify3(execFile3);
32657
+ AT_PATH_TOKEN = /(^|[\s([{<])@([^\s]+)/g;
32658
+ TRAILING_PUNCT = /[.,;:!?)\]}>"'`]+$/;
32659
+ });
32660
+
31983
32661
  // ../../adapters/terminal/src/terminal-width.ts
31984
32662
  function stripAnsi(value) {
31985
32663
  return value.replace(new RegExp("\\u001b\\]8;[^\\u0007\\u001b]*(?:\\u0007|\\u001b\\\\)", "g"), "").replace(new RegExp("\\u001b\\[[0-?]*[ -/]*[@-~]", "g"), "");
@@ -32811,9 +33489,9 @@ function helpLines() {
32811
33489
  line(" Esc/Ctrl-C stop active turn \xB7 Ctrl-C twice when idle to quit"),
32812
33490
  line(""),
32813
33491
  line(" Commands", { fg: theme.cyan, bold: true }),
32814
- line(" /new /model /effort /topics /public /private"),
32815
- line(" /fork /spawn /del /copy"),
32816
- line(" /abort /help /quit", { fg: theme.muted })
33492
+ line(" /new /model /effort /status /context"),
33493
+ line(" /topics /public /private /fork /spawn"),
33494
+ line(" /del /copy /abort /help /quit", { fg: theme.muted })
32817
33495
  ];
32818
33496
  }
32819
33497
  function tokenCount(value) {
@@ -32828,19 +33506,17 @@ function tokenCount(value) {
32828
33506
  function latestActiveUsage(state) {
32829
33507
  return activeMessages(state).slice().reverse().find((message) => message.authorId === "ai" && message.usage)?.usage;
32830
33508
  }
32831
- function activeContextUsage(state) {
32832
- const usage = latestActiveUsage(state);
32833
- if (usage?.context !== undefined && usage.contextWindow) {
32834
- return { context: usage.context, contextWindow: usage.contextWindow };
32835
- }
32836
- const topic = activeTopic(state);
32837
- const current3 = topic ? state.topicUsage[topic.id]?.currentSession : undefined;
32838
- return current3 ? { context: current3.contextTokens, contextWindow: current3.contextWindow } : undefined;
33509
+ function contextUsageColor(ratio) {
33510
+ if (ratio !== undefined && ratio >= 90)
33511
+ return theme.red;
33512
+ if (ratio !== undefined && ratio >= 80)
33513
+ return theme.amber;
33514
+ return theme.text;
32839
33515
  }
32840
33516
  function statusLines(state) {
32841
33517
  const topic = activeTopic(state);
32842
33518
  const usage = latestActiveUsage(state);
32843
- const contextUsage = activeContextUsage(state);
33519
+ const contextUsage = activeContextBreakdown(state);
32844
33520
  const total = topic ? state.topicUsage[topic.id] : undefined;
32845
33521
  const hasTopicUsage = total !== undefined && total.queries > 0;
32846
33522
  const ratio = contextUsage?.context !== undefined && contextUsage.contextWindow ? Math.round(contextUsage.context / contextUsage.contextWindow * 100) : undefined;
@@ -32852,8 +33528,8 @@ function statusLines(state) {
32852
33528
  line(` Model ${effectiveTopicModel(topic)}`),
32853
33529
  line(` Effort ${topic?.effectiveEffort ?? topic?.defaultEffort ?? "-"}`),
32854
33530
  line(""),
32855
- line(` Context ${tokenCount(contextUsage?.context)} / ${tokenCount(contextUsage?.contextWindow)}${ratio === undefined ? "" : ` (${ratio}%)`}`, { fg: ratio !== undefined && ratio >= 80 ? theme.amber : theme.text }),
32856
- line(" Measured on the latest model request", { fg: theme.muted, dim: true }),
33531
+ line(` Context ${contextUsage?.estimated ? "~" : ""}${tokenCount(contextUsage?.context)} / ${tokenCount(contextUsage?.contextWindow)}${ratio === undefined ? "" : ` (${ratio}%)`}`, { fg: contextUsageColor(ratio) }),
33532
+ line(contextUsage?.estimated ? " Live estimate; replaced by provider usage when the turn finishes" : " Measured on the latest model request", { fg: theme.muted, dim: true }),
32857
33533
  line(""),
32858
33534
  line(` Last turn input ${tokenCount(usage?.input)} \xB7 output ${tokenCount(usage?.output)}`),
32859
33535
  line(` Cache read ${tokenCount(usage?.cachedInput)}`),
@@ -32873,6 +33549,31 @@ function statusLines(state) {
32873
33549
  line(" Esc close", { fg: theme.muted })
32874
33550
  ];
32875
33551
  }
33552
+ function contextLines(state) {
33553
+ const usage = activeContextBreakdown(state);
33554
+ const ratio = usage ? Math.round(usage.context / usage.contextWindow * 100) : undefined;
33555
+ const value = (tokens) => tokenCount(tokens);
33556
+ return [
33557
+ line(" Context", { fg: theme.accent, bold: true }),
33558
+ line(""),
33559
+ line(` ${usage?.estimated ? "Estimated" : "Current"} ${usage?.estimated ? "~" : ""}${value(usage?.context)} / ${value(usage?.contextWindow)}${ratio === undefined ? "" : ` (${ratio}%)`}`, { fg: contextUsageColor(ratio) }),
33560
+ line(""),
33561
+ line(" Breakdown", { fg: theme.cyan, bold: true }),
33562
+ line(` Confirmed ${value(usage?.confirmed)}`),
33563
+ line(` New user ${value(usage?.user)}`),
33564
+ line(` Assistant ${value(usage?.assistant)}`),
33565
+ line(` Tools ${value(usage?.tools)}`),
33566
+ line(` Free ${value(usage?.free)}`),
33567
+ line(""),
33568
+ line(usage?.estimated ? " Live estimate; final provider usage replaces it" : " Provider-confirmed latest request; live categories appear during a turn", { fg: theme.muted, dim: true }),
33569
+ line(" System prompt and tool schemas are included in Confirmed", {
33570
+ fg: theme.muted,
33571
+ dim: true
33572
+ }),
33573
+ line(""),
33574
+ line(" Esc close", { fg: theme.muted })
33575
+ ];
33576
+ }
32876
33577
  function subagentTreePrefix(topic, topics, rootTopicId) {
32877
33578
  if (!topic.isSubagent || topic.id === rootTopicId)
32878
33579
  return "";
@@ -33241,6 +33942,8 @@ function conversationLines(state, width, height, animationFrame = 0, nowMs = ter
33241
33942
  return helpLines().slice(0, height);
33242
33943
  if (state.overlay === "status")
33243
33944
  return statusLines(state).slice(0, height);
33945
+ if (state.overlay === "context")
33946
+ return contextLines(state).slice(0, height);
33244
33947
  if (state.overlay === "topics")
33245
33948
  return topicOverlayLines(state, width, height, animationFrame).slice(0, height);
33246
33949
  if (state.overlay === "subagents")
@@ -33461,35 +34164,71 @@ function footerStatusText(state) {
33461
34164
  function footerUsageText(state) {
33462
34165
  const topic = activeTopic(state);
33463
34166
  if (!topic)
33464
- return [""];
33465
- const contextUsage = activeContextUsage(state);
34167
+ return [{ text: "", spans: [] }];
34168
+ const contextUsage = activeContextBreakdown(state);
33466
34169
  const total = state.topicUsage[topic.id];
33467
34170
  const ratio = contextUsage?.context !== undefined && contextUsage.contextWindow ? Math.round(contextUsage.context / contextUsage.contextWindow * 100) : undefined;
33468
- const context = contextUsage?.context !== undefined && contextUsage.contextWindow ? `${tokenCount(contextUsage.context)}/${tokenCount(contextUsage.contextWindow)} ${ratio}%` : "";
34171
+ const context = contextUsage?.context !== undefined && contextUsage.contextWindow ? `${contextUsage.estimated ? "~" : ""}${tokenCount(contextUsage.context)}/${tokenCount(contextUsage.contextWindow)} ${ratio}%` : "";
33469
34172
  const hasTopicUsage = total !== undefined && total.queries > 0;
33470
34173
  const cumulative = hasTopicUsage ? `\u03A3 ${tokenCount(total.inputTokens)} in/${tokenCount(total.outputTokens)} out` : "";
33471
34174
  const cache = hasTopicUsage && total.cacheReadInputTokens ? `cache ${tokenCount(total.cacheReadInputTokens)}` : "";
33472
34175
  const cost = hasTopicUsage ? `est $${total.estimatedCostUsd.toFixed(2)}` : "";
33473
- const parts = [context, cumulative, cache, cost].filter(Boolean);
34176
+ const variant = (values2) => {
34177
+ const present = values2.filter((value) => value.text);
34178
+ const spans = present.flatMap((value, index) => [
34179
+ ...index > 0 ? [{ text: " \xB7 ", fg: theme.subtle }] : [],
34180
+ { text: value.text, fg: value.fg }
34181
+ ]);
34182
+ if (spans.length > 0)
34183
+ spans.push({ text: " ", fg: theme.muted });
34184
+ return {
34185
+ text: `${present.map((value) => value.text).join(" \xB7 ")}${present.length ? " " : ""}`,
34186
+ spans
34187
+ };
34188
+ };
34189
+ const values = {
34190
+ context: { text: context, fg: contextUsageColor(ratio) },
34191
+ cumulative: { text: cumulative, fg: theme.muted },
34192
+ cache: { text: cache, fg: theme.muted },
34193
+ cost: { text: cost, fg: theme.muted }
34194
+ };
33474
34195
  return [
33475
- `${parts.join(" \xB7 ")} `,
33476
- `${[context, cumulative, cache].filter(Boolean).join(" \xB7 ")} `,
33477
- `${[context, cumulative].filter(Boolean).join(" \xB7 ")} `,
33478
- context ? `${context} ` : "",
33479
- ""
34196
+ variant([values.context, values.cumulative, values.cache, values.cost]),
34197
+ variant([values.context, values.cumulative, values.cache]),
34198
+ variant([values.context, values.cumulative]),
34199
+ variant([values.context]),
34200
+ variant([])
33480
34201
  ];
33481
34202
  }
33482
34203
  function footerStatusLine(state, width) {
33483
34204
  const leftCandidates = footerStatusText(state);
33484
34205
  const rightCandidates = footerUsageText(state);
33485
34206
  for (const right of rightCandidates) {
33486
- for (const left of leftCandidates) {
33487
- if (displayWidth(left) + (right ? 1 + displayWidth(right) : 0) <= width) {
33488
- return joinSides(left, right, width);
34207
+ for (const left2 of leftCandidates) {
34208
+ const rightWidth = displayWidth(right.text);
34209
+ if (displayWidth(left2) + (rightWidth ? 1 + rightWidth : 0) <= width) {
34210
+ const gap = width - displayWidth(left2) - rightWidth;
34211
+ return [
34212
+ { text: left2, fg: theme.accent, bold: true },
34213
+ { text: " ".repeat(gap), bg: theme.surfaceRaised },
34214
+ ...right.spans
34215
+ ];
33489
34216
  }
33490
34217
  }
33491
34218
  }
33492
- return joinSides(leftCandidates[leftCandidates.length - 1] ?? "", "", width);
34219
+ const left = sliceWidth(leftCandidates[leftCandidates.length - 1] ?? "", width);
34220
+ return [
34221
+ { text: left, fg: theme.accent, bold: true },
34222
+ { text: " ".repeat(Math.max(0, width - displayWidth(left))), bg: theme.surfaceRaised }
34223
+ ];
34224
+ }
34225
+ function paintFooterSpans(spans) {
34226
+ return spans.map((span) => paint(span.text, {
34227
+ fg: span.fg ?? theme.text,
34228
+ bg: span.bg ?? theme.surfaceRaised,
34229
+ bold: span.bold,
34230
+ dim: span.dim
34231
+ })).join("");
33493
34232
  }
33494
34233
  function noticeStyle(state) {
33495
34234
  return NOTICE_STYLE[state.noticeLevel ?? "warn"];
@@ -33540,11 +34279,7 @@ function footerLines(state, width) {
33540
34279
  ];
33541
34280
  }
33542
34281
  return [
33543
- paint(footerStatusLine(state, width), {
33544
- fg: theme.accent,
33545
- bg: theme.surfaceRaised,
33546
- bold: true
33547
- }),
34282
+ paintFooterSpans(footerStatusLine(state, width)),
33548
34283
  paint(joinSides("", pickFitting(footerHintText(state), width), width), {
33549
34284
  fg: state.notice ? noticeStyle(state).fg : theme.muted,
33550
34285
  bg: theme.canvas
@@ -33679,6 +34414,7 @@ var init_render = __esm(async () => {
33679
34414
  init_terminal_width();
33680
34415
  await __promiseAll([
33681
34416
  init_src(),
34417
+ init_context_usage(),
33682
34418
  init_state2()
33683
34419
  ]);
33684
34420
  theme = {
@@ -34821,6 +35557,11 @@ async function runTerminalCommand(commandLine, context) {
34821
35557
  context.queueRender();
34822
35558
  return;
34823
35559
  }
35560
+ if (command === "context") {
35561
+ context.state = { ...context.state, overlay: "context" };
35562
+ context.queueRender();
35563
+ return;
35564
+ }
34824
35565
  if (command === "model") {
34825
35566
  if (args.length > 0) {
34826
35567
  context.state = { ...context.state, notice: "Usage: /model", noticeLevel: "warn" };
@@ -40784,6 +41525,7 @@ var init_canonical_mcp_bridge = __esm(async () => {
40784
41525
  task: new Set(["task_create", "task_update", "task_list", "task_get", "task_delete"]),
40785
41526
  wiki: new Set([
40786
41527
  "wiki_query",
41528
+ "wiki_read",
40787
41529
  "wiki_topic_brief",
40788
41530
  "wiki_last_conversation",
40789
41531
  "save_wiki_entry",
@@ -45650,4 +46392,4 @@ switch (command) {
45650
46392
  }
45651
46393
  }
45652
46394
 
45653
- //# debugId=5D6FE2D4C44E041364756E2164756E21
46395
+ //# debugId=9061330203CC009264756E2164756E21