scream-code 0.8.1 → 0.8.3

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.
@@ -5,7 +5,7 @@ const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
6
  import { a as __toESM, i as __require, r as __exportAll, t as __commonJSMin } from "./chunk-apG1qJts.mjs";
7
7
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
8
- import { C as parse$7, S as normalize, T as resolve$1, _ as KnowledgeStore, b as isAbsolute$1, i as isSupportedFile, n as ingestDirectory, r as ingestFile, t as multiSearch, v as basename$1, w as relative$1, x as join$1, y as dirname$2 } from "./src-CFaXGblk.mjs";
8
+ import { C as join$1, D as resolve$1, E as relative$1, S as isAbsolute$1, T as parse$7, a as isSupportedFile, b as basename$1, i as ingestFile, r as ingestDirectory, t as multiSearch, w as normalize, x as dirname$2, y as KnowledgeStore } from "./src-DWWVKqBK.mjs";
9
9
  import { createRequire } from "node:module";
10
10
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
11
11
  import Jn, { access, appendFile, chmod, copyFile, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
@@ -58216,8 +58216,8 @@ var KnowledgeLookupTool = class {
58216
58216
  const llm = { generate: async (systemPrompt, userPrompt) => {
58217
58217
  return this.agent.generateText(systemPrompt, userPrompt);
58218
58218
  } };
58219
- const { multiSearch } = await import("./src-BJO7qNnY.mjs");
58220
- const results = await multiSearch(store, llm, query, { topK });
58219
+ const { multiSearchWithTrace } = await import("./src-BChzuWm-.mjs");
58220
+ const { results, trace } = await multiSearchWithTrace(store, llm, query, { topK });
58221
58221
  if (results.length === 0) return {
58222
58222
  isError: false,
58223
58223
  output: `No knowledge base entries found for query "${query}".`
@@ -58228,6 +58228,13 @@ var KnowledgeLookupTool = class {
58228
58228
  const eventLine = result.eventTitle !== null ? ` Event: ${result.eventTitle}` : void 0;
58229
58229
  lines.push(`**${i + 1}. ${heading}** (from: ${result.sourceName})`, ` Score: ${result.score.toFixed(3)}`, ...eventLine !== void 0 ? [eventLine] : [], ` ${result.content}`, "");
58230
58230
  }
58231
+ lines.push("---", "Retrieval trace:");
58232
+ if (trace.fallbackReason !== null) lines.push(` ⚠ Fallback: ${trace.fallbackReason}`);
58233
+ for (const step of trace.steps) {
58234
+ const payloadStr = step.payload !== void 0 ? ` ${JSON.stringify(step.payload)}` : "";
58235
+ lines.push(` • ${step.step} (${step.durationMs}ms) — ${step.detail}${payloadStr}`);
58236
+ }
58237
+ if (trace.rerankedEventTitles.length > 0) lines.push(` Reranked events: ${trace.rerankedEventTitles.join(" | ")}`);
58231
58238
  return {
58232
58239
  isError: false,
58233
58240
  output: lines.join("\n")
@@ -78393,7 +78400,21 @@ function stripContextMetadata(message) {
78393
78400
  //#region ../../packages/agent-core/src/agent/compaction/compaction-instruction.md
78394
78401
  var compaction_instruction_default = "\n--- This message is a direct task, not part of the above conversation ---\n\nYou are now given a task to compact this conversation context according to specific priorities and output requirements.\n\nOutput text only. DO NOT CALL ANY TOOLS. Calling tools will be rejected and fails the task. You already have all the information you need in the conversation history. You have only one chance.\n\nThe goal of compaction is to keep essential code patterns, technical details, and architectural decisions for continuing development without losing context after the above messages are cleared work.\n\n{{ customInstruction }}\n\n<!-- Memory Memo Extraction (PRIORITY — do not skip) -->\n\n## 任务经验提取\n\nAFTER completing the compaction summary below, scan the messages being compacted for **completed task loops**. A task loop is \"completed\" when:\n- The user made a clear request or asked a specific question\n- You provided a solution or answer\n- The outcome is clear (success, partial success, or failure)\n\nFor each completed task loop found, output a structured experience record **at the very end of your response**:\n\n```memory-memo\n{\n \"userNeed\": \"<the user's need or goal, one sentence>\",\n \"approach\": \"<what was done — the approach taken, 2-4 sentences>\",\n \"outcome\": \"<final result, e.g. '完成', '部分完成', '失败: reason'>\",\n \"whatFailed\": \"<dead ends tried — things that didn't work, or 'none'>\",\n \"whatWorked\": \"<key actions that ultimately worked, or 'none'>\",\n \"tags\": [\"<tag1>\", \"<tag2>\", \"<tag3>\"]\n}\n```\n\nGuidelines:\n- Record important failed attempts in \"whatFailed\" to help avoid repeating mistakes.\n- Record key successful actions in \"whatWorked\" to help reuse effective approaches.\n- Include 3-5 semantic \"tags\" summarizing the task domain, tech stack, or action type (e.g. [\"react\", \"auth\", \"部署\"]).\n- Skip in-progress work unless it contains a valuable error+fix experience.\n- Merge closely related sub-tasks into a single record.\n- Use the exact field names and JSON format shown above.\n\nIf no completed task loops are found in the compacted messages, output:\n```memory-memo\n{\"none\": true}\n```\n\n<!-- Compression Priorities (in order) -->\n\n1. **Current Task State**: What is being worked on RIGHT NOW\n2. **Errors & Solutions**: All encountered errors and their resolutions\n3. **Code Evolution**: Final working versions only (remove intermediate attempts)\n4. **System Context**: Project structure, dependencies, environment setup\n5. **Design Decisions**: Architectural choices and their rationale\n6. **TODO Items**: Unfinished tasks and known issues\n\n<!-- Required Output Structure -->\n\n## Current Focus\n\n[What we're working on now]\n\n## Environment\n\n- [Key setup/config points]\n- ...\n\n## Completed Tasks\n\n- [Task]: [Brief outcome]\n- ...\n\n## Active Issues\n\n- [Issue]: [Status/Next steps]\n- ...\n\n## Code State\n\n### [Critical file name]\n\n[Brief description of the file's purpose and current state]\n\n```\n[The latest version of critical code snippets in this file, <20 lines]\n```\n\n### [Critical file name]\n\n- [Useful classes/methods/functions]: [Brief description/usage]\n- ...\n\n<!-- Omit non-critical code, intermediate attempts, and resolved errors -->\n\n## Important Context\n\n- [Any crucial information not covered above]\n- ...\n\n## All User Messages\n\n- [Detailed non tool use user message]\n- ...\n";
78395
78402
  //#endregion
78403
+ //#region ../../packages/agent-core/src/agent/compaction/compaction-update-instruction.md
78404
+ var compaction_update_instruction_default = "\n--- This message is a direct task, not part of the above conversation ---\n\nYou are now given a task to UPDATE an existing compaction summary with new messages that came in after the last compaction.\n\nA previous compaction summary already exists at the top of the conversation (the first assistant message). You must NOT discard it. Instead, merge the new information into the existing structure, keeping all previously captured state intact unless explicitly contradicted or resolved by the new messages.\n\nOutput text only. DO NOT CALL ANY TOOLS. Calling tools will be rejected and fails the task. You have only one chance.\n\n{{ customInstruction }}\n\n<!-- Previous Summary Reference -->\n\nThe first assistant message above is the PREVIOUS summary. Treat it as the source of truth for everything that happened before the new messages. Do not restate it verbatim — produce an updated, merged summary.\n\n<!-- Memory Memo Extraction (PRIORITY — do not skip) -->\n\n## 任务经验提取\n\nAFTER completing the updated summary below, scan ONLY the new messages being compacted (not those already covered by the previous summary) for **completed task loops**. A task loop is \"completed\" when:\n- The user made a clear request or asked a specific question\n- You provided a solution or answer\n- The outcome is clear (success, partial success, or failure)\n\nFor each completed task loop found, output a structured experience record **at the very end of your response**:\n\n```memory-memo\n{\n \"userNeed\": \"<the user's need or goal, one sentence>\",\n \"approach\": \"<what was done — the approach taken, 2-4 sentences>\",\n \"outcome\": \"<final result, e.g. '完成', '部分完成', '失败: reason'>\",\n \"whatFailed\": \"<dead ends tried — things that didn't work, or 'none'>\",\n \"whatWorked\": \"<key actions that ultimately worked, or 'none'>\",\n \"tags\": [\"<tag1>\", \"<tag2>\", \"<tag3>\"]\n}\n```\n\nGuidelines:\n- Record important failed attempts in \"whatFailed\" to help avoid repeating mistakes.\n- Record key successful actions in \"whatWorked\" to help reuse effective approaches.\n- Include 3-5 semantic \"tags\" summarizing the task domain, tech stack, or action type.\n- Skip in-progress work unless it contains a valuable error+fix experience.\n- Merge closely related sub-tasks into a single record.\n- Use the exact field names and JSON format shown above.\n\nIf no completed task loops are found in the new compacted messages, output:\n```memory-memo\n{\"none\": true}\n```\n\n<!-- Update Rules -->\n\n1. Preserve the section structure of the previous summary (Current Focus, Environment, Completed Tasks, Active Issues, Code State, Important Context, All User Messages).\n2. Move newly-completed tasks from \"Current Focus\" / \"Active Issues\" into \"Completed Tasks\".\n3. Update \"Current Focus\" to reflect what is being worked on RIGHT NOW.\n4. Append new user messages to \"All User Messages\" — do not repeat those already captured.\n5. Refresh \"Code State\" code snippets only if newer versions exist in the new messages.\n6. Drop resolved issues from \"Active Issues\"; add newly-discovered ones.\n7. Do not invent new information. If the new messages say nothing about a section, carry the previous content forward unchanged.\n\n<!-- Required Output Structure -->\n\n## Current Focus\n\n[What we're working on now]\n\n## Environment\n\n- [Key setup/config points]\n- ...\n\n## Completed Tasks\n\n- [Task]: [Brief outcome]\n- ...\n\n## Active Issues\n\n- [Issue]: [Status/Next steps]\n- ...\n\n## Code State\n\n### [Critical file name]\n\n[Brief description of the file's purpose and current state]\n\n```\n[The latest version of critical code snippets in this file, <20 lines]\n```\n\n## Important Context\n\n- [Any crucial information not covered above]\n- ...\n\n## All User Messages\n\n- [Detailed non tool use user message]\n- ...\n";
78405
+ //#endregion
78396
78406
  //#region ../../packages/agent-core/src/agent/compaction/render-messages.ts
78407
+ const TOOL_RESULT_MAX_CHARS = 2e3;
78408
+ const TOOL_CALL_MAX_CHARS = 2e3;
78409
+ const TRUNCATE_HEAD_RATIO = .6;
78410
+ function truncateForSummary(text, maxChars, headRatio = TRUNCATE_HEAD_RATIO) {
78411
+ if (text.length <= maxChars) return text;
78412
+ const headChars = Math.round(maxChars * Math.min(Math.max(headRatio, 0), 1));
78413
+ const tailChars = maxChars - headChars;
78414
+ const elided = text.length - maxChars;
78415
+ const tail = tailChars > 0 ? text.slice(-tailChars) : "";
78416
+ return `${text.slice(0, headChars)} […${elided}ch elided…] ${tail}`;
78417
+ }
78397
78418
  function renderMessagesToText(messages) {
78398
78419
  return messages.map((message, index) => renderMessageToText(message, index)).join("\n\n");
78399
78420
  }
@@ -78413,8 +78434,8 @@ function renderMessageToText(message, index) {
78413
78434
  }
78414
78435
  function renderContentPartToText(part) {
78415
78436
  switch (part.type) {
78416
- case "text": return renderBlock("text", part.text);
78417
- case "think": return renderBlock("think", part.think);
78437
+ case "text": return renderBlock("text", truncateForSummary(part.text, TOOL_RESULT_MAX_CHARS));
78438
+ case "think": return renderBlock("think", truncateForSummary(part.think, TOOL_RESULT_MAX_CHARS));
78418
78439
  case "image_url": return renderMediaPart("image_url", part.imageUrl.url, part.imageUrl.id);
78419
78440
  case "audio_url": return renderMediaPart("audio_url", part.audioUrl.url, part.audioUrl.id);
78420
78441
  case "video_url": return renderMediaPart("video_url", part.videoUrl.url, part.videoUrl.id);
@@ -78428,11 +78449,13 @@ function renderToolCallToText(toolCall) {
78428
78449
  }
78429
78450
  function renderToolCallArguments(args) {
78430
78451
  if (args === null) return "null";
78452
+ let pretty;
78431
78453
  try {
78432
- return stringifyJsonish(JSON.parse(args));
78454
+ pretty = stringifyJsonish(JSON.parse(args));
78433
78455
  } catch {
78434
- return args;
78456
+ pretty = args;
78435
78457
  }
78458
+ return truncateForSummary(pretty, TOOL_CALL_MAX_CHARS);
78436
78459
  }
78437
78460
  function renderMediaPart(type, url, id) {
78438
78461
  if (id === void 0) return `${type}: ${url}`;
@@ -78471,7 +78494,7 @@ const DEFAULT_COMPACTION_CONFIG = {
78471
78494
  reservedContextSize: 5e4,
78472
78495
  maxCompactionPerTurn: 3,
78473
78496
  maxRecentMessages: 4,
78474
- maxRecentUserMessages: Infinity,
78497
+ maxRecentUserMessages: 2,
78475
78498
  maxRecentSizeRatio: .2,
78476
78499
  minOverflowReductionRatio: .05,
78477
78500
  turnGrowthMultiplier: 2.5
@@ -78567,6 +78590,117 @@ function canSplitAfter(messages, index) {
78567
78590
  if (messages[index + 1]?.role === "tool") return false;
78568
78591
  return true;
78569
78592
  }
78593
+ //#endregion
78594
+ //#region ../../packages/agent-core/src/agent/compaction/file-operations.ts
78595
+ function createFileOps() {
78596
+ return {
78597
+ read: /* @__PURE__ */ new Set(),
78598
+ written: /* @__PURE__ */ new Set(),
78599
+ edited: /* @__PURE__ */ new Set()
78600
+ };
78601
+ }
78602
+ function extractFileOpsFromMessage(message, ops) {
78603
+ if (message.role !== "assistant") return;
78604
+ for (const call of message.toolCalls) {
78605
+ if (call.type !== "function" || call.arguments === null) continue;
78606
+ let args;
78607
+ try {
78608
+ args = JSON.parse(call.arguments);
78609
+ } catch {
78610
+ continue;
78611
+ }
78612
+ const path = typeof args["path"] === "string" ? args["path"] : void 0;
78613
+ if (path === void 0 || path.length === 0) continue;
78614
+ switch (call.name) {
78615
+ case "Read":
78616
+ ops.read.add(path);
78617
+ break;
78618
+ case "Edit":
78619
+ ops.edited.add(path);
78620
+ break;
78621
+ case "Write":
78622
+ ops.written.add(path);
78623
+ break;
78624
+ }
78625
+ }
78626
+ }
78627
+ const FILE_LIMIT = 20;
78628
+ function formatFileOperations(ops) {
78629
+ const modified = new Set([...ops.edited, ...ops.written]);
78630
+ const readOnly = [...ops.read].filter((f) => !modified.has(f)).sort();
78631
+ const modifiedFiles = [...modified].sort();
78632
+ const all = [...new Set([...readOnly, ...modifiedFiles])].sort();
78633
+ if (all.length === 0) return "";
78634
+ const mode = /* @__PURE__ */ new Map();
78635
+ for (const f of readOnly) mode.set(f, "Read");
78636
+ for (const f of modifiedFiles) mode.set(f, ops.read.has(f) ? "RW" : "Write");
78637
+ const lines = ["<files>"];
78638
+ for (const f of all.slice(0, FILE_LIMIT)) lines.push(`${f} (${mode.get(f)})`);
78639
+ if (all.length > FILE_LIMIT) lines.push(`[…${all.length - FILE_LIMIT} files elided…]`);
78640
+ lines.push("</files>");
78641
+ return lines.join("\n");
78642
+ }
78643
+ /** Max recursion depth for re-summarize fallback. Each level halves the
78644
+ * input, so depth 3 means we can compress a 8x-oversized input down to a
78645
+ * single summary by chaining 2^3 = 8 partial summaries. */
78646
+ const MAX_RE_SUMMARIZE_DEPTH = 3;
78647
+ var TruncatedError = class extends Error {};
78648
+ /**
78649
+ * Recursive re-summarize fallback for context overflow. When even the
78650
+ * minimum safe split still overflows the model (typically because a single
78651
+ * message contains a giant tool result), split the input in half at a safe
78652
+ * boundary, summarize each half, then concatenate the two partial summaries
78653
+ * and re-summarize them into one. If a half still overflows, recurse.
78654
+ *
78655
+ * The split uses `canSplitAfter` from the strategy to make sure each half
78656
+ * ends at a message boundary that doesn't orphan tool results. If no safe
78657
+ * split exists in the half (e.g. one giant message), feed it as-is to the
78658
+ * model and let the outer retry loop handle the overflow.
78659
+ */
78660
+ async function summarizeWithFallback(messages, summarizeOnce, depth = 0) {
78661
+ if (messages.length <= 1) return summarizeOnce(messages);
78662
+ let split = -1;
78663
+ const mid = Math.floor(messages.length / 2);
78664
+ for (let i = mid; i > 0; i--) if (canSplitAfterContext(messages, i - 1)) {
78665
+ split = i;
78666
+ break;
78667
+ }
78668
+ if (split === -1) {
78669
+ for (let i = mid + 1; i < messages.length; i++) if (canSplitAfterContext(messages, i - 1)) {
78670
+ split = i;
78671
+ break;
78672
+ }
78673
+ }
78674
+ if (split === -1) return summarizeOnce(messages);
78675
+ const firstHalf = messages.slice(0, split);
78676
+ const secondHalf = messages.slice(split);
78677
+ const summarizeHalf = async (half) => {
78678
+ try {
78679
+ return (await summarizeOnce(half)).summary;
78680
+ } catch (error) {
78681
+ if ((error instanceof APIContextOverflowError || error instanceof TruncatedError) && depth + 1 < MAX_RE_SUMMARIZE_DEPTH) return (await summarizeWithFallback(half, summarizeOnce, depth + 1)).summary;
78682
+ throw error;
78683
+ }
78684
+ };
78685
+ return summarizeOnce([{
78686
+ role: "user",
78687
+ content: [{
78688
+ type: "text",
78689
+ text: `${await summarizeHalf(firstHalf)}\n\n---\n\n${await summarizeHalf(secondHalf)}`
78690
+ }],
78691
+ toolCalls: []
78692
+ }]);
78693
+ }
78694
+ /** Same split-safety rule as DefaultCompactionStrategy.canSplitAfter, but
78695
+ * operates on ContextMessage (which carries toolCalls on assistant msgs). */
78696
+ function canSplitAfterContext(messages, index) {
78697
+ const m = messages[index];
78698
+ if (m === void 0) return false;
78699
+ if (m.role === "user") return false;
78700
+ if (m.role === "assistant" && m.toolCalls.length > 0) return false;
78701
+ if (messages[index + 1]?.role === "tool") return false;
78702
+ return true;
78703
+ }
78570
78704
  /** Max consecutive compaction failures before auto-compaction is
78571
78705
  * disabled for the remainder of the turn. Resets each turn. */
78572
78706
  const MAX_CONSECUTIVE_FAILURES = 3;
@@ -78582,6 +78716,16 @@ var FullCompaction = class {
78582
78716
  consecutiveCompactionFailures = 0;
78583
78717
  _shouldInjectSessionSummary = false;
78584
78718
  compactionTimedOut = false;
78719
+ /** Token count below which compaction should not re-trigger. Set after a
78720
+ * successful compaction to 110% of the post-compaction token count, so
78721
+ * that a context sitting just above triggerRatio doesn't immediately
78722
+ * re-trigger on every step. Reset each turn. */
78723
+ lowWaterMark = 0;
78724
+ /** Whether a reactive (overflow-triggered) compaction has already been
78725
+ * attempted this turn. Prevents the overflow → compact → still near
78726
+ * limit → overflow → compact cycle from consuming the entire
78727
+ * maxCompactionPerTurn budget with marginal savings. */
78728
+ reactiveAttempted = false;
78585
78729
  compacting = null;
78586
78730
  _compactedHistory = [];
78587
78731
  strategy;
@@ -78660,8 +78804,11 @@ var FullCompaction = class {
78660
78804
  resetForTurn() {
78661
78805
  this.compactionCountInTurn = 0;
78662
78806
  this.consecutiveCompactionFailures = 0;
78807
+ this.lowWaterMark = 0;
78808
+ this.reactiveAttempted = false;
78663
78809
  }
78664
78810
  async handleOverflowError(signal, error) {
78811
+ if (this.reactiveAttempted) throw error;
78665
78812
  if (!this.beginAutoCompaction(false) && !this.compacting) {
78666
78813
  if (this.consecutiveCompactionFailures >= MAX_CONSECUTIVE_FAILURES) this.agent.emitEvent({
78667
78814
  type: "warning",
@@ -78670,11 +78817,12 @@ var FullCompaction = class {
78670
78817
  });
78671
78818
  throw error;
78672
78819
  }
78820
+ this.reactiveAttempted = true;
78673
78821
  }
78674
78822
  async beforeStep(signal) {
78675
78823
  this.agent.microCompaction.detect();
78676
78824
  const effectiveTokens = this.effectiveTokenCount;
78677
- const isReactiveTrigger = this.strategy.shouldCompact(effectiveTokens);
78825
+ const isReactiveTrigger = this.strategy.shouldCompact(effectiveTokens) && effectiveTokens >= this.lowWaterMark;
78678
78826
  const isProactiveTrigger = !isReactiveTrigger && this.strategy.shouldCompactProactively(effectiveTokens, this.estimatedMaxOutputTokens);
78679
78827
  if (isReactiveTrigger) this.checkAutoCompaction();
78680
78828
  else if (isProactiveTrigger) this.beginAutoCompaction();
@@ -78697,7 +78845,9 @@ var FullCompaction = class {
78697
78845
  }
78698
78846
  checkAutoCompaction(throwOnLimit = true) {
78699
78847
  if (this.compacting) return true;
78700
- if (!this.strategy.shouldCompact(this.tokenCountWithPending)) return false;
78848
+ const effectiveTokens = this.effectiveTokenCount;
78849
+ if (!this.strategy.shouldCompact(effectiveTokens)) return false;
78850
+ if (effectiveTokens < this.lowWaterMark) return false;
78701
78851
  return this.beginAutoCompaction(throwOnLimit);
78702
78852
  }
78703
78853
  beginAutoCompaction(throwOnLimit = true) {
@@ -78744,32 +78894,52 @@ var FullCompaction = class {
78744
78894
  const originalHistory = [...this.agent.context.history];
78745
78895
  const tokensBefore = estimateTokensForMessages(originalHistory);
78746
78896
  const model = this.agent.config.model;
78897
+ const isUpdate = extractPreviousSummary(originalHistory) !== null;
78747
78898
  let retryCount = 0;
78748
78899
  try {
78749
78900
  await this.triggerPreCompactHook(data, tokensBefore, signal);
78750
78901
  const delays = retryBackoffDelays(5);
78751
- let usage;
78752
- let summary;
78753
- while (true) {
78754
- const messagesToCompact = originalHistory.slice(0, compactedCount);
78902
+ const summarizeOnce = async (messagesToCompact) => {
78903
+ const instruction = isUpdate ? COMPACTION_UPDATE_INSTRUCTION(data.instruction) : COMPACTION_INSTRUCTION(data.instruction);
78755
78904
  const messages = [...project(messagesToCompact), {
78756
78905
  role: "user",
78757
78906
  content: [{
78758
78907
  type: "text",
78759
- text: COMPACTION_INSTRUCTION(data.instruction)
78908
+ text: instruction
78760
78909
  }],
78761
78910
  toolCalls: []
78762
78911
  }];
78763
- class TruncatedError extends Error {}
78912
+ const response = await this.agent.generate(this.agent.config.provider, COMPACTION_SYSTEM_PROMPT, [], messages, void 0, { signal });
78913
+ if (response.finishReason === "truncated") throw new TruncatedError();
78914
+ return {
78915
+ summary: extractCompactionSummary(response, model),
78916
+ usage: response.usage
78917
+ };
78918
+ };
78919
+ let usage;
78920
+ let summary;
78921
+ while (true) {
78922
+ const messagesToCompact = originalHistory.slice(0, compactedCount);
78764
78923
  try {
78765
- const response = await this.agent.generate(this.agent.config.provider, COMPACTION_SYSTEM_PROMPT, [], messages, void 0, { signal });
78766
- if (response.finishReason === "truncated") throw new TruncatedError();
78767
- usage = response.usage;
78768
- summary = extractCompactionSummary(response, model);
78924
+ const result = await summarizeOnce(messagesToCompact);
78925
+ usage = result.usage;
78926
+ summary = result.summary;
78769
78927
  break;
78770
78928
  } catch (error) {
78771
- if (error instanceof APIContextOverflowError || error instanceof TruncatedError) compactedCount = this.strategy.reduceCompactOnOverflow(messagesToCompact);
78772
- else if (!isRetryableGenerateError(error)) throw error;
78929
+ if (error instanceof APIContextOverflowError || error instanceof TruncatedError) {
78930
+ const reduced = this.strategy.reduceCompactOnOverflow(messagesToCompact);
78931
+ if (reduced < compactedCount) compactedCount = reduced;
78932
+ else {
78933
+ this.agent.log.warn("compaction overflow at minimum split, falling back to re-summarize", {
78934
+ compactedCount,
78935
+ tokensBefore: estimateTokensForMessages(messagesToCompact)
78936
+ });
78937
+ const result = await summarizeWithFallback(messagesToCompact, summarizeOnce);
78938
+ summary = result.summary;
78939
+ usage = result.usage;
78940
+ break;
78941
+ }
78942
+ } else if (!isRetryableGenerateError(error)) throw error;
78773
78943
  if (retryCount + 1 >= 5) throw error;
78774
78944
  await sleepForRetry(delays[retryCount], signal);
78775
78945
  retryCount += 1;
@@ -78782,12 +78952,17 @@ var FullCompaction = class {
78782
78952
  return;
78783
78953
  }
78784
78954
  const recent = originalHistory.slice(compactedCount);
78785
- const tokensAfter = estimateTokens$1(summary) + estimateTokensForMessages(recent);
78955
+ const messagesToCompactForOps = originalHistory.slice(0, compactedCount);
78956
+ const fileOps = createFileOps();
78957
+ for (const msg of messagesToCompactForOps) extractFileOpsFromMessage(msg, fileOps);
78958
+ const processedSummary = this.postProcessSummary(summary, fileOps);
78959
+ const tokensAfter = estimateTokens$1(processedSummary) + estimateTokensForMessages(recent);
78786
78960
  const result = {
78787
- summary,
78961
+ summary: processedSummary,
78788
78962
  compactedCount,
78789
78963
  tokensBefore,
78790
- tokensAfter
78964
+ tokensAfter,
78965
+ ...isUpdate ? { isUpdate: true } : {}
78791
78966
  };
78792
78967
  this.markCompleted();
78793
78968
  this.agent.emitEvent({
@@ -78795,7 +78970,8 @@ var FullCompaction = class {
78795
78970
  result
78796
78971
  });
78797
78972
  this.agent.context.applyCompaction(result);
78798
- await this.extractAndStoreMemos(summary);
78973
+ this.lowWaterMark = Math.floor(this.effectiveTokenCount * 1.1);
78974
+ await this.extractAndStoreMemos(processedSummary);
78799
78975
  this.triggerPostCompactHook(data, result);
78800
78976
  this.consecutiveCompactionFailures = 0;
78801
78977
  this._shouldInjectSessionSummary = true;
@@ -78887,22 +79063,27 @@ var FullCompaction = class {
78887
79063
  });
78888
79064
  }
78889
79065
  /**
78890
- * Append the current todo list as a markdown section to the compaction
78891
- * summary so active tasks survive compression. This mirrors kimi-code's
78892
- * approach: without it, the todo list is lost after compaction because
78893
- * the original messages containing it are removed from the context window.
79066
+ * Append the current todo list and file operations as markdown sections to
79067
+ * the compaction summary so active tasks and file context survive
79068
+ * compression. Without this, both are lost after compaction because the
79069
+ * original messages containing them are removed from the context window.
78894
79070
  */
78895
- postProcessSummary(summary) {
79071
+ postProcessSummary(summary, fileOps) {
78896
79072
  const todos = this.agent.tools.storeData()["todo"] ?? [];
78897
- if (todos.length === 0) return summary;
78898
- const todoMarkdown = [
78899
- "## TODO List",
78900
- "",
78901
- ...todos.map((t) => {
79073
+ const sections = [summary.trim()];
79074
+ if (todos.length > 0) {
79075
+ const lines = todos.map((t) => {
78902
79076
  return `- [${t.status === "done" ? "x" : t.status === "in_progress" ? "-" : " "}] ${t.title}`;
78903
- })
78904
- ].join("\n");
78905
- return `${summary.trim()}\n\n${todoMarkdown}`;
79077
+ });
79078
+ sections.push([
79079
+ "## TODO List",
79080
+ "",
79081
+ ...lines
79082
+ ].join("\n"));
79083
+ }
79084
+ const filesSection = formatFileOperations(fileOps);
79085
+ if (filesSection.length > 0) sections.push(filesSection);
79086
+ return sections.join("\n\n");
78906
79087
  }
78907
79088
  };
78908
79089
  function extractCompactionSummary(response, model) {
@@ -78911,6 +79092,18 @@ function extractCompactionSummary(response, model) {
78911
79092
  return summary;
78912
79093
  }
78913
79094
  const COMPACTION_INSTRUCTION = (customInstruction = "") => renderPrompt(compaction_instruction_default, { customInstruction });
79095
+ const COMPACTION_UPDATE_INSTRUCTION = (customInstruction = "") => renderPrompt(compaction_update_instruction_default, { customInstruction });
79096
+ /**
79097
+ * If history starts with a compaction_summary message, return its text so the
79098
+ * next compaction can merge new content into it instead of starting fresh.
79099
+ * Returns null when no prior summary exists (first compaction in the session).
79100
+ */
79101
+ function extractPreviousSummary(history) {
79102
+ const head = history[0];
79103
+ if (head?.origin?.kind !== "compaction_summary") return null;
79104
+ const text = head.content.filter((p) => p.type === "text").map((p) => p.text).join("");
79105
+ return text.length > 0 ? text : null;
79106
+ }
78914
79107
  //#endregion
78915
79108
  //#region ../../packages/agent-core/src/flags/registry.ts
78916
79109
  /**
@@ -78993,11 +79186,33 @@ const flags = new FlagResolver();
78993
79186
  //#region ../../packages/agent-core/src/agent/compaction/micro.ts
78994
79187
  const DEFAULT_CONFIG = {
78995
79188
  keepRecentMessages: 20,
79189
+ keepRecentTokens: 4e4,
79190
+ pruneMinReclaimTokens: 2e4,
78996
79191
  minContentTokens: 100,
78997
79192
  minContextUsageRatio: .5,
78998
- truncatedMarker: "[Old tool result content cleared]"
79193
+ truncatedMarker: "[Old tool result content cleared]",
79194
+ uselessMarker: "[Uneventful result elided]"
78999
79195
  };
79000
79196
  /**
79197
+ * Compute the cutoff index: everything at index < cutoff is eligible for
79198
+ * truncation. The default floor is `keepRecentMessages` (the message-count
79199
+ * protection window). But if the trailing window exceeds `keepRecentTokens`,
79200
+ * the cutoff walks forward (toward the tail) until the window fits — so a
79201
+ * few giant tool results can't pin the cutoff behind them and starve the
79202
+ * prefix of reclaimable content.
79203
+ */
79204
+ function computeCutoff(messages, config) {
79205
+ const messageFloor = Math.max(0, messages.length - config.keepRecentMessages);
79206
+ let windowTokens = estimateTokensForMessages(messages.slice(messageFloor));
79207
+ let cutoff = messageFloor;
79208
+ while (cutoff < messages.length && windowTokens > config.keepRecentTokens) {
79209
+ const removed = messages[cutoff];
79210
+ windowTokens -= estimateTokensForMessages([removed]);
79211
+ cutoff += 1;
79212
+ }
79213
+ return cutoff;
79214
+ }
79215
+ /**
79001
79216
  * Walk the message list and find Read tool calls whose file paths were
79002
79217
  * superseded by a later Read of the same path. Returns a map from the
79003
79218
  * superseded tool call's ID to the file path (for the marker text).
@@ -79047,8 +79262,8 @@ var MicroCompaction = class {
79047
79262
  };
79048
79263
  }
79049
79264
  /** Reset the internal cutoff line (e.g. after a full compaction). */
79050
- reset(maxCutoff = 0) {
79051
- this.cutoff = Math.min(this.cutoff, maxCutoff);
79265
+ reset() {
79266
+ this.cutoff = 0;
79052
79267
  }
79053
79268
  /** Advance the cutoff line and log the change. */
79054
79269
  apply(cutoff) {
@@ -79066,14 +79281,19 @@ var MicroCompaction = class {
79066
79281
  const maxContextTokens = this.agent.config.modelCapabilities.max_context_tokens;
79067
79282
  const contextTokens = this.agent.context.tokenCountWithPending;
79068
79283
  if ((maxContextTokens !== void 0 && maxContextTokens > 0 ? contextTokens / maxContextTokens : 0) < config.minContextUsageRatio) return;
79069
- const nextCutoff = Math.max(0, history.length - config.keepRecentMessages);
79284
+ const nextCutoff = computeCutoff(history, config);
79285
+ if (nextCutoff <= this.cutoff) return;
79286
+ const { beforeTokens, afterTokens } = this.measureEffect(history, nextCutoff);
79287
+ if (beforeTokens - afterTokens < config.pruneMinReclaimTokens) return;
79070
79288
  this.apply(nextCutoff);
79071
79289
  }
79072
79290
  /**
79073
79291
  * Apply micro-compaction to a message list: replace old tool results
79074
79292
  * before the cutoff line with truncated markers. Read results for files
79075
79293
  * that were re-read later get a supersede marker so the model knows
79076
- * the old content is stale.
79294
+ * the old content is stale. Tool results explicitly marked useless are
79295
+ * elided with a short notice regardless of size, since they carry no
79296
+ * actionable information.
79077
79297
  */
79078
79298
  compact(messages) {
79079
79299
  const config = this.config;
@@ -79081,7 +79301,16 @@ var MicroCompaction = class {
79081
79301
  const result = [];
79082
79302
  let i = 0;
79083
79303
  for (const msg of messages) {
79084
- if (i < this.cutoff && msg.role === "tool" && msg.toolCallId !== void 0 && estimateTokensForMessages([msg]) >= config.minContentTokens) {
79304
+ const isUseless = i < this.cutoff && msg.role === "tool" && msg.toolCallId !== void 0 && msg.useless === true;
79305
+ const isOversizedTruncatable = i < this.cutoff && msg.role === "tool" && msg.toolCallId !== void 0 && estimateTokensForMessages([msg]) >= config.minContentTokens;
79306
+ if (isUseless) result.push({
79307
+ ...msg,
79308
+ content: [{
79309
+ type: "text",
79310
+ text: config.uselessMarker
79311
+ }]
79312
+ });
79313
+ else if (isOversizedTruncatable) {
79085
79314
  const marker = msg.toolCallId !== void 0 && superseded.has(msg.toolCallId) ? `[Superseded by a newer read of ${superseded.get(msg.toolCallId)}]` : config.truncatedMarker;
79086
79315
  result.push({
79087
79316
  ...msg,
@@ -79106,6 +79335,7 @@ var MicroCompaction = class {
79106
79335
  }
79107
79336
  measureEffect(messages, cutoff) {
79108
79337
  let markerTokenCount;
79338
+ let uselessMarkerTokenCount;
79109
79339
  let truncatedToolResultCount = 0;
79110
79340
  let beforeTokens = 0;
79111
79341
  let afterTokens = 0;
@@ -79113,11 +79343,19 @@ var MicroCompaction = class {
79113
79343
  const message = messages[i];
79114
79344
  if (message?.role !== "tool" || message.toolCallId === void 0) continue;
79115
79345
  const contentTokens = estimateTokensForMessages([message]);
79116
- if (contentTokens < this.config.minContentTokens) continue;
79117
- markerTokenCount ??= estimateTokens$1(this.config.truncatedMarker);
79118
- truncatedToolResultCount += 1;
79119
- beforeTokens += contentTokens;
79120
- afterTokens += markerTokenCount;
79346
+ const isUseless = message.useless === true;
79347
+ if (!isUseless && contentTokens < this.config.minContentTokens) continue;
79348
+ if (isUseless) {
79349
+ uselessMarkerTokenCount ??= estimateTokens$1(this.config.uselessMarker);
79350
+ truncatedToolResultCount += 1;
79351
+ beforeTokens += contentTokens;
79352
+ afterTokens += uselessMarkerTokenCount;
79353
+ } else {
79354
+ markerTokenCount ??= estimateTokens$1(this.config.truncatedMarker);
79355
+ truncatedToolResultCount += 1;
79356
+ beforeTokens += contentTokens;
79357
+ afterTokens += markerTokenCount;
79358
+ }
79121
79359
  }
79122
79360
  return {
79123
79361
  truncatedToolResultCount,
@@ -80326,7 +80564,8 @@ var ContextMemory = class {
80326
80564
  this.pushHistory({
80327
80565
  ...message,
80328
80566
  role: "tool",
80329
- isError: event.result.isError
80567
+ isError: event.result.isError,
80568
+ useless: event.result.isError !== true && event.result.useless === true ? true : void 0
80330
80569
  });
80331
80570
  this.pendingToolResultIds.delete(event.toolCallId);
80332
80571
  this.flushDeferredMessagesIfToolExchangeClosed();
@@ -122014,7 +122253,7 @@ function optionalBuildString(value) {
122014
122253
  return typeof value === "string" && value.length > 0 ? value : void 0;
122015
122254
  }
122016
122255
  const SCREAM_BUILD_INFO = {
122017
- version: optionalBuildString("0.8.1"),
122256
+ version: optionalBuildString("0.8.3"),
122018
122257
  channel: optionalBuildString(""),
122019
122258
  commit: optionalBuildString(""),
122020
122259
  buildTarget: optionalBuildString("darwin-arm64")
@@ -122173,7 +122412,7 @@ function errorMessage(error) {
122173
122412
  //#region src/cli/commands.ts
122174
122413
  function createProgram(version, onMain, onMigrate, onPluginNodeRunner = () => {}, onStreamJson = () => {}, onChannelSetup = () => {}) {
122175
122414
  const program = new Command(CLI_COMMAND_NAME).description("下一代智能体的起点").version(version, "-V, --version").allowUnknownOption(false).configureHelp({ helpWidth: 100 }).helpOption("-h, --help", "显示帮助。").addHelpText("after", "\n文档: https://scream-cli.github.io/scream-code/\n");
122176
- program.addOption(new Option("-S, --session [id]", "恢复会话。带 ID:恢复该会话。不带 ID:交互式选择。").argParser((val) => val === true ? "" : val)).addOption(new Option("-r, --resume [id]").hideHelp().argParser((val) => val === true ? "" : val)).option("-C, --continue", "继续当前工作目录的上一个会话。", false).option("-y, --yolo", "自动批准所有操作。", false).option("--auto", "以自动权限模式启动。", false).addOption(new Option("-m, --model <model>", "本次调用使用的 LLM 模型别名。默认使用 config.toml 中的 default_model。")).addOption(new Option("-p, --prompt <prompt>", "非交互式运行一条提示并打印响应。")).addOption(new Option("--output-format <format>", "提示模式的输出格式。默认为 text。").choices(["text", "stream-json"])).addOption(new Option("--skills-dir <dir>", "从该目录加载技能,而不是自动发现的用户和项目目录。可多次指定。").argParser((value, previous) => [...previous ?? [], value]).default([])).addOption(new Option("--yes").hideHelp().default(false)).addOption(new Option("--auto-approve").hideHelp().default(false)).option("--plan", "以计划模式启动。", false);
122415
+ program.addOption(new Option("-S, --session [id]", "恢复会话。带 ID:恢复该会话。不带 ID:交互式选择。").argParser((val) => val === true ? "" : val)).addOption(new Option("-r, --resume [id]").hideHelp().argParser((val) => val === true ? "" : val)).option("-C, --continue", "继续当前工作目录的上一个会话。", false).option("-y, --yolo", "自动批准所有操作。", false).option("--auto", "以自动权限模式启动。", false).addOption(new Option("-m, --model <model>", "本次调用使用的 LLM 模型别名。默认使用 config.toml 中的 default_model。")).addOption(new Option("-p, --prompt <prompt>", "非交互式运行一条提示并打印响应。")).addOption(new Option("--output-format <format>", "提示模式的输出格式。默认为 text。").choices(["text", "stream-json"])).addOption(new Option("--skills-dir <dir>", "从该目录加载技能,而不是自动发现的用户和项目目录。可多次指定。").argParser((value, previous) => [...previous ?? [], value]).default([])).addOption(new Option("--yes").hideHelp().default(false)).addOption(new Option("--auto-approve").hideHelp().default(false)).option("--plan", "以计划模式启动。", false).option("--wolfpack", "启动时默认开启 WolfPack 批量并发模式。", false);
122177
122416
  registerExportCommand(program);
122178
122417
  registerMigrateCommand(program, onMigrate);
122179
122418
  program.command("stream-json", { hidden: true }).option("--input-format <fmt>", "stream-json").option("--output-format <fmt>", "stream-json").option("--resume <id>", "resume a previous session").option("--model <model>", "model to use").option("--permission-mode <mode>", "permission mode").option("--permission-prompt-tool <mode>", "(ignored, cc-connect compat)").option("--replay-user-messages", "(ignored, cc-connect compat)").option("--verbose", "(ignored, cc-connect compat)").option("--system-prompt <text>", "(ignored, cc-connect compat)").option("--append-system-prompt <text>", "(passed through to agent)").option("--append-system-prompt-file <path>", "(passed through to agent; file contents are read and merged)").option("--allowedTools <list>", "(ignored, cc-connect compat)").option("--disallowedTools <list>", "(ignored, cc-connect compat)").option("--effort <value>", "(ignored, cc-connect compat)").option("--max-context-tokens <N>", "(ignored, cc-connect compat)").option("--skills-dir <dir>", "additional skills directory (repeatable)", (value, previous) => [...previous ?? [], value], []).option("--plugin-dir <dir>", "(ignored, cc-connect compat; repeatable)", (value, previous) => [...previous ?? [], value], []).action((subOpts) => {
@@ -122204,6 +122443,7 @@ function createProgram(version, onMain, onMigrate, onPluginNodeRunner = () => {}
122204
122443
  yolo: yoloValue,
122205
122444
  auto: autoValue,
122206
122445
  plan: raw["plan"],
122446
+ wolfpack: raw["wolfpack"],
122207
122447
  model: raw["model"],
122208
122448
  outputFormat: raw["outputFormat"],
122209
122449
  prompt: raw["prompt"],
@@ -122229,12 +122469,14 @@ function validateOptions(opts) {
122229
122469
  if (promptMode && opts.yolo) throw new OptionConflictError("--prompt 不能与 --yolo 同时使用。");
122230
122470
  if (promptMode && opts.auto) throw new OptionConflictError("--prompt 不能与 --auto 同时使用。");
122231
122471
  if (promptMode && opts.plan) throw new OptionConflictError("--prompt 不能与 --plan 同时使用。");
122472
+ if (promptMode && opts.wolfpack) throw new OptionConflictError("--prompt 不能与 --wolfpack 同时使用。");
122232
122473
  if (promptMode && opts.session === "") throw new OptionConflictError("在提示模式下不能使用不带 ID 的 --session。");
122233
122474
  if (opts.continue && opts.session !== void 0) throw new OptionConflictError("--continue 和 --session 不能同时使用。");
122234
122475
  if (opts.yolo && opts.auto) throw new OptionConflictError("--yolo 不能与 --auto 同时使用。");
122235
122476
  if (!promptMode && (opts.continue || opts.session !== void 0) && opts.yolo) throw new OptionConflictError("--yolo 不能与 --continue 或 --session 同时使用。");
122236
122477
  if (!promptMode && (opts.continue || opts.session !== void 0) && opts.auto) throw new OptionConflictError("--auto 不能与 --continue 或 --session 同时使用。");
122237
122478
  if (!promptMode && (opts.continue || opts.session !== void 0) && opts.plan) throw new OptionConflictError("--plan 不能与 --continue 或 --session 同时使用。");
122479
+ if (!promptMode && (opts.continue || opts.session !== void 0) && opts.wolfpack) throw new OptionConflictError("--wolfpack 不能与 --continue 或 --session 同时使用。");
122238
122480
  return {
122239
122481
  options: opts,
122240
122482
  uiMode: promptMode ? "print" : "shell"
@@ -132838,6 +133080,517 @@ async function handleLikeCommand(host) {
132838
133080
  host.showStatus("偏好已保存(下次新会话生效)", host.state.theme.colors.success);
132839
133081
  }
132840
133082
  //#endregion
133083
+ //#region src/tui/utils/open-url.ts
133084
+ function openUrl(url) {
133085
+ const command = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", [
133086
+ "/c",
133087
+ "start",
133088
+ "",
133089
+ url
133090
+ ]] : ["xdg-open", [url]];
133091
+ execFile(command[0], command[1], () => {});
133092
+ }
133093
+ //#endregion
133094
+ //#region src/tui/commands/knowledge-store.ts
133095
+ let knowledgeStoreInstance;
133096
+ async function getKnowledgeStore() {
133097
+ if (knowledgeStoreInstance === void 0) {
133098
+ knowledgeStoreInstance = new KnowledgeStore(getDataDir());
133099
+ await knowledgeStoreInstance.init();
133100
+ knowledgeStoreInstance.setEmbeddingEngine(createFastEmbedEngine());
133101
+ }
133102
+ return knowledgeStoreInstance;
133103
+ }
133104
+ //#endregion
133105
+ //#region src/tui/commands/knowledge-web.ts
133106
+ /**
133107
+ * /knowledge web — start a local HTTP server and open the browser
133108
+ * to display an interactive Cytoscape.js knowledge graph visualization.
133109
+ */
133110
+ const activeServers = /* @__PURE__ */ new Set();
133111
+ function registerServer(server) {
133112
+ activeServers.add(server);
133113
+ server.on("close", () => {
133114
+ activeServers.delete(server);
133115
+ });
133116
+ }
133117
+ function closeAllServers() {
133118
+ for (const server of activeServers) server.close();
133119
+ }
133120
+ process.on("exit", closeAllServers);
133121
+ const HTML = `<!doctype html>
133122
+ <html lang="zh-CN">
133123
+ <head>
133124
+ <meta charset="utf-8">
133125
+ <meta name="viewport" content="width=device-width,initial-scale=1">
133126
+ <title>Scream 知识图谱</title>
133127
+ <style>
133128
+ *{margin:0;padding:0;box-sizing:border-box}
133129
+ html,body{width:100%;height:100%;overflow:hidden;font-family:Inter,"PingFang SC","Microsoft YaHei",system-ui,sans-serif;background:#fff;color:#18181b}
133130
+ #container{width:100%;height:100%;position:relative;overflow:hidden}
133131
+ svg{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:1}
133132
+ .node{position:absolute;border-radius:10px;display:flex;align-items:center;justify-content:center;text-align:center;cursor:pointer;z-index:2;user-select:none;transition:opacity .25s,box-shadow .25s,border-color .25s;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;padding:0 14px;font-size:12px;line-height:1.3;animation:float 6s ease-in-out infinite}
133133
+ .node.entity{background:#fff;border:1.5px solid #d4d4d8;font-weight:600;color:#1a1a1a;box-shadow:0 2px 8px rgba(0,0,0,.06)}
133134
+ .node.entity.root{border:2px solid #15803d;background:#f0fdf4;box-shadow:0 4px 20px rgba(21,128,61,.15);font-size:14px}
133135
+ .node.entity.expanded{border-color:#15803d;box-shadow:0 2px 12px rgba(21,128,61,.1)}
133136
+ .node.event{background:#fafafa;border:1px solid #e5e7eb;font-weight:500;color:#525252;box-shadow:0 1px 4px rgba(0,0,0,.04);border-radius:8px}
133137
+ .node.event.expanded{border-color:#22c55e;background:#f8fef9;box-shadow:0 2px 8px rgba(21,128,61,.08)}
133138
+ .node.selected{border-color:#15803d !important;background:#f0fdf4 !important;box-shadow:0 0 0 3px rgba(21,128,61,.12),0 4px 16px rgba(21,128,61,.14) !important}
133139
+ .node.dimmed{opacity:.12;filter:grayscale(.6)}
133140
+ .node:hover{box-shadow:0 4px 16px rgba(0,0,0,.08) !important}
133141
+ .edge{stroke:#b0b0b0;stroke-width:.8;fill:none;opacity:.6}
133142
+ .edge.animated{stroke:#999;stroke-width:1;stroke-dasharray:6 4;animation:dash 1.2s linear infinite;opacity:.45}
133143
+ .edge.dimmed{stroke:#e0e0e0;stroke-width:.4;opacity:.06}
133144
+ .edge.highlighted{stroke:#15803d;stroke-width:1.8;opacity:1}
133145
+ @keyframes dash{to{stroke-dashoffset:-10}}
133146
+ @keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-5px)}}
133147
+ #toolbar{position:absolute;left:16px;top:16px;z-index:10;display:flex;align-items:center;gap:10px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:8px 18px;box-shadow:0 2px 8px rgba(0,0,0,.04)}
133148
+ #toolbar button{background:#fff;color:#525252;border:1px solid #d4d4d8;padding:5px 16px;border-radius:8px;cursor:pointer;font-size:12px;font-weight:500;transition:all .15s}
133149
+ #toolbar button:hover{background:#f0fdf4;border-color:#15803d;color:#15803d}
133150
+ #toolbar .sep{width:1px;height:16px;background:#e5e7eb}
133151
+ #toolbar .chip{color:#a1a1aa;font-size:11px;letter-spacing:.3px}
133152
+ #toolbar .chip b{color:#15803d;font-size:13px;font-weight:700;margin-left:4px}
133153
+ #hint{position:absolute;left:16px;bottom:16px;z-index:10;color:#c4c4c4;font-size:11px;letter-spacing:.2px}
133154
+ #detail{position:fixed;right:0;top:0;width:380px;height:100vh;background:#fff;border-left:1px solid #e5e7eb;transform:translateX(100%);transition:transform .3s cubic-bezier(.4,0,.2,1);z-index:20;overflow-y:auto;padding:32px 28px;box-shadow:-4px 0 24px rgba(0,0,0,.04)}
133155
+ #detail.open{transform:translateX(0)}
133156
+ #detail h3{font-size:17px;font-weight:700;color:#18181b;margin-bottom:20px;padding-right:32px;line-height:1.4}
133157
+ #detail .field{margin-bottom:18px}
133158
+ #detail .field .label{font-size:10px;color:#15803d;text-transform:uppercase;letter-spacing:1px;font-weight:700;margin-bottom:5px}
133159
+ #detail .field .value{font-size:13px;color:#525252;line-height:1.6}
133160
+ #detail .close{position:absolute;top:20px;right:20px;background:none;border:none;color:#c4c4c4;cursor:pointer;font-size:18px;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:8px;transition:all .15s}
133161
+ #detail .close:hover{color:#18181b;background:#f4f4f5}
133162
+ #detail .conn-item{font-size:13px;color:#15803d;cursor:pointer;padding:7px 10px;border-radius:8px;transition:all .15s;border:1px solid transparent}
133163
+ #detail .conn-item:hover{background:#f0fdf4;border-color:#bbf7d0}
133164
+ #detail .badge{font-size:10px;color:#15803d;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:5px;padding:1px 7px;margin-left:8px;font-weight:500}
133165
+ #detail .back-btn{display:inline-flex;align-items:center;gap:5px;background:none;border:1px solid #d4d4d8;color:#525252;padding:6px 14px;border-radius:8px;cursor:pointer;font-size:12px;font-weight:500;margin-bottom:16px;transition:all .15s}
133166
+ #detail .back-btn:hover{background:#f0fdf4;border-color:#15803d;color:#15803d}
133167
+ #detail .type-tag{display:inline-block;font-size:10px;color:#15803d;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:5px;padding:2px 10px;font-weight:600;margin-bottom:16px;letter-spacing:.3px}
133168
+ #detail .divider{height:1px;background:#f0f0f0;margin:18px 0}
133169
+ #loading{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:30;color:#15803d;font-size:14px;font-weight:500;display:flex;align-items:center;gap:8px}
133170
+ #loading .dot{width:6px;height:6px;background:#15803d;border-radius:50%;animation:blink 1.2s ease-in-out infinite}
133171
+ #loading .dot:nth-child(2){animation-delay:.2s}
133172
+ #loading .dot:nth-child(3){animation-delay:.4s}
133173
+ @keyframes blink{0%,80%,100%{opacity:.2}40%{opacity:1}}
133174
+ </style>
133175
+ </head>
133176
+ <body>
133177
+ <div id="container"><svg id="edges-svg"></svg></div>
133178
+ <div id="toolbar">
133179
+ <span class="chip">实体<b id="stat-ent">0</b></span>
133180
+ <span class="chip">事件<b id="stat-evt">0</b></span>
133181
+ <span class="chip">关系<b id="stat-edg">0</b></span>
133182
+ <span class="sep"></span>
133183
+ <button id="btn-reset">重置</button>
133184
+ <button id="btn-expand">展开全部</button>
133185
+ </div>
133186
+ <div id="hint">单击展开/收起 · 双击查看详情 · 滚轮缩放 · 拖拽平移</div>
133187
+ <div id="detail"><button class="close" id="btn-close">&times;</button><div id="detail-body"></div></div>
133188
+ <div id="loading"><span class="dot"></span><span class="dot"></span><span class="dot"></span>加载知识图谱...</div>
133189
+ <script>
133190
+ (function(){
133191
+ var graphData;
133192
+ var container = document.getElementById('container');
133193
+ var svg = document.getElementById('edges-svg');
133194
+
133195
+ // camera
133196
+ var cam = {x:0,y:0,zoom:1};
133197
+ var W=window.innerWidth, H=window.innerHeight;
133198
+
133199
+ function w2s(wx,wy){return{x:(wx-cam.x)*cam.zoom+W/2,y:(wy-cam.y)*cam.zoom+H/2}}
133200
+ function s2w(sx,sy){return{x:(sx-W/2)/cam.zoom+cam.x,y:(sy-H/2)/cam.zoom+cam.y}}
133201
+
133202
+ // pan & zoom
133203
+ var drag=false,moved=false,dragX,dragY;
133204
+ container.addEventListener('mousedown',function(e){if(e.target!==container&&e.target!==svg)return;drag=true;moved=false;dragX=e.clientX;dragY=e.clientY});
133205
+ window.addEventListener('mousemove',function(e){if(!drag)return;var dx=e.clientX-dragX,dy=e.clientY-dragY;if(Math.abs(dx)>2||Math.abs(dy)>2)moved=true;cam.x-=dx/cam.zoom;cam.y-=dy/cam.zoom;dragX=e.clientX;dragY=e.clientY;render()});
133206
+ window.addEventListener('mouseup',function(){drag=false});
133207
+ container.addEventListener('wheel',function(e){e.preventDefault();var pt=s2w(e.clientX,e.clientY);var f=e.deltaY<0?1.12:1/1.12;cam.zoom=Math.max(.15,Math.min(4,cam.zoom*f));cam.x=pt.x-(e.clientX-W/2)/cam.zoom;cam.y=pt.y-(e.clientY-H/2)/cam.zoom;render()},{passive:false});
133208
+ window.addEventListener('resize',function(){W=window.innerWidth;H=window.innerHeight;render()});
133209
+
133210
+ // layout constants (SAG)
133211
+ var EW=160,EH=40,EVW=170,EVH=40;
133212
+ var ER_START=220,ER_GAP=190,ER_SLOT=200;
133213
+ var VR_START=500,VR_GAP=180,VR_SLOT=220;
133214
+ var ROOT_LIMIT=8;
133215
+ var GA=Math.PI*(3-Math.sqrt(5));
133216
+
133217
+ // state
133218
+ var eById={},evById={};
133219
+ var evByEnt={},entByEv={};
133220
+ var expEnt=new Set(),expEv=new Set();
133221
+ var selId=null;
133222
+ var pos={};
133223
+ var navStack=[];
133224
+
133225
+ // node/edge DOM elements
133226
+ var nodeEls={},edgeEls=[];
133227
+
133228
+ fetch('/api/graph').then(function(r){return r.json()}).then(function(data){
133229
+ graphData=data;
133230
+ document.getElementById('stat-ent').textContent=data.entities.length;
133231
+ document.getElementById('stat-evt').textContent=data.events.length;
133232
+ document.getElementById('stat-edg').textContent=data.edges.length;
133233
+ document.getElementById('loading').style.display='none';
133234
+
133235
+ data.entities.sort(function(a,b){return(b.eventCount||0)-(a.eventCount||0)||a.name.localeCompare(b.name)});
133236
+ data.events.sort(function(a,b){return(a.rank||0)-(b.rank||0)||a.title.localeCompare(b.title)});
133237
+
133238
+ data.entities.forEach(function(e){eById[e.id]=e});
133239
+ data.events.forEach(function(e){evById[e.id]=e});
133240
+
133241
+ data.edges.forEach(function(e){
133242
+ if(!evByEnt[e.entityId])evByEnt[e.entityId]=[];
133243
+ evByEnt[e.entityId].push(e.eventId);
133244
+ if(!entByEv[e.eventId])entByEv[e.eventId]=[];
133245
+ entByEv[e.eventId].push(e.entityId);
133246
+ });
133247
+
133248
+ // default: show all entities, no events expanded
133249
+ graphData.entities.forEach(function(e){expEnt.add(e.id)});
133250
+
133251
+ buildPositions();
133252
+ fitView();
133253
+ rebuildDOM();
133254
+ });
133255
+
133256
+ function buildPositions(){
133257
+ pos={};
133258
+ var root=graphData.entities[0];
133259
+ if(root)pos[root.id]={x:-EW/2,y:-EH/2,root:true};
133260
+ var sec=graphData.entities.slice(1);
133261
+ placeRings(sec.map(function(e){return e.id}),ER_START,ER_GAP,ER_SLOT,EW,EH,-Math.PI/2);
133262
+ placeEvents(graphData.events,-Math.PI/2+Math.PI/12);
133263
+ }
133264
+
133265
+ function placeRings(ids,sr,gap,slot,nw,nh,aoff){
133266
+ var idx=0,ring=0;
133267
+ while(idx<ids.length){
133268
+ var r=sr+ring*gap;
133269
+ var cap=Math.max(6,Math.floor(2*Math.PI*r/slot));
133270
+ for(var s=0;s<cap&&idx<ids.length;s++){
133271
+ var a=aoff+(2*Math.PI*s)/cap;
133272
+ pos[ids[idx]]={x:Math.cos(a)*r-nw/2,y:Math.sin(a)*r-nh/2};
133273
+ idx++;
133274
+ }
133275
+ ring++;
133276
+ }
133277
+ }
133278
+
133279
+ function placeEvents(events,aoff){
133280
+ var occ={};
133281
+ for(var i=0;i<events.length;i++){
133282
+ var da=i*GA;
133283
+ var sl=findSlot(da,aoff,occ);
133284
+ var r=VR_START+sl.ring*VR_GAP;
133285
+ var a=aoff+(2*Math.PI*sl.idx)/sl.cap;
133286
+ pos[events[i].id]={x:Math.cos(a)*r-EVW/2,y:Math.sin(a)*r-EVH/2};
133287
+ }
133288
+ }
133289
+
133290
+ function findSlot(da,aoff,occ){
133291
+ var ring=0;
133292
+ while(true){
133293
+ var r=VR_START+ring*VR_GAP;
133294
+ var cap=Math.max(8,Math.floor(2*Math.PI*r/VR_SLOT));
133295
+ var o=occ[ring]||{};
133296
+ var ds=((Math.round(((da-aoff)/(2*Math.PI))*cap)%cap)+cap)%cap;
133297
+ var f=nearFree(ds,cap,o);
133298
+ if(f!=null){o[f]=true;occ[ring]=o;return{ring:ring,idx:f,cap:cap}}
133299
+ ring++;
133300
+ }
133301
+ }
133302
+
133303
+ function nearFree(d,cap,o){
133304
+ for(var i=0;i<cap;i++){
133305
+ var cs=i===0?[d]:[((d-i)%cap+cap)%cap,((d+i)%cap+cap)%cap];
133306
+ for(var j=0;j<cs.length;j++)if(!o[cs[j]])return cs[j];
133307
+ }
133308
+ return null;
133309
+ }
133310
+
133311
+ function fitView(){
133312
+ var vis=getVisible();
133313
+ var ids=[];
133314
+ vis.entities.forEach(function(id){ids.push(id)});
133315
+ vis.events.forEach(function(id){ids.push(id)});
133316
+ if(!ids.length){cam.x=0;cam.y=0;cam.zoom=1;return}
133317
+ var x0=Infinity,x1=-Infinity,y0=Infinity,y1=-Infinity;
133318
+ ids.forEach(function(id){var p=pos[id];if(!p)return;var w=eById[id]?EW:EVW,h=eById[id]?EH:EVH;if(p.x<x0)x0=p.x;if(p.x+w>x1)x1=p.x+w;if(p.y<y0)y0=p.y;if(p.y+h>y1)y1=p.y+h});
133319
+ cam.x=(x0+x1)/2;cam.y=(y0+y1)/2;
133320
+ cam.zoom=Math.min((W-120)/(x1-x0),(H-120)/(y1-y0),1.2);
133321
+ }
133322
+
133323
+ function getVisible(){
133324
+ var ve=new Set(),vv=new Set();
133325
+ graphData.entities.slice(0,ROOT_LIMIT).forEach(function(e){ve.add(e.id)});
133326
+ expEnt.forEach(function(eid){ve.add(eid);(evByEnt[eid]||[]).forEach(function(v){vv.add(v)})});
133327
+ expEv.forEach(function(eid){vv.add(eid);(entByEv[eid]||[]).forEach(function(e){ve.add(e)})});
133328
+ return{entities:ve,events:vv};
133329
+ }
133330
+
133331
+ function rebuildDOM(){
133332
+ // clear old
133333
+ Object.keys(nodeEls).forEach(function(id){if(nodeEls[id].parentNode)nodeEls[id].parentNode.removeChild(nodeEls[id])});
133334
+ while(svg.firstChild)svg.removeChild(svg.firstChild);
133335
+ nodeEls={};edgeEls=[];
133336
+
133337
+ var vis=getVisible();
133338
+ var visAll=new Set();vis.entities.forEach(function(id){visAll.add(id)});vis.events.forEach(function(id){visAll.add(id)});
133339
+
133340
+ // connection set for highlighting
133341
+ var conn=null;
133342
+ if(selId){
133343
+ conn=new Set();conn.add(selId);
133344
+ graphData.edges.forEach(function(e){
133345
+ if(!visAll.has(e.entityId)||!visAll.has(e.eventId))return;
133346
+ if(e.entityId===selId||e.eventId===selId){conn.add(e.entityId);conn.add(e.eventId)}
133347
+ });
133348
+ }
133349
+
133350
+ // edges
133351
+ graphData.edges.forEach(function(e){
133352
+ if(!visAll.has(e.entityId)||!visAll.has(e.eventId))return;
133353
+ var line=document.createElementNS('http://www.w3.org/2000/svg','line');
133354
+ var isDir=selId&&(e.entityId===selId||e.eventId===selId);
133355
+ var isRel=conn&&conn.has(e.entityId)&&conn.has(e.eventId);
133356
+ var isExp=expEnt.has(e.entityId)||expEv.has(e.eventId);
133357
+ if(isDir)line.setAttribute('class','edge highlighted');
133358
+ else if(isRel)line.setAttribute('class','edge');
133359
+ else if(conn)line.setAttribute('class','edge dimmed');
133360
+ else line.setAttribute('class',isExp?'edge animated':'edge');
133361
+ line.setAttribute('data-src',e.entityId);
133362
+ line.setAttribute('data-tgt',e.eventId);
133363
+ svg.appendChild(line);
133364
+ edgeEls.push(line);
133365
+ });
133366
+
133367
+ // nodes
133368
+ graphData.entities.forEach(function(ent){
133369
+ if(!vis.entities.has(ent.id))return;
133370
+ var el=mkNode(ent.id,ent.name,'entity',pos[ent.id].root,expEnt.has(ent.id));
133371
+ container.appendChild(el);nodeEls[ent.id]=el;
133372
+ });
133373
+ graphData.events.forEach(function(ev){
133374
+ if(!vis.events.has(ev.id))return;
133375
+ var el=mkNode(ev.id,ev.title,'event',false,expEv.has(ev.id));
133376
+ container.appendChild(el);nodeEls[ev.id]=el;
133377
+ });
133378
+
133379
+ // apply dimming
133380
+ if(conn){
133381
+ Object.keys(nodeEls).forEach(function(id){
133382
+ if(conn.has(id))nodeEls[id].classList.remove('dimmed');
133383
+ else nodeEls[id].classList.add('dimmed');
133384
+ });
133385
+ if(selId&&nodeEls[selId])nodeEls[selId].classList.add('selected');
133386
+ }
133387
+
133388
+ render();
133389
+ }
133390
+
133391
+ function mkNode(id,label,kind,root,expanded){
133392
+ var el=document.createElement('div');
133393
+ el.className='node '+kind;
133394
+ if(root)el.classList.add('root');
133395
+ if(expanded)el.classList.add('expanded');
133396
+ el.textContent=label;
133397
+ el.setAttribute('data-id',id);
133398
+ el.setAttribute('data-kind',kind);
133399
+ var dur=5+Math.random()*4;
133400
+ var del=Math.random()*dur;
133401
+ el.style.animationDuration=dur+'s';
133402
+ el.style.animationDelay='-'+del+'s';
133403
+
133404
+ var clickTimer=null;
133405
+ el.addEventListener('click',function(e){
133406
+ e.stopPropagation();
133407
+ if(clickTimer){clearTimeout(clickTimer);clickTimer=null;return}
133408
+ clickTimer=setTimeout(function(){
133409
+ clickTimer=null;
133410
+ toggleNode(id,kind);
133411
+ selId=id;
133412
+ rebuildDOM();
133413
+ },200);
133414
+ });
133415
+ el.addEventListener('dblclick',function(e){
133416
+ e.stopPropagation();
133417
+ if(clickTimer){clearTimeout(clickTimer);clickTimer=null}
133418
+ navStack=[];
133419
+ showDetail(id,kind);
133420
+ });
133421
+ return el;
133422
+ }
133423
+
133424
+ function toggleNode(id,kind){
133425
+ if(kind==='entity'){
133426
+ if(expEnt.has(id)){
133427
+ expEnt.delete(id);
133428
+ var related=new Set(evByEnt[id]||[]);
133429
+ expEv.forEach(function(eid){if(related.has(eid))expEv.delete(eid)});
133430
+ }else expEnt.add(id);
133431
+ }else{
133432
+ if(expEv.has(id))expEv.delete(id);
133433
+ else expEv.add(id);
133434
+ }
133435
+ }
133436
+
133437
+ function render(){
133438
+ // position nodes
133439
+ Object.keys(nodeEls).forEach(function(id){
133440
+ var p=pos[id];if(!p)return;
133441
+ var sp=w2s(p.x,p.y);
133442
+ var w=eById[id]?EW:EVW,h=eById[id]?EH:EVH;
133443
+ var el=nodeEls[id];
133444
+ el.style.left=sp.x+'px';
133445
+ el.style.top=sp.y+'px';
133446
+ el.style.width=(w*cam.zoom)+'px';
133447
+ el.style.height=(h*cam.zoom)+'px';
133448
+ el.style.fontSize=Math.max(8,12*cam.zoom)+'px';
133449
+ });
133450
+
133451
+ // position edges
133452
+ edgeEls.forEach(function(line){
133453
+ var srcId=line.getAttribute('data-src');
133454
+ var tgtId=line.getAttribute('data-tgt');
133455
+ var sp=pos[srcId],tp=pos[tgtId];
133456
+ if(!sp||!tp)return;
133457
+ var s=w2s(sp.x+EW/2,sp.y+EH/2);
133458
+ var t=w2s(tp.x+EVW/2,tp.y+EVH/2);
133459
+ line.setAttribute('x1',s.x);line.setAttribute('y1',s.y);
133460
+ line.setAttribute('x2',t.x);line.setAttribute('y2',t.y);
133461
+ });
133462
+ }
133463
+
133464
+ // click background to deselect
133465
+ container.addEventListener('click',function(e){
133466
+ if(e.target===container||e.target===svg){selId=null;rebuildDOM()}
133467
+ });
133468
+
133469
+ // detail panel
133470
+ function showDetail(id,kind,pushNav){
133471
+ if(pushNav!==false)navStack.push({id:id,kind:kind});
133472
+ var panel=document.getElementById('detail');
133473
+ var body=document.getElementById('detail-body');
133474
+ var html='';
133475
+ if(navStack.length>1)html+='<button class="back-btn" id="btn-back">← 返回</button>';
133476
+ if(kind==='entity'){
133477
+ var e=eById[id];
133478
+ html+='<div class="type-tag">'+esc(e.type)+'</div>';
133479
+ html+='<h3>'+esc(e.name)+'</h3>';
133480
+ html+='<div class="field"><div class="label">关联事件</div><div class="value">'+(e.eventCount||0)+' 个</div></div>';
133481
+ var ce=evByEnt[id]||[];
133482
+ if(ce.length){html+='<div class="divider"></div><div class="field"><div class="label">事件列表</div>';ce.forEach(function(eid){var ev=evById[eid];if(ev)html+='<div class="conn-item" data-id="'+eid+'" data-kind="event">'+esc(ev.title)+'</div>'});html+='</div>'}
133483
+ }else{
133484
+ var ev=evById[id];
133485
+ html+='<h3>'+esc(ev.title)+'</h3>';
133486
+ var ce=entByEv[id]||[];
133487
+ if(ce.length){html+='<div class="divider"></div><div class="field"><div class="label">关联实体</div>';ce.forEach(function(eid){var e=eById[eid];if(e)html+='<div class="conn-item" data-id="'+eid+'" data-kind="entity">'+esc(e.name)+'<span class="badge">'+esc(e.type)+'</span></div>'});html+='</div>'}
133488
+ }
133489
+ body.innerHTML=html;panel.classList.add('open');
133490
+ var backBtn=document.getElementById('btn-back');
133491
+ if(backBtn)backBtn.addEventListener('click',function(){
133492
+ navStack.pop();
133493
+ var prev=navStack[navStack.length-1];
133494
+ if(prev){selId=prev.id;rebuildDOM();showDetail(prev.id,prev.kind,false)}
133495
+ else{panel.classList.remove('open')}
133496
+ });
133497
+ body.querySelectorAll('.conn-item').forEach(function(el){
133498
+ el.addEventListener('click',function(){
133499
+ var nid=el.getAttribute('data-id'),nk=el.getAttribute('data-kind');
133500
+ if(nk==='entity'&&!expEnt.has(nid))expEnt.add(nid);
133501
+ if(nk==='event'&&!expEv.has(nid))expEv.add(nid);
133502
+ selId=nid;rebuildDOM();showDetail(nid,nk);
133503
+ });
133504
+ });
133505
+ }
133506
+
133507
+ function esc(s){if(!s)return'';return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
133508
+
133509
+ document.getElementById('btn-reset').onclick=function(){expEnt.clear();expEv.clear();selId=null;graphData.entities.forEach(function(e){expEnt.add(e.id)});document.getElementById('detail').classList.remove('open');navStack=[];fitView();rebuildDOM()};
133510
+ document.getElementById('btn-expand').onclick=function(){graphData.entities.forEach(function(e){expEnt.add(e.id)});graphData.events.forEach(function(e){expEv.add(e.id)});selId=null;document.getElementById('detail').classList.remove('open');fitView();rebuildDOM()};
133511
+ document.getElementById('btn-close').onclick=function(){document.getElementById('detail').classList.remove('open');navStack=[]};
133512
+ })();
133513
+ <\/script>
133514
+ </body>
133515
+ </html>`;
133516
+ async function serveGraphJSON(store, res) {
133517
+ try {
133518
+ const [entities, events, edges, sources] = await Promise.all([
133519
+ store.listEntities(),
133520
+ store.listEvents(),
133521
+ store.listEventEntities(),
133522
+ store.listSources()
133523
+ ]);
133524
+ const eventEntityMap = /* @__PURE__ */ new Map();
133525
+ for (const edge of edges) {
133526
+ const list = eventEntityMap.get(edge.eventId) ?? [];
133527
+ list.push(edge.entityId);
133528
+ eventEntityMap.set(edge.eventId, list);
133529
+ }
133530
+ const data = {
133531
+ entities: entities.map(({ id, sourceId, type, name, normalizedName, eventCount }) => ({
133532
+ id,
133533
+ sourceId,
133534
+ type,
133535
+ name,
133536
+ normalizedName,
133537
+ eventCount
133538
+ })),
133539
+ events: events.map(({ id, sourceId, documentId, title, rank }) => ({
133540
+ id,
133541
+ sourceId,
133542
+ documentId,
133543
+ title,
133544
+ rank,
133545
+ entityIds: eventEntityMap.get(id) ?? []
133546
+ })),
133547
+ edges,
133548
+ sources: sources.map(({ id, name }) => ({
133549
+ id,
133550
+ name
133551
+ }))
133552
+ };
133553
+ res.writeHead(200, {
133554
+ "Content-Type": "application/json",
133555
+ "Cache-Control": "no-store"
133556
+ });
133557
+ res.end(JSON.stringify(data));
133558
+ } catch (error) {
133559
+ res.writeHead(500, { "Content-Type": "application/json" });
133560
+ res.end(JSON.stringify({ error: String(error) }));
133561
+ }
133562
+ }
133563
+ function serveHTML(res) {
133564
+ res.writeHead(200, {
133565
+ "Content-Type": "text/html; charset=utf-8",
133566
+ "Cache-Control": "no-store"
133567
+ });
133568
+ res.end(HTML);
133569
+ }
133570
+ async function handleWeb() {
133571
+ const store = await getKnowledgeStore();
133572
+ const s = await store.stats();
133573
+ if (s.entities === 0 && s.events === 0) throw new Error("知识库为空,请先用 /knowledge 摄入文档");
133574
+ const server = createServer((req, res) => {
133575
+ if (req.url === "/api/graph") {
133576
+ serveGraphJSON(store, res);
133577
+ return;
133578
+ }
133579
+ serveHTML(res);
133580
+ });
133581
+ registerServer(server);
133582
+ await new Promise((resolve, reject) => {
133583
+ server.once("error", reject);
133584
+ server.listen(0, "127.0.0.1", () => {
133585
+ server.off("error", reject);
133586
+ resolve();
133587
+ });
133588
+ });
133589
+ const url = `http://127.0.0.1:${server.address().port}`;
133590
+ openUrl(url);
133591
+ console.log(`\n 知识图谱: ${url}\n 按 Ctrl+C 停止服务器\n`);
133592
+ }
133593
+ //#endregion
132841
133594
  //#region src/tui/components/dialogs/knowledge-result-viewer.ts
132842
133595
  /**
132843
133596
  * KnowledgeResultViewer — full-screen scrollable text viewer for /knowledge
@@ -133192,15 +133945,6 @@ function fitExactly$3(line, width) {
133192
133945
  }
133193
133946
  //#endregion
133194
133947
  //#region src/tui/commands/knowledge.ts
133195
- let knowledgeStoreInstance;
133196
- async function getKnowledgeStore() {
133197
- if (knowledgeStoreInstance === void 0) {
133198
- knowledgeStoreInstance = new KnowledgeStore(getDataDir());
133199
- await knowledgeStoreInstance.init();
133200
- knowledgeStoreInstance.setEmbeddingEngine(createFastEmbedEngine());
133201
- }
133202
- return knowledgeStoreInstance;
133203
- }
133204
133948
  function promptTextInput(host, title, opts) {
133205
133949
  const { promise, resolve } = Promise.withResolvers();
133206
133950
  const dialog = new TextInputDialogComponent((result) => {
@@ -133505,6 +134249,11 @@ async function handleKnowledgeCommand(host, _args) {
133505
134249
  value: "stats",
133506
134250
  label: "📊 统计信息",
133507
134251
  description: "查看知识库整体统计"
134252
+ },
134253
+ {
134254
+ value: "web",
134255
+ label: "🌐 知识图谱",
134256
+ description: "在浏览器中查看交互式知识图谱"
133508
134257
  }
133509
134258
  ];
133510
134259
  const showMenu = () => {
@@ -133522,6 +134271,7 @@ async function handleKnowledgeCommand(host, _args) {
133522
134271
  else if (value === "search") await handleSearch(host);
133523
134272
  else if (value === "delete") await handleDelete(host);
133524
134273
  else if (value === "stats") await handleStats(host);
134274
+ else if (value === "web") await handleWeb();
133525
134275
  } catch (error) {
133526
134276
  const msg = error instanceof Error ? error.message : String(error);
133527
134277
  host.showError(`操作失败: ${msg}`);
@@ -134863,17 +135613,6 @@ function mcpServerStatusKey(server) {
134863
135613
  ]);
134864
135614
  }
134865
135615
  //#endregion
134866
- //#region src/tui/utils/open-url.ts
134867
- function openUrl(url) {
134868
- const command = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", [
134869
- "/c",
134870
- "start",
134871
- "",
134872
- url
134873
- ]] : ["xdg-open", [url]];
134874
- execFile(command[0], command[1], () => {});
134875
- }
134876
- //#endregion
134877
135616
  //#region src/tui/utils/mcp-oauth.ts
134878
135617
  const MAX_AUTH_URL_SET_SIZE = 200;
134879
135618
  var McpOAuthAuthorizationUrlOpener = class {
@@ -142940,6 +143679,7 @@ var SessionManager = class {
142940
143679
  if (session === void 0) throw new Error("启动会话未初始化。");
142941
143680
  await this.setSession(session);
142942
143681
  await this.syncRuntimeState(session);
143682
+ if (startup.wolfpack && !isResumeStartup) await session.setWolfpackMode(true);
142943
143683
  this.host.state.startupState = "ready";
142944
143684
  this.host.sessionEventHandler.startSubscription();
142945
143685
  return {
@@ -145057,7 +145797,7 @@ function createInitialAppState(input) {
145057
145797
  goalActive: false,
145058
145798
  goalContinuationCount: 0,
145059
145799
  ccConnectActive: false,
145060
- wolfpackMode: false,
145800
+ wolfpackMode: input.cliOptions.wolfpack === true,
145061
145801
  loopModeEnabled: false,
145062
145802
  loopPrompt: void 0,
145063
145803
  loopLimit: void 0,
@@ -145117,6 +145857,7 @@ var ScreamTUI = class {
145117
145857
  yolo: startupInput.cliOptions.yolo,
145118
145858
  auto: startupInput.cliOptions.auto,
145119
145859
  plan: startupInput.cliOptions.plan,
145860
+ wolfpack: startupInput.cliOptions.wolfpack,
145120
145861
  model: startupInput.cliOptions.model,
145121
145862
  startupNotice: startupInput.startupNotice
145122
145863
  },
@@ -146391,6 +147132,14 @@ var ClaudeStreamJsonWriter = class {
146391
147132
  if (input > 0) this.tokenUsage.input += input;
146392
147133
  if (output > 0) this.tokenUsage.output += output;
146393
147134
  }
147135
+ /** Returns the accumulated token usage for the current turn. Used by
147136
+ * the result event emitter so the final `result` event carries the
147137
+ * turn's total token usage. Without this, usage was accumulated but
147138
+ * never emitted — consumers (cc-connect, mobile clients) saw empty
147139
+ * usage fields. */
147140
+ getTokenUsage() {
147141
+ return { ...this.tokenUsage };
147142
+ }
146394
147143
  nextMsgId() {
146395
147144
  this.msgCounter += 1;
146396
147145
  return `msg_${this.msgCounter.toString(36)}`;
@@ -146637,22 +147386,26 @@ async function runStreamJson(opts) {
146637
147386
  let activeAgentId;
146638
147387
  let settled = false;
146639
147388
  let unsubscribe;
146640
- const finish = (error) => {
147389
+ const finish = (error, exitCode) => {
146641
147390
  if (settled) return;
146642
147391
  settled = true;
146643
147392
  unsubscribe?.();
146644
147393
  if (error) {
146645
- writer.emitResult("error", error.message);
147394
+ writer.emitResult("error", error.message, writer.getTokenUsage());
147395
+ if (exitCode !== void 0) process.exitCode = exitCode;
146646
147396
  reject(error);
146647
147397
  } else {
146648
- writer.emitResult("success", "");
147398
+ writer.emitResult("success", "", writer.getTokenUsage());
146649
147399
  resolve();
146650
147400
  }
146651
147401
  };
146652
147402
  unsubscribe = session.onEvent((event) => {
146653
147403
  if (event.type === "error") {
146654
147404
  if (event.agentId !== "main") return;
146655
- finish(/* @__PURE__ */ new Error(`${event.code}: ${event.message}`));
147405
+ const errCode = event.code;
147406
+ let exitCode = 1;
147407
+ if (errCode === "provider.auth_error" || errCode === "provider.rate_limit" || errCode === "provider.api_error" || errCode === "provider.connection_error") exitCode = 3;
147408
+ finish(/* @__PURE__ */ new Error(`${event.code}: ${event.message}`), exitCode);
146656
147409
  return;
146657
147410
  }
146658
147411
  if (event.type === "subagent.spawned") {
@@ -146689,9 +147442,14 @@ async function runStreamJson(opts) {
146689
147442
  writer.updateUsage(inputTotal, event.usage.output ?? 0);
146690
147443
  }
146691
147444
  } else if (type === "turn.ended") if (event.reason === "completed") finish();
147445
+ else if (event.reason === "cancelled") finish(/* @__PURE__ */ new Error("Turn cancelled"));
146692
147446
  else {
147447
+ const errCode = event.error?.code;
146693
147448
  const errMsg = event.error !== void 0 ? `${event.error.code}: ${event.error.message}` : `Turn ended: ${event.reason}`;
146694
- finish(new Error(errMsg));
147449
+ let code = 1;
147450
+ if (errCode === "loop.max_steps_exceeded") code = 4;
147451
+ else if (errCode === "provider.auth_error" || errCode === "provider.rate_limit" || errCode === "provider.api_error" || errCode === "provider.connection_error") code = 3;
147452
+ finish(new Error(errMsg), code);
146695
147453
  }
146696
147454
  });
146697
147455
  session.prompt(userText).catch((error) => {
@@ -146712,9 +147470,11 @@ async function runStreamJson(opts) {
146712
147470
  await turnPromise;
146713
147471
  }
146714
147472
  } catch (error) {
146715
- log.error("stream-json: fatal", { error });
146716
- writer.emitResult("error", error instanceof Error ? error.message : String(error));
146717
- process.exitCode = 1;
147473
+ if (process.exitCode === void 0) {
147474
+ log.error("stream-json: fatal", { error });
147475
+ writer.emitResult("error", error instanceof Error ? error.message : String(error), writer.getTokenUsage());
147476
+ process.exitCode = 1;
147477
+ } else log.error("stream-json: turn error (exitCode already set)", { error });
146718
147478
  } finally {
146719
147479
  for (const [, pending] of pendingApprovals) pending.resolve({
146720
147480
  decision: "rejected",