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
@@ -952,7 +952,7 @@ var init_claude_registry = __esm(() => {
952
952
  });
953
953
 
954
954
  // ../../packages/core/src/version.ts
955
- var NEGOTIUM_VERSION = "0.2.16";
955
+ var NEGOTIUM_VERSION = "0.2.18";
956
956
 
957
957
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
958
958
  import { spawn as spawn2 } from "child_process";
@@ -2128,6 +2128,8 @@ var init_mcp_config = __esm(() => {
2128
2128
  return env ? buildStdioMcpServer(agent, CANONICAL_MCP_PROXY_SERVER, ["--surface=wiki"], env) : null;
2129
2129
  }
2130
2130
  const args = [`--user-id=${userId}`];
2131
+ if (topicId)
2132
+ args.push(`--current-topic-id=${topicId}`);
2131
2133
  const resolvedWikiTopicId = wikiTopicId ?? topicId ?? (session !== "dm" ? session : undefined);
2132
2134
  if (resolvedWikiTopicId)
2133
2135
  args.push(`--topic-id=${resolvedWikiTopicId}`);
@@ -4140,6 +4142,7 @@ function initializeApiTopicsSchema() {
4140
4142
  last_message_at TEXT,
4141
4143
  parent_topic_id TEXT,
4142
4144
  memory_topic_id TEXT,
4145
+ memory_key TEXT,
4143
4146
  is_fork INTEGER NOT NULL DEFAULT 0 CHECK (is_fork IN (0,1)),
4144
4147
  is_subagent INTEGER NOT NULL DEFAULT 0 CHECK (is_subagent IN (0,1)),
4145
4148
  visibility TEXT NOT NULL DEFAULT 'visible' CHECK (visibility IN ('visible','hidden')),
@@ -4189,8 +4192,8 @@ function initializeApiTopicsSchema() {
4189
4192
  const legacyBaseEffort = row.base_effort ?? row.default_effort;
4190
4193
  db.query(`INSERT INTO api_topics_next
4191
4194
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
4192
- created_at,last_message_at,parent_topic_id,memory_topic_id,is_fork,is_subagent,visibility,access_mode,session_id)
4193
- 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);
4195
+ created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,access_mode,session_id)
4196
+ 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);
4194
4197
  }
4195
4198
  db.exec("DROP TABLE IF EXISTS topic_members");
4196
4199
  db.exec("DROP TABLE api_topics");
@@ -4259,6 +4262,9 @@ function initializeApiTopicsSchema() {
4259
4262
  if (!tableColumns("api_topics").has("memory_topic_id")) {
4260
4263
  db.exec("ALTER TABLE api_topics ADD COLUMN memory_topic_id TEXT");
4261
4264
  }
4265
+ if (!tableColumns("api_topics").has("memory_key")) {
4266
+ db.exec("ALTER TABLE api_topics ADD COLUMN memory_key TEXT");
4267
+ }
4262
4268
  db.exec(`
4263
4269
  UPDATE api_topics
4264
4270
  SET browser_profile_owner = (
@@ -4319,6 +4325,7 @@ function rowToDto2(r, participants = getTopicParticipants(r.id), tellTargets) {
4319
4325
  lastMessageAt: r.last_message_at ?? new Date().toISOString(),
4320
4326
  parentTopicId: r.parent_topic_id ?? undefined,
4321
4327
  memoryTopicId: r.memory_topic_id ?? undefined,
4328
+ memoryKey: r.memory_key ?? undefined,
4322
4329
  isFork: r.is_fork !== 0,
4323
4330
  ...r.is_subagent !== 0 ? { isSubagent: true } : {},
4324
4331
  ...r.is_subagent !== 0 ? {
@@ -4390,9 +4397,9 @@ function upsertTopic(t) {
4390
4397
  db.transaction(() => {
4391
4398
  db.query(`INSERT INTO api_topics
4392
4399
  (id,title,kind,description,agent,base_model,base_effort,response_policy,
4393
- created_at,last_message_at,parent_topic_id,memory_topic_id,is_fork,is_subagent,visibility,access_mode,
4400
+ created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,access_mode,
4394
4401
  subagent_report_mode)
4395
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
4402
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
4396
4403
  ON CONFLICT(id) DO UPDATE SET
4397
4404
  title = excluded.title,
4398
4405
  kind = excluded.kind,
@@ -4405,11 +4412,12 @@ function upsertTopic(t) {
4405
4412
  last_message_at = excluded.last_message_at,
4406
4413
  parent_topic_id = excluded.parent_topic_id,
4407
4414
  memory_topic_id = excluded.memory_topic_id,
4415
+ memory_key = excluded.memory_key,
4408
4416
  is_fork = excluded.is_fork,
4409
4417
  is_subagent = excluded.is_subagent,
4410
4418
  visibility = excluded.visibility,
4411
4419
  access_mode = excluded.access_mode,
4412
- 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");
4420
+ 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");
4413
4421
  db.query("DELETE FROM topic_members WHERE topic_id = ?").run(t.id);
4414
4422
  for (const participant of t.participants) {
4415
4423
  db.query("INSERT INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)").run(t.id, participant.userId, participant.role);
@@ -9006,7 +9014,7 @@ __export(exports_codex_provider, {
9006
9014
  import { execFileSync as execFileSync6 } from "child_process";
9007
9015
  import { existsSync as existsSync15, readFileSync as readFileSync14, realpathSync as realpathSync4, statSync as statSync6 } from "fs";
9008
9016
  import { homedir as homedir6 } from "os";
9009
- import { dirname as dirname11, isAbsolute as isAbsolute3, join as join16, relative, resolve as resolve12 } from "path";
9017
+ import { dirname as dirname11, isAbsolute as isAbsolute3, join as join16, relative as relative2, resolve as resolve12 } from "path";
9010
9018
  import { Codex } from "@openai/codex-sdk";
9011
9019
  function sameCodexUsage(usage, total) {
9012
9020
  return usage.input_tokens === total.inputTokens && usage.output_tokens === total.outputTokens && (usage.cached_input_tokens ?? 0) === total.cachedInputTokens && (usage.cache_write_input_tokens ?? 0) === total.cacheWriteInputTokens;
@@ -9190,7 +9198,7 @@ class CodexFilePreviewTracker {
9190
9198
  if (!this.#root || !this.#baselineAvailable || !succeeded)
9191
9199
  return {};
9192
9200
  const absolute = canonicalPath(isAbsolute3(path) ? path : resolve12(this.#cwd, path));
9193
- const relativePath = relative(this.#root, absolute);
9201
+ const relativePath = relative2(this.#root, absolute);
9194
9202
  if (!relativePath || relativePath.startsWith("..") || isAbsolute3(relativePath))
9195
9203
  return {};
9196
9204
  const before = this.#baseline.has(relativePath) ? this.#baseline.get(relativePath) : this.#headFile(relativePath);
@@ -9787,6 +9795,7 @@ function maestroProvider(opts) {
9787
9795
  ...opts,
9788
9796
  enableToolSearch: !opts.toolPolicy && opts.enableToolSearch !== false,
9789
9797
  toolResultTruncation: buildMaestroToolResultTruncation(opts),
9798
+ ephemeralSystemPrompt: opts.ephemeralSystemPrompt,
9790
9799
  apiKeyOverrides: resolveMaestroApiKeyOverrides(userId),
9791
9800
  agent: "maestro",
9792
9801
  disallowedTools: buildMaestroDisallowedTools(callerDisallowedTools, opts.toolPolicy),
@@ -10281,14 +10290,14 @@ function buildMemoryPromptSection(opts) {
10281
10290
  const parts = [`
10282
10291
 
10283
10292
  ## Memory`];
10284
- parts.push(`Memory directory: ${opts.wikiDir}/topic`);
10285
- parts.push(`Files: ${opts.hasFiles ? opts.briefFile : "(none)"}`);
10286
- if (opts.latestSummaryFile) {
10287
- parts.push(`Latest summary: ${opts.latestSummaryFile}`);
10288
- }
10289
- 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.");
10290
- 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.");
10291
- 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.");
10293
+ if (opts.isManager) {
10294
+ parts.push("Use `wiki_query` for past workspace decisions or cross-topic context, then `wiki_read` only for the relevant result.");
10295
+ } else if (opts.memoryKey) {
10296
+ 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}")\`.`);
10297
+ } else {
10298
+ 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.");
10299
+ }
10300
+ 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.');
10292
10301
  if (opts.hasArchive) {
10293
10302
  parts.push("", "If you need the actual conversation from an earlier session, use the `wiki_last_conversation` MCP tool.");
10294
10303
  }
@@ -11244,11 +11253,12 @@ function archiveActiveTopicForMemory(topicId, userId, options) {
11244
11253
  }, "topic-memory-archiver: raw snapshot preserved below exchange threshold");
11245
11254
  return "below-threshold";
11246
11255
  }
11247
- const memoryTopic = getTopicMemoryOrigin(topicId) ?? topic;
11256
+ const memoryTopic = topic.memoryKey ? topic : getTopicMemoryOrigin(topicId) ?? topic;
11257
+ const memoryTitle = memoryTopic.memoryKey?.trim() || memoryTopic.title;
11248
11258
  const launched = (options.launchArchiver ?? runArchiverTurn)({
11249
11259
  userId,
11250
11260
  topicId: memoryTopic.id,
11251
- topicTitle: memoryTopic.title,
11261
+ topicTitle: memoryTitle,
11252
11262
  archivePath: job.archivePath,
11253
11263
  ...options.rawArchivePaths ? { rawArchivePaths: options.rawArchivePaths } : {},
11254
11264
  messageCount: job.messageCount,
@@ -13356,11 +13366,12 @@ async function deleteTopicCascadeImpl(topic, userId, options, deletingAncestorId
13356
13366
  }
13357
13367
  if (archived && meetsMemoryArchiveExchangeThreshold(archived.exchangeCount, MIN_MEMORY_ARCHIVE_EXCHANGES, topic)) {
13358
13368
  try {
13359
- const memoryTopic = getTopicMemoryOrigin(topicId) ?? topic;
13369
+ const memoryTopic = topic.memoryKey ? topic : getTopicMemoryOrigin(topicId) ?? topic;
13370
+ const memoryTitle = memoryTopic.memoryKey?.trim() || memoryTopic.title;
13360
13371
  runArchiverTurn({
13361
13372
  userId,
13362
13373
  ...memoryTopic.id !== topicId && !deletingTopicIds.has(memoryTopic.id) ? { topicId: memoryTopic.id } : {},
13363
- topicTitle: memoryTopic.title,
13374
+ topicTitle: memoryTitle,
13364
13375
  archivePath: archived.path,
13365
13376
  ...rawArchives.length > 0 ? { rawArchivePaths: rawArchives } : {},
13366
13377
  messageCount: archived.messageCount
@@ -14883,6 +14894,9 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
14883
14894
  let pendingSawDelta = false;
14884
14895
  let accumulatedText = "";
14885
14896
  let pendingText = "";
14897
+ let assistantContextTokens = 0;
14898
+ let toolContextTokens = 0;
14899
+ let lastContextProgressAt = 0;
14886
14900
  let lastVisibleMessageId = null;
14887
14901
  const visibleMessageIds = [];
14888
14902
  let lastTaskPanelText = null;
@@ -14901,6 +14915,20 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
14901
14915
  model
14902
14916
  });
14903
14917
  };
14918
+ const broadcastContextProgress = (force = false) => {
14919
+ if (silent)
14920
+ return;
14921
+ const now = Date.now();
14922
+ if (!force && lastContextProgressAt > 0 && now - lastContextProgressAt < 250)
14923
+ return;
14924
+ lastContextProgressAt = now;
14925
+ hub.broadcastAiStatus(topicId, {
14926
+ kind: "context_progress",
14927
+ queryId,
14928
+ assistantTokens: assistantContextTokens,
14929
+ toolTokens: toolContextTokens
14930
+ });
14931
+ };
14904
14932
  const nextSyntheticToolUseId = () => `tool-${queryId}-${++syntheticToolCounter}`;
14905
14933
  const bindToolUseId = (providerToolUseId, fallback) => {
14906
14934
  const providerId = normalizeToolUseId(providerToolUseId);
@@ -15010,6 +15038,8 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
15010
15038
  const incoming = event.content;
15011
15039
  accumulatedText += incoming;
15012
15040
  pendingText += incoming;
15041
+ assistantContextTokens = estimateTextTokens(accumulatedText);
15042
+ broadcastContextProgress();
15013
15043
  break;
15014
15044
  }
15015
15045
  case "text":
@@ -15017,10 +15047,15 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
15017
15047
  const incoming = event.content;
15018
15048
  accumulatedText += incoming;
15019
15049
  pendingText += incoming;
15050
+ assistantContextTokens = estimateTextTokens(accumulatedText);
15051
+ broadcastContextProgress();
15020
15052
  }
15021
15053
  break;
15022
15054
  case "tool_use":
15023
15055
  emitPendingAssistantMessage();
15056
+ toolContextTokens += estimateTextTokens(`${event.name}
15057
+ ${JSON.stringify(event.input ?? {})}`);
15058
+ broadcastContextProgress(true);
15024
15059
  if (isVisualsShowHtmlTool(event.name)) {
15025
15060
  const input = event.input;
15026
15061
  if (typeof input.html !== "string" || input.html.trim().length === 0) {
@@ -15174,6 +15209,8 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
15174
15209
  }
15175
15210
  break;
15176
15211
  case "tool_result":
15212
+ toolContextTokens += estimateTextTokens(event.content);
15213
+ broadcastContextProgress(true);
15177
15214
  if (isVisualToolResultHandled(event.toolUseId))
15178
15215
  break;
15179
15216
  if (!silent) {
@@ -15559,6 +15596,7 @@ __export(exports_turn_runner, {
15559
15596
  channelTranscriptSpeaker: () => channelTranscriptSpeaker,
15560
15597
  canonicalModelId: () => canonicalModelId,
15561
15598
  buildVideoHtml: () => buildVideoHtml,
15599
+ buildTurnReminderQuery: () => buildTurnReminderQuery,
15562
15600
  buildMermaidHtml: () => buildMermaidHtml,
15563
15601
  buildMentionOnlyChannelPrompt: () => buildMentionOnlyChannelPrompt,
15564
15602
  buildImageHtml: () => buildImageHtml,
@@ -15584,6 +15622,18 @@ function withDefaultPlaywright(configuredMcp, isManager) {
15584
15622
  enabled.add("background-bash");
15585
15623
  return [...enabled];
15586
15624
  }
15625
+ function buildTurnReminderQuery(agent, prompt, reminders) {
15626
+ const reminderPrompt = reminders.join(`
15627
+
15628
+ `);
15629
+ if (!reminderPrompt)
15630
+ return { prompt };
15631
+ if (agent === "maestro")
15632
+ return { prompt, ephemeralSystemPrompt: reminderPrompt };
15633
+ return { prompt: `${prompt}
15634
+
15635
+ ${reminderPrompt}` };
15636
+ }
15587
15637
  function appendSystemMessage(topicId, text2) {
15588
15638
  const message = {
15589
15639
  id: randomUUID16(),
@@ -16399,25 +16449,24 @@ function startAiTurn(params) {
16399
16449
  const isManager = topicRecord?.kind === "manager";
16400
16450
  const isMentionOnlyChannel = topic.aiMode === "mention" && !isManager;
16401
16451
  let systemPrompt = isManager ? buildManagerSystemPrompt(systemPromptOpts) : isMentionOnlyChannel ? buildChannelSystemPrompt(systemPromptOpts) : buildTopicSystemPrompt(systemPromptOpts);
16402
- const memoryTopic = isMentionOnlyChannel || isManager ? topic : getTopicMemoryOrigin(topicId) ?? topic;
16452
+ const memoryTopic = isMentionOnlyChannel || isManager || topic.memoryKey ? topic : getTopicMemoryOrigin(topicId) ?? topic;
16453
+ const memoryTitle = memoryTopic.memoryKey?.trim() || memoryTopic.title;
16403
16454
  if (!isMentionOnlyChannel) {
16455
+ let hasMemoryArchive = false;
16404
16456
  try {
16405
- const resolvedBrief = resolveTopicBrief(memoryTopic.id, memoryTopic.title);
16457
+ const resolvedBrief = resolveTopicBrief(memoryTopic.id, memoryTitle);
16406
16458
  const wikiDir = getSharedWikiDir();
16407
- const { briefFile, hasBriefFile, latestSummaryFile } = resolveWikiMemoryMirror(wikiDir, memoryTopic.id, memoryTopic.title);
16408
- if (resolvedBrief || hasBriefFile || latestSummaryFile) {
16409
- systemPrompt += buildMemoryPromptSection({
16410
- briefFile,
16411
- wikiDir,
16412
- hasFiles: hasBriefFile,
16413
- latestSummaryFile,
16414
- hasArchive: Boolean(resolvedBrief?.brief.latestSummaryMd || latestSummaryFile),
16415
- isManager
16416
- });
16417
- }
16459
+ const { latestSummaryFile } = resolveWikiMemoryMirror(wikiDir, memoryTopic.id, memoryTitle);
16460
+ hasMemoryArchive = Boolean(resolvedBrief?.brief.latestSummaryMd || latestSummaryFile);
16418
16461
  } catch (err) {
16419
16462
  logger.warn({ topicId, err }, "ai: failed to inject topic brief");
16420
16463
  }
16464
+ systemPrompt += buildMemoryPromptSection({
16465
+ topicTitle: topic.title,
16466
+ ...memoryTopic.memoryKey ? { memoryKey: memoryTopic.memoryKey } : {},
16467
+ hasArchive: hasMemoryArchive,
16468
+ isManager
16469
+ });
16421
16470
  }
16422
16471
  const activeVisual = visualTools ? getActiveVisualForPrompt(topicId, userId) : null;
16423
16472
  if (activeVisual) {
@@ -16531,15 +16580,11 @@ function startAiTurn(params) {
16531
16580
  if (consumePlaywrightUnavailable(userId, topic.title)) {
16532
16581
  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>");
16533
16582
  }
16534
- const effectivePrompt = turnReminders.length > 0 ? `${agentPrompt}
16535
-
16536
- ${turnReminders.join(`
16537
-
16538
- `)}` : agentPrompt;
16583
+ const reminderQuery = buildTurnReminderQuery(agentKind, agentPrompt, turnReminders);
16539
16584
  try {
16540
16585
  yield* runAgent({
16541
16586
  agent: agentKind,
16542
- prompt: effectivePrompt,
16587
+ ...reminderQuery,
16543
16588
  attachments: promptAttachments,
16544
16589
  cwd: workspaceCwd,
16545
16590
  systemPrompt,
@@ -18161,7 +18206,7 @@ import {
18161
18206
  unlinkSync,
18162
18207
  writeFileSync as writeFileSync2
18163
18208
  } from "fs";
18164
- import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
18209
+ import { basename, dirname as dirname2, join as join2, relative, resolve as resolve2 } from "path";
18165
18210
  init_config();
18166
18211
  init_wiki_summary_names();
18167
18212
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -18295,9 +18340,314 @@ function topicNameFrom(topicIdStr) {
18295
18340
  function slugify(topic) {
18296
18341
  return wikiSummarySlug(topic);
18297
18342
  }
18343
+ function normalizeSearchText(value2) {
18344
+ return value2.toLowerCase().replaceAll(/[^a-z0-9\uAC00-\uD7A3]+/g, " ").trim().replaceAll(/\s+/g, " ");
18345
+ }
18346
+ function compactSearchText(value2) {
18347
+ return normalizeSearchText(value2).replaceAll(" ", "");
18348
+ }
18349
+ function trigrams(value2) {
18350
+ const compact = compactSearchText(value2);
18351
+ if (compact.length < 3)
18352
+ return new Set(compact ? [compact] : []);
18353
+ const result = new Set;
18354
+ for (let i = 0;i <= compact.length - 3; i += 1)
18355
+ result.add(compact.slice(i, i + 3));
18356
+ return result;
18357
+ }
18358
+ function fuzzySearchScore(query, candidate, maximum) {
18359
+ const compactQuery = compactSearchText(query);
18360
+ const compactCandidate = compactSearchText(candidate);
18361
+ if (compactQuery.length < 3 || compactCandidate.length < 3)
18362
+ return 0;
18363
+ const similarity = trigramSimilarity(compactQuery, compactCandidate);
18364
+ const minimumSimilarity = compactQuery.length <= 4 ? 0.6 : 0.45;
18365
+ return similarity >= minimumSimilarity ? similarity * maximum : 0;
18366
+ }
18367
+ var SEARCH_STOP_WORDS = new Set([
18368
+ "a",
18369
+ "an",
18370
+ "and",
18371
+ "are",
18372
+ "as",
18373
+ "at",
18374
+ "be",
18375
+ "by",
18376
+ "for",
18377
+ "from",
18378
+ "in",
18379
+ "is",
18380
+ "it",
18381
+ "of",
18382
+ "on",
18383
+ "or",
18384
+ "that",
18385
+ "the",
18386
+ "to",
18387
+ "with"
18388
+ ]);
18389
+ var SEARCH_MONTHS = new Map([
18390
+ ["january", "01"],
18391
+ ["jan", "01"],
18392
+ ["1\uC6D4", "01"],
18393
+ ["february", "02"],
18394
+ ["feb", "02"],
18395
+ ["2\uC6D4", "02"],
18396
+ ["march", "03"],
18397
+ ["mar", "03"],
18398
+ ["3\uC6D4", "03"],
18399
+ ["april", "04"],
18400
+ ["apr", "04"],
18401
+ ["4\uC6D4", "04"],
18402
+ ["may", "05"],
18403
+ ["5\uC6D4", "05"],
18404
+ ["june", "06"],
18405
+ ["jun", "06"],
18406
+ ["6\uC6D4", "06"],
18407
+ ["july", "07"],
18408
+ ["jul", "07"],
18409
+ ["7\uC6D4", "07"],
18410
+ ["august", "08"],
18411
+ ["aug", "08"],
18412
+ ["8\uC6D4", "08"],
18413
+ ["september", "09"],
18414
+ ["sep", "09"],
18415
+ ["sept", "09"],
18416
+ ["9\uC6D4", "09"],
18417
+ ["october", "10"],
18418
+ ["oct", "10"],
18419
+ ["10\uC6D4", "10"],
18420
+ ["november", "11"],
18421
+ ["nov", "11"],
18422
+ ["11\uC6D4", "11"],
18423
+ ["december", "12"],
18424
+ ["dec", "12"],
18425
+ ["12\uC6D4", "12"]
18426
+ ]);
18427
+ var SEARCH_RECENCY_TERMS = new Set([
18428
+ "latest",
18429
+ "newest",
18430
+ "recent",
18431
+ "earliest",
18432
+ "oldest",
18433
+ "\uCD5C\uADFC",
18434
+ "\uCD5C\uC2E0",
18435
+ "\uCD5C\uCD08",
18436
+ "\uC624\uB798\uB41C"
18437
+ ]);
18438
+ function meaningfulSearchTokens(value2) {
18439
+ return normalizeSearchText(value2).split(" ").filter((token) => (token.length >= 2 || /^\d+$/.test(token)) && !SEARCH_STOP_WORDS.has(token));
18440
+ }
18441
+ function canonicalSearchToken(token) {
18442
+ return token.length > 4 && token.endsWith("s") ? token.slice(0, -1) : token;
18443
+ }
18444
+ function canonicalSearchTokens(value2) {
18445
+ return meaningfulSearchTokens(value2).map(canonicalSearchToken);
18446
+ }
18447
+ function countToken(tokens, token) {
18448
+ let count = 0;
18449
+ for (const candidate of tokens)
18450
+ if (candidate === token)
18451
+ count += 1;
18452
+ return count;
18453
+ }
18454
+ function countPhrase(text, phrase) {
18455
+ if (!phrase)
18456
+ return 0;
18457
+ let count = 0;
18458
+ let offset = 0;
18459
+ let match = text.indexOf(phrase, offset);
18460
+ while (match >= 0) {
18461
+ count += 1;
18462
+ offset = match + phrase.length;
18463
+ match = text.indexOf(phrase, offset);
18464
+ }
18465
+ return count;
18466
+ }
18467
+ function summaryTopicIdentity(key) {
18468
+ return normalizeSearchText(key.replace(/^\d{4}-\d{2}-\d{2}-/, ""));
18469
+ }
18470
+ function scoreSummaryTime(query, key, title) {
18471
+ const date = key.match(/^(\d{4})-(\d{2})-(\d{2})/);
18472
+ if (!date)
18473
+ return 0;
18474
+ const tokens = meaningfulSearchTokens(query);
18475
+ const documentDate = `${date[1]}-${date[2]}-${date[3]}`;
18476
+ const requestedDateMatch = query.match(/\b(\d{4}) (\d{2}) (\d{2})\b/);
18477
+ const requestedDate = requestedDateMatch ? `${requestedDateMatch[1]}-${requestedDateMatch[2]}-${requestedDateMatch[3]}` : undefined;
18478
+ const beforeDate = /\bbefore\b/.test(query);
18479
+ const afterDate = /\bafter\b/.test(query);
18480
+ if (requestedDate && beforeDate && documentDate >= requestedDate)
18481
+ return null;
18482
+ if (requestedDate && afterDate && documentDate <= requestedDate)
18483
+ return null;
18484
+ const queryYear = tokens.find((token) => /^\d{4}(?:\uB144)?$/.test(token))?.replace(/\uB144$/, "");
18485
+ const queryMonth = tokens.map((token) => SEARCH_MONTHS.get(token)).find(Boolean);
18486
+ const hasRecencyTerm = tokens.some((token) => SEARCH_RECENCY_TERMS.has(token));
18487
+ const yearMatches = !queryYear || queryYear === date[1];
18488
+ const monthMatches = !queryMonth || queryMonth === date[2];
18489
+ if (!queryYear && !queryMonth && !hasRecencyTerm || !yearMatches || !monthMatches)
18490
+ return 0;
18491
+ const normalizedMetadata = `${normalizeSearchText(key)} ${normalizeSearchText(title)}`;
18492
+ const contentTokens = tokens.filter((token) => !/^\d{4}(?:\uB144)?$/.test(token) && !SEARCH_MONTHS.has(token) && !SEARCH_RECENCY_TERMS.has(token));
18493
+ const contentMatches = contentTokens.filter((token) => normalizedMetadata.includes(token)).length;
18494
+ const relationScore = requestedDate && (beforeDate || afterDate) ? 60 : 0;
18495
+ const exactDateScore = requestedDate === documentDate ? 90 : 0;
18496
+ return 30 + relationScore + exactDateScore + contentMatches * 15;
18497
+ }
18498
+ function trigramSimilarity(left, right) {
18499
+ const a = trigrams(left);
18500
+ const b = trigrams(right);
18501
+ if (a.size === 0 || b.size === 0)
18502
+ return 0;
18503
+ let overlap = 0;
18504
+ for (const gram of a)
18505
+ if (b.has(gram))
18506
+ overlap += 1;
18507
+ return 2 * overlap / (a.size + b.size);
18508
+ }
18509
+ function parseWikiIndex(path) {
18510
+ let text;
18511
+ try {
18512
+ text = readFileSync2(path, "utf-8");
18513
+ } catch {
18514
+ return [];
18515
+ }
18516
+ const candidates = [];
18517
+ const entryPattern = /^\s*-\s*\[\[(topic|articles|summaries)\/([^\]]+)\]\]\s*(.*)$/;
18518
+ for (const line of text.split(`
18519
+ `)) {
18520
+ const match = line.match(entryPattern);
18521
+ if (!match)
18522
+ continue;
18523
+ const namespace = match[1];
18524
+ const key = match[2].replace(/\.md$/i, "");
18525
+ const tail = match[3].trim();
18526
+ const dateMatch = tail.match(/\((\d{4}-\d{2}-\d{2})(?:[^)]*)\)\s*$/);
18527
+ const description = dateMatch ? tail.slice(0, dateMatch.index).trim() : tail;
18528
+ candidates.push({
18529
+ kind: namespace === "topic" ? "topic" : namespace === "articles" ? "article" : "summary",
18530
+ key,
18531
+ description,
18532
+ ...dateMatch ? { date: dateMatch[1] } : {}
18533
+ });
18534
+ }
18535
+ return candidates;
18536
+ }
18537
+ function indexCandidateExists(candidate) {
18538
+ if (candidate.kind === "topic")
18539
+ return true;
18540
+ const root = candidate.kind === "article" ? runtime().articlesDir : runtime().summariesDir;
18541
+ const filePath = resolve2(root, `${candidate.key}.md`);
18542
+ const relativePath = relative(root, filePath);
18543
+ if (relativePath.startsWith("..") || relativePath.startsWith("/"))
18544
+ return false;
18545
+ try {
18546
+ return statSync(filePath).isFile();
18547
+ } catch {
18548
+ return false;
18549
+ }
18550
+ }
18551
+ function scoreIndexKey(query, key) {
18552
+ const normalizedQuery = normalizeSearchText(query);
18553
+ const compactQuery = compactSearchText(normalizedQuery);
18554
+ const normalizedKey = normalizeSearchText(key);
18555
+ const compactKey = compactSearchText(normalizedKey);
18556
+ const tokens = meaningfulSearchTokens(normalizedQuery);
18557
+ let score = 0;
18558
+ if (normalizedKey === normalizedQuery)
18559
+ score += 120;
18560
+ else if (compactKey === compactQuery)
18561
+ score += 110;
18562
+ else {
18563
+ if (compactQuery.length >= 2 && (compactKey.startsWith(compactQuery) || compactQuery.startsWith(compactKey))) {
18564
+ score += 65;
18565
+ }
18566
+ if (tokens.length > 0 && compactQuery.length >= 3 && compactKey.includes(compactQuery)) {
18567
+ score += 45;
18568
+ }
18569
+ score += fuzzySearchScore(compactQuery, compactKey, 55);
18570
+ }
18571
+ for (const token of tokens) {
18572
+ if (normalizedKey.includes(token))
18573
+ score += 10;
18574
+ }
18575
+ return score;
18576
+ }
18577
+ function scoreIndexCandidate(query, candidate) {
18578
+ const normalizedQuery = normalizeSearchText(query);
18579
+ if (!normalizedQuery)
18580
+ return 0;
18581
+ const normalizedDescription = normalizeSearchText(candidate.description);
18582
+ const tokens = canonicalSearchTokens(normalizedQuery);
18583
+ const descriptionTokens = new Set(canonicalSearchTokens(normalizedDescription));
18584
+ let score = scoreIndexKey(normalizedQuery, candidate.key);
18585
+ if (tokens.length > 0 && normalizedDescription.includes(normalizedQuery))
18586
+ score += 30;
18587
+ let descriptionMatches = 0;
18588
+ for (const token of tokens) {
18589
+ if (descriptionTokens.has(token)) {
18590
+ score += 5;
18591
+ descriptionMatches += 1;
18592
+ }
18593
+ }
18594
+ if (tokens.length > 1 && descriptionMatches === tokens.length)
18595
+ score += 25;
18596
+ if (candidate.kind === "topic") {
18597
+ const requestsCurrent = /\b(?:current|active|ongoing|open)\b|\uD604\uC7AC|\uC9C4\uD589 \uC911/.test(normalizedQuery);
18598
+ const archivedCandidate = /\b(?:archive|archived|historical|closed)\b|\uBCF4\uAD00|\uC885\uB8CC/.test(`${normalizeSearchText(candidate.key)} ${normalizedDescription}`);
18599
+ if (requestsCurrent && archivedCandidate)
18600
+ score -= 40;
18601
+ }
18602
+ return score;
18603
+ }
18298
18604
  function wikiQuery(args) {
18299
- const question = safeExt(args, ["question", "query", "q", "text"], "");
18300
- const query = question.toLowerCase();
18605
+ const question = String(safeExt(args, ["question", "query", "q", "text"], "")).trim();
18606
+ const requestedKind = String(safeExt(args, ["kind", "type"], "all"));
18607
+ const kind = requestedKind === "topic" || requestedKind === "article" || requestedKind === "summary" ? requestedKind : "all";
18608
+ const requestedLimit = Number(safeExt(args, ["limit", "maxResults"], 8));
18609
+ const limit = Math.min(Math.max(Number.isFinite(requestedLimit) ? requestedLimit : 8, 1), 20);
18610
+ if (!question) {
18611
+ return {
18612
+ content: [{ type: "text", text: "A non-empty wiki query is required." }],
18613
+ isError: true
18614
+ };
18615
+ }
18616
+ const normalizedQuery = normalizeSearchText(question);
18617
+ if (!normalizedQuery) {
18618
+ return { content: [{ type: "text", text: "No matching wiki articles found." }] };
18619
+ }
18620
+ const canReadTopicMemory = runtime().host.canReadTopicMemory;
18621
+ 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;
18622
+ const visibleIndexCandidates = [
18623
+ ...parseWikiIndex(resolve2(runtime().wikiDir, "topic-index.md")),
18624
+ ...parseWikiIndex(resolve2(runtime().wikiDir, "article-index.md"))
18625
+ ].filter((candidate) => kind === "all" || candidate.kind === kind).filter((candidate) => candidate.kind !== "topic" || !canReadTopicMemory || canReadTopicMemory(candidate.key, runtime().userId));
18626
+ const hasStaleExactIndexMatch = visibleIndexCandidates.some((candidate) => !indexCandidateExists(candidate) && scoreIndexKey(question, candidate.key) >= 100);
18627
+ const indexCandidates = visibleIndexCandidates.filter(indexCandidateExists).map((candidate) => ({
18628
+ candidate,
18629
+ keyScore: scoreIndexKey(question, candidate.key),
18630
+ score: scoreIndexCandidate(question, candidate)
18631
+ })).filter(({ keyScore, score }) => keyScore >= 25 || score >= 40).sort((left, right) => right.score - left.score || left.candidate.key.localeCompare(right.candidate.key));
18632
+ const strongestIndex = indexCandidates[0];
18633
+ if (kind === "topic" && strongestIndex && (strongestIndex.keyScore >= 25 || strongestIndex.score >= 40)) {
18634
+ const runnerUp = indexCandidates[1];
18635
+ const ambiguous = strongestIndex.keyScore < 100 && runnerUp !== undefined && runnerUp.score >= strongestIndex.score * 0.9;
18636
+ if (ambiguous) {
18637
+ const lines2 = ["Found 2 ambiguous wiki candidates:", ""];
18638
+ for (const { candidate, score } of [strongestIndex, runnerUp]) {
18639
+ lines2.push(`- kind: ${candidate.kind}`, ` key: ${candidate.key}`, ` score: ${score.toFixed(2)}`, ` description: ${candidate.description || "(none)"}`, ...candidate.date ? [` date: ${candidate.date}`] : []);
18640
+ }
18641
+ return { content: [{ type: "text", text: lines2.join(`
18642
+ `) }] };
18643
+ }
18644
+ const lines = ["Found 1 wiki candidate:", ""];
18645
+ for (const { candidate, score } of [strongestIndex]) {
18646
+ lines.push(`- kind: ${candidate.kind}`, ` key: ${candidate.key}`, ` score: ${score.toFixed(2)}`, ` description: ${candidate.description || "(none)"}`, ...candidate.date ? [` date: ${candidate.date}`] : []);
18647
+ }
18648
+ return { content: [{ type: "text", text: lines.join(`
18649
+ `) }] };
18650
+ }
18301
18651
  const results = [];
18302
18652
  const scored = [];
18303
18653
  function scan(dir, label) {
@@ -18314,67 +18664,234 @@ function wikiQuery(args) {
18314
18664
  const fp = join2(sub, f.name);
18315
18665
  try {
18316
18666
  const text = readFileSync2(fp, "utf-8");
18667
+ const key = `${entry.name}/${f.name.replace(/\.md$/i, "")}`;
18317
18668
  scored.push({
18318
- score: scoreMatch(text),
18669
+ kind: label === "articles" ? "article" : "skill",
18670
+ key,
18671
+ keyScore: scoreIndexKey(question, key),
18672
+ score: scoreMatch(text, key, label === "articles" ? "article" : "skill"),
18319
18673
  path: `${label}/${entry.name}/${f.name}`,
18320
- text
18674
+ text,
18675
+ title: titleFromDocument(text, key)
18321
18676
  });
18322
18677
  } catch {}
18323
18678
  }
18324
18679
  } catch {}
18325
18680
  }
18326
18681
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
18682
+ if (label === "topic" && canReadTopicMemory && !canReadTopicMemory(entry.name.replace(/\.md$/i, ""), runtime().userId)) {
18683
+ continue;
18684
+ }
18327
18685
  const fp = join2(dir, entry.name);
18328
18686
  try {
18329
18687
  const text = readFileSync2(fp, "utf-8");
18688
+ const key = entry.name.replace(/\.md$/i, "");
18689
+ const resultKind = label === "articles" ? "article" : label === "topic" ? "topic" : label === "summaries" ? "summary" : "skill";
18330
18690
  scored.push({
18331
- score: scoreMatch(text),
18691
+ kind: resultKind,
18692
+ key,
18693
+ keyScore: scoreIndexKey(question, key),
18694
+ score: scoreMatch(text, key, resultKind),
18332
18695
  path: `${label}/${entry.name}`,
18333
- text
18696
+ text,
18697
+ title: titleFromDocument(text, key),
18698
+ ...label === "summaries" && /^\d{4}-\d{2}-\d{2}/.test(key) ? { date: key.slice(0, 10) } : {}
18334
18699
  });
18335
18700
  } catch {}
18336
18701
  }
18337
18702
  }
18338
18703
  } catch {}
18339
18704
  }
18340
- function scoreMatch(text) {
18341
- const lower = text.toLowerCase();
18705
+ function titleFromDocument(text, key) {
18706
+ const titleLine = text.split(`
18707
+ `).find((line) => /^#+\s*/.test(line));
18708
+ return titleLine ? titleLine.replace(/^#+\s*/, "") : basename(key);
18709
+ }
18710
+ function scoreMatch(text, key, resultKind) {
18711
+ const titleLine = text.split(`
18712
+ `).find((line) => /^#+\s*/.test(line)) ?? "";
18713
+ const title = titleLine.replace(/^#+\s*/, "");
18714
+ const normalizedKey = normalizeSearchText(key);
18715
+ const normalizedTitle = normalizeSearchText(title);
18716
+ const normalizedBody = normalizeSearchText(text.replace(titleLine, ""));
18717
+ const compactQuery = compactSearchText(normalizedQuery);
18718
+ const compactKey = compactSearchText(normalizedKey);
18719
+ const compactTitle = compactSearchText(normalizedTitle);
18720
+ const tokens = canonicalSearchTokens(normalizedQuery);
18721
+ const keyTokens = new Set(canonicalSearchTokens(normalizedKey));
18722
+ const titleTokens = new Set(canonicalSearchTokens(normalizedTitle));
18723
+ const bodyTokens = canonicalSearchTokens(normalizedBody);
18342
18724
  let score = 0;
18343
- const words = query.split(/\s+/).filter(Boolean);
18344
- for (const w of words) {
18345
- if (lower.includes(w)) {
18346
- score += w.length >= 3 ? 3 : 1;
18347
- const firstLine = lower.split(`
18348
- `)[0];
18349
- if (firstLine.startsWith("#") && firstLine.includes(w))
18350
- score += 5;
18725
+ let strongMetadataEvidence = false;
18726
+ if (normalizedKey === normalizedQuery) {
18727
+ score += 120;
18728
+ strongMetadataEvidence = true;
18729
+ } else if (compactKey === compactQuery) {
18730
+ score += 110;
18731
+ strongMetadataEvidence = true;
18732
+ } else if (compactQuery.length >= 2 && (compactKey.startsWith(compactQuery) || compactQuery.startsWith(compactKey))) {
18733
+ score += 65;
18734
+ strongMetadataEvidence = true;
18735
+ }
18736
+ if (tokens.length > 0 && compactQuery.length >= 3 && compactKey.includes(compactQuery)) {
18737
+ score += 45;
18738
+ strongMetadataEvidence = true;
18739
+ }
18740
+ const fuzzyKeyScore = fuzzySearchScore(compactQuery, compactKey, 55);
18741
+ score += fuzzyKeyScore;
18742
+ if (fuzzyKeyScore > 0)
18743
+ strongMetadataEvidence = true;
18744
+ if (normalizedTitle === normalizedQuery) {
18745
+ score += 115;
18746
+ strongMetadataEvidence = true;
18747
+ } else if (compactTitle === compactQuery) {
18748
+ score += 105;
18749
+ strongMetadataEvidence = true;
18750
+ } else if (compactQuery.length >= 2 && compactTitle.startsWith(compactQuery)) {
18751
+ score += 60;
18752
+ strongMetadataEvidence = true;
18753
+ }
18754
+ const fuzzyTitleScore = fuzzySearchScore(compactQuery, compactTitle, 50);
18755
+ score += fuzzyTitleScore;
18756
+ if (fuzzyTitleScore > 0)
18757
+ strongMetadataEvidence = true;
18758
+ if (resultKind === "summary") {
18759
+ const temporalScore = scoreSummaryTime(normalizedQuery, key, title);
18760
+ if (temporalScore === null)
18761
+ return 0;
18762
+ score += temporalScore;
18763
+ if (temporalScore > 0)
18764
+ strongMetadataEvidence = true;
18765
+ }
18766
+ const isMultiToken = tokens.length > 1;
18767
+ if (tokens.length > 0 && normalizedTitle.includes(normalizedQuery)) {
18768
+ score += 35;
18769
+ strongMetadataEvidence = true;
18770
+ }
18771
+ const phraseMatches = isMultiToken ? countPhrase(normalizedBody, normalizedQuery) : 0;
18772
+ if (phraseMatches > 0)
18773
+ score += phraseMatches * 12;
18774
+ else if (tokens.length === 1 && tokens[0].length >= 5 && normalizedBody.includes(tokens[0])) {
18775
+ score += 16;
18776
+ }
18777
+ let bodyMatches = 0;
18778
+ let minimumBodyFrequency = Number.POSITIVE_INFINITY;
18779
+ for (const token of tokens) {
18780
+ if (keyTokens.has(token))
18781
+ score += 10;
18782
+ if (titleTokens.has(token))
18783
+ score += 12;
18784
+ const bodyFrequency = countToken(bodyTokens, token);
18785
+ if (bodyFrequency > 0) {
18786
+ score += Math.min(bodyFrequency, 3) * 4;
18787
+ bodyMatches += 1;
18788
+ minimumBodyFrequency = Math.min(minimumBodyFrequency, bodyFrequency);
18351
18789
  }
18352
18790
  }
18791
+ if (isMultiToken && bodyMatches === tokens.length) {
18792
+ score += 8 * Math.min(minimumBodyFrequency, 3);
18793
+ }
18794
+ const minimumBodyTokens = hasStaleExactIndexMatch ? 3 : 2;
18795
+ const partialArticleEvidence = resultKind === "article" && bodyMatches >= 3 && bodyMatches / tokens.length >= 0.6;
18796
+ const strongBodyEvidence = tokens.length >= minimumBodyTokens && (bodyMatches === tokens.length && (phraseMatches > 0 || minimumBodyFrequency >= 1) || partialArticleEvidence);
18797
+ if (!strongMetadataEvidence && !strongBodyEvidence)
18798
+ return 0;
18353
18799
  return score;
18354
18800
  }
18355
- scan(runtime().articlesDir, "articles");
18356
- if (runtime().surface === "all")
18801
+ if (kind === "all" || kind === "article")
18802
+ scan(runtime().articlesDir, "articles");
18803
+ if (kind === "all" && runtime().surface === "all")
18357
18804
  scan(runtime().skillsDir, "skills");
18358
- scan(runtime().topicsDir, "topic");
18359
- scan(runtime().summariesDir, "summaries");
18360
- scored.sort((a, b) => b.score - a.score);
18361
- const top = scored.slice(0, 8);
18362
- if (top.length === 0) {
18805
+ if (kind === "all" || kind === "topic")
18806
+ scan(runtime().topicsDir, "topic");
18807
+ if (kind === "all" || kind === "summary")
18808
+ scan(runtime().summariesDir, "summaries");
18809
+ const merged = new Map;
18810
+ for (const { candidate, keyScore, score } of indexCandidates) {
18811
+ merged.set(`${candidate.kind}:${candidate.key}`, {
18812
+ kind: candidate.kind,
18813
+ key: candidate.key,
18814
+ keyScore,
18815
+ score,
18816
+ description: candidate.description,
18817
+ date: candidate.date
18818
+ });
18819
+ }
18820
+ const documentThreshold = kind === "topic" ? 24 : 16;
18821
+ for (const document of scored) {
18822
+ if (document.score < documentThreshold)
18823
+ continue;
18824
+ const id = `${document.kind}:${document.key}`;
18825
+ const indexed = merged.get(id);
18826
+ merged.set(id, indexed ? { ...indexed, ...document, score: Math.max(indexed.score, document.score) } : document);
18827
+ }
18828
+ 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));
18829
+ if (kind === "summary") {
18830
+ const sameTopic = ranked.filter((result) => {
18831
+ const identity = summaryTopicIdentity(result.key);
18832
+ return identity.length >= 3 && normalizedQuery.includes(identity);
18833
+ });
18834
+ if (sameTopic.length > 0)
18835
+ ranked = sameTopic;
18836
+ const requestedDateMatch = normalizedQuery.match(/\b(\d{4}) (\d{2}) (\d{2})\b/);
18837
+ const requestedDate = requestedDateMatch ? `${requestedDateMatch[1]}-${requestedDateMatch[2]}-${requestedDateMatch[3]}` : undefined;
18838
+ const beforeDate = /\bbefore\b/.test(normalizedQuery);
18839
+ const afterDate = /\bafter\b/.test(normalizedQuery);
18840
+ if (requestedDate && beforeDate) {
18841
+ ranked = ranked.filter((result) => !result.date || result.date < requestedDate);
18842
+ } else if (requestedDate && afterDate) {
18843
+ ranked = ranked.filter((result) => !result.date || result.date > requestedDate);
18844
+ }
18845
+ ranked.sort((left, right) => {
18846
+ if (beforeDate) {
18847
+ return (right.date ?? "").localeCompare(left.date ?? "") || right.score - left.score;
18848
+ }
18849
+ if (afterDate) {
18850
+ return (left.date ?? "").localeCompare(right.date ?? "") || right.score - left.score;
18851
+ }
18852
+ if (requestedDate) {
18853
+ const dateDifference = Number(right.date === requestedDate) - Number(left.date === requestedDate);
18854
+ if (dateDifference !== 0)
18855
+ return dateDifference;
18856
+ }
18857
+ if (temporalDirection) {
18858
+ const dateDifference = temporalDirection === "oldest" ? (left.date ?? "").localeCompare(right.date ?? "") : (right.date ?? "").localeCompare(left.date ?? "");
18859
+ if (dateDifference !== 0)
18860
+ return dateDifference;
18861
+ }
18862
+ return right.score - left.score || (right.date ?? "").localeCompare(left.date ?? "") || left.key.localeCompare(right.key);
18863
+ });
18864
+ }
18865
+ if (kind === "topic" && ranked[0]) {
18866
+ const runnerUp = ranked[1];
18867
+ const ambiguous = ranked[0].keyScore < 100 && runnerUp !== undefined && runnerUp.score >= ranked[0].score * 0.9;
18868
+ if (ambiguous) {
18869
+ ranked = ranked.slice(0, 2);
18870
+ } else {
18871
+ ranked = ranked.slice(0, 1);
18872
+ }
18873
+ } else {
18874
+ ranked = ranked.slice(0, limit);
18875
+ }
18876
+ if (ranked.length === 0) {
18363
18877
  results.push("No matching wiki articles found.");
18364
18878
  } else {
18365
- results.push(`Found ${top.length} matching result(s):
18879
+ results.push(`Found ${ranked.length} matching result(s):
18366
18880
  `);
18367
- for (const { path, text } of top) {
18368
- const titleLine = text.split(`
18369
- `).find((l) => l.startsWith("#"));
18370
- const title = titleLine ? titleLine.replace(/^#+\s*/, "") : basename(path, ".md");
18371
- const body = text.replace(/^---[\s\S]*?---\n?/, "").replace(/^#.*$/m, "").trim().slice(0, 400);
18372
- results.push(`## ${title}
18373
- Path: ${path}
18881
+ for (const result of ranked) {
18882
+ if (result.kind === "skill") {
18883
+ const body = (result.text ?? "").trim().slice(0, 400);
18884
+ results.push(`## ${result.title ?? result.key}
18885
+ Path: ${result.path}
18374
18886
 
18375
- ${body}${text.length > 400 ? `
18887
+ ${body}${(result.text?.length ?? 0) > 400 ? `
18376
18888
  ...(truncated)` : ""}
18377
18889
  `);
18890
+ continue;
18891
+ }
18892
+ 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 ? [
18893
+ ` excerpt: ${result.text.replace(/^---[\s\S]*?---\n?/, "").replace(/^#.*$/m, "").trim().slice(0, 400)}`
18894
+ ] : []);
18378
18895
  }
18379
18896
  }
18380
18897
  return { content: [{ type: "text", text: results.join(`
@@ -18393,36 +18910,94 @@ function wikiTopicBrief(args) {
18393
18910
  };
18394
18911
  }
18395
18912
  const { getTopicBrief, resolveTopicBrief } = runtime().host;
18396
- if (runtime().topicId && (getTopicBrief || resolveTopicBrief)) {
18913
+ const hasAuthorizedBridge = Boolean(runtime().topicId && (getTopicBrief || resolveTopicBrief));
18914
+ if (hasAuthorizedBridge) {
18397
18915
  try {
18398
18916
  const brief = resolveTopicBrief ? resolveTopicBrief(rawTopic, runtime().userId) : getTopicBrief(rawTopic);
18399
- if (!brief) {
18400
- return {
18401
- content: [{ type: "text", text: `No brief found for topic: ${rawTopic}` }]
18402
- };
18403
- }
18404
- const lines = [
18405
- `# Topic Brief: ${topicNameFrom(rawTopic)}`,
18406
- "",
18407
- brief.briefMd || "(empty brief)"
18408
- ];
18409
- if (brief.latestSummaryMd) {
18410
- lines.push("", "## Latest Summary", "", brief.latestSummaryMd);
18411
- }
18412
- if (brief.summaryDate) {
18413
- lines.push("", `Summary date: ${brief.summaryDate}`);
18414
- }
18415
- return { content: [{ type: "text", text: lines.join(`
18917
+ if (brief) {
18918
+ const lines = [
18919
+ `# Topic Brief: ${topicNameFrom(rawTopic)}`,
18920
+ "",
18921
+ brief.briefMd || "(empty brief)"
18922
+ ];
18923
+ if (brief.latestSummaryMd) {
18924
+ lines.push("", "## Latest Summary", "", brief.latestSummaryMd);
18925
+ }
18926
+ if (brief.summaryDate) {
18927
+ lines.push("", `Summary date: ${brief.summaryDate}`);
18928
+ }
18929
+ return { content: [{ type: "text", text: lines.join(`
18416
18930
  `) }] };
18417
- } catch {
18931
+ }
18932
+ } catch {}
18933
+ return {
18934
+ content: [{ type: "text", text: `No brief found for topic: ${rawTopic}` }]
18935
+ };
18936
+ }
18937
+ const topicKey = wikiSummarySlug(rawTopic.replace(/^topic\//, "").replace(/\.md$/i, ""));
18938
+ const filePath = resolve2(runtime().topicsDir, `${topicKey}.md`);
18939
+ try {
18940
+ const content = readFileSync2(filePath, "utf-8");
18941
+ return {
18942
+ content: [{ type: "text", text: `# Topic Brief: ${topicKey}
18943
+
18944
+ ${content}` }]
18945
+ };
18946
+ } catch {}
18947
+ return {
18948
+ content: [{ type: "text", text: `No brief found for topic: ${rawTopic}` }]
18949
+ };
18950
+ }
18951
+ function wikiRead(args) {
18952
+ const rawKind = String(safeExt(args, ["kind", "type"], "article"));
18953
+ const kind = rawKind === "topic" || rawKind === "summary" ? rawKind : "article";
18954
+ const rawKey = String(safeExt(args, ["key", "slug", "topic"], "")).trim();
18955
+ if (!rawKey) {
18956
+ return { content: [{ type: "text", text: "A wiki document key is required." }], isError: true };
18957
+ }
18958
+ if (kind === "topic") {
18959
+ const result = wikiTopicBrief({ topic: rawKey });
18960
+ const resultText = result.content.map((entry) => entry.type === "text" ? entry.text : "").join(`
18961
+ `);
18962
+ const adopt = safeExt(args, ["adopt", "use", "select"], false);
18963
+ if (!adopt || resultText.startsWith("No brief found for topic"))
18964
+ return result;
18965
+ const currentTopicId = runtime().currentTopicId;
18966
+ const memoryKey = wikiSummarySlug(rawKey.replace(/^topic\//, "").replace(/\.md$/i, ""));
18967
+ if (!currentTopicId || !runtime().host.adoptTopicMemory?.(currentTopicId, runtime().userId, memoryKey)) {
18418
18968
  return {
18419
- content: [{ type: "text", text: `Could not read topic brief for: ${rawTopic}` }]
18969
+ content: [
18970
+ ...result.content,
18971
+ { type: "text", text: `
18972
+ Memory was read but could not be adopted by this topic.` }
18973
+ ]
18420
18974
  };
18421
18975
  }
18976
+ return {
18977
+ content: [...result.content, { type: "text", text: `
18978
+ Adopted topic memory: ${memoryKey}` }]
18979
+ };
18980
+ }
18981
+ if (rawKey.includes("\\") || rawKey.split("/").some((part) => part === "..") || rawKey.startsWith("/")) {
18982
+ return { content: [{ type: "text", text: "Invalid wiki document key." }], isError: true };
18983
+ }
18984
+ const root = kind === "summary" ? runtime().summariesDir : runtime().articlesDir;
18985
+ const key = rawKey.replace(new RegExp(`^(?:${kind === "summary" ? "summaries" : "articles"})/`), "").replace(/\.md$/i, "");
18986
+ const filePath = resolve2(root, `${key}.md`);
18987
+ const relativePath = relative(root, filePath);
18988
+ if (relativePath.startsWith("..") || relativePath.startsWith("/")) {
18989
+ return { content: [{ type: "text", text: "Invalid wiki document key." }], isError: true };
18990
+ }
18991
+ try {
18992
+ const content = readFileSync2(filePath, "utf-8");
18993
+ return {
18994
+ content: [{ type: "text", text: `# Wiki ${kind}: ${key}
18995
+
18996
+ ${content}` }]
18997
+ };
18998
+ } catch {
18999
+ return { content: [{ type: "text", text: `No ${kind} found for: ${rawKey}` }] };
18422
19000
  }
18423
- return {
18424
- content: [{ type: "text", text: `No topic brief found for: ${rawTopic}` }]
18425
- };
18426
19001
  }
18427
19002
  function wikiLastConversation(args) {
18428
19003
  const rawTopic = safeExt(args, ["topic", "topicName", "topicId"], runtime().topicId);
@@ -18789,7 +19364,7 @@ function indexUpsert(args) {
18789
19364
  var WIKI_TOOLS = [
18790
19365
  {
18791
19366
  name: "wiki_query",
18792
- description: "Search the wiki knowledge base and return relevant articles.",
19367
+ description: "Search topic, article, or summary indexes and return relevant candidates.",
18793
19368
  inputSchema: {
18794
19369
  type: "object",
18795
19370
  properties: {
@@ -18800,11 +19375,38 @@ var WIKI_TOOLS = [
18800
19375
  topic: {
18801
19376
  type: "string",
18802
19377
  description: "Optional topic name to narrow the search"
19378
+ },
19379
+ kind: {
19380
+ type: "string",
19381
+ enum: ["all", "topic", "article", "summary"],
19382
+ description: "Document kind to search; defaults to all"
19383
+ },
19384
+ limit: {
19385
+ type: "number",
19386
+ minimum: 1,
19387
+ maximum: 20,
19388
+ description: "Maximum number of candidates; defaults to 8"
18803
19389
  }
18804
19390
  },
18805
19391
  required: ["question"]
18806
19392
  }
18807
19393
  },
19394
+ {
19395
+ name: "wiki_read",
19396
+ description: "Read one topic, article, or summary returned by wiki_query. For a clearly matching continuing persona, set adopt=true when reading a topic.",
19397
+ inputSchema: {
19398
+ type: "object",
19399
+ properties: {
19400
+ kind: { type: "string", enum: ["topic", "article", "summary"] },
19401
+ key: { type: "string", description: "Canonical key returned by wiki_query" },
19402
+ adopt: {
19403
+ type: "boolean",
19404
+ description: "For kind=topic, persist this canonical memory key for the current room"
19405
+ }
19406
+ },
19407
+ required: ["kind", "key"]
19408
+ }
19409
+ },
18808
19410
  {
18809
19411
  name: "wiki_topic_brief",
18810
19412
  description: "Get the lightweight topic brief for a specific topic.",
@@ -18937,6 +19539,7 @@ function createWikiMcpServer(context, host) {
18937
19539
  const wikiDir = resolve2(host.wikiRoot);
18938
19540
  const current = {
18939
19541
  userId: context.userId,
19542
+ currentTopicId: context.currentTopicId ?? context.topicId,
18940
19543
  topicId: context.topicId,
18941
19544
  surface,
18942
19545
  host,
@@ -18966,6 +19569,7 @@ function createWikiMcpServer(context, host) {
18966
19569
  const { name, arguments: args } = request.params;
18967
19570
  const handlers = {
18968
19571
  wiki_query: wikiQuery,
19572
+ wiki_read: wikiRead,
18969
19573
  wiki_topic_brief: wikiTopicBrief,
18970
19574
  wiki_last_conversation: wikiLastConversation,
18971
19575
  skill_query: skillQuery,
@@ -19907,4 +20511,4 @@ export {
19907
20511
  createAgentHealthMcpServer
19908
20512
  };
19909
20513
 
19910
- //# debugId=AD21E471FB1B87CE64756E2164756E21
20514
+ //# debugId=AE60D4AF576C043E64756E2164756E21