scream-code 0.16.7 → 0.16.8

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.
@@ -4,13 +4,13 @@ import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
6
  import { i as __require, o as __toESM, r as __exportAll, t as __commonJSMin } from "./chunk-D90kvbyJ.mjs";
7
- import { C as join$1, D as resolve$2, 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$1, x as dirname$2, y as KnowledgeStore } from "./src-tDEINaMV.mjs";
7
+ import { A as classifyEmbeddingFailure, B as rankMemos, C as applyConsolidation, D as parseMemoryMemos, E as buildExitExtractionPrompt, F as createMemoryMemo, G as normalize$1, H as dirname$2, I as toSummary, J as resolve$2, K as parse$7, L as generateTags, M as createFastEmbedEngine, N as hasEmbeddingModelCache$1, O as stripMemoryTags, P as probeLocalEmbeddingSupport, R as normalizeTags, S as DreamTracker, T as EXIT_EXTRACTION_SYSTEM_PROMPT, U as isAbsolute$1, V as basename$1, W as join$1, a as isSupportedFile, b as sharedKnowledgeStoreReady, i as ingestFile, j as clearEmbeddingModelCache, k as MemoryMemoStore, q as relative$1, r as ingestDirectory, t as multiSearch, w as buildConsolidationPlan, y as sharedKnowledgeStore, z as buildProjectTagCloud } from "./src-CXmZg1VT.mjs";
8
8
  import { t as require_base64_js } from "./base64-js-DzVmk6Nb.mjs";
9
- import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-D9ByqSsK.mjs";
9
+ import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-6v2667rm.mjs";
10
10
  import { createRequire } from "node:module";
11
11
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
12
12
  import * as fs$1 from "node:fs/promises";
13
- import Jn, { access, appendFile, chmod, copyFile, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
13
+ import Jn, { access, appendFile, chmod, copyFile, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
14
14
  import I, { createWriteStream } from "fs";
15
15
  import Vr, { EventEmitter } from "events";
16
16
  import * as path$1$1 from "path";
@@ -22,13 +22,12 @@ import Ds, { PassThrough, Readable } from "node:stream";
22
22
  import { finished, pipeline as pipeline$1 } from "node:stream/promises";
23
23
  import * as NodeWs from "ws";
24
24
  import * as fs$10 from "node:fs";
25
- import Vt, { appendFileSync, chmodSync, closeSync, constants, createReadStream, createWriteStream as createWriteStream$1, existsSync, fchmodSync, fsyncSync, mkdirSync, openSync, promises, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
25
+ import Vt, { appendFileSync, chmodSync, closeSync, constants, createReadStream, createWriteStream as createWriteStream$1, existsSync, fchmodSync, fsyncSync, mkdirSync, openSync, promises, readFileSync, readSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
26
26
  import * as path$8 from "node:path";
27
27
  import path, { basename, dirname as dirname$1, extname, isAbsolute, join, normalize, posix, relative, resolve, sep, win32 } from "node:path";
28
28
  import { z } from "zod";
29
29
  import { exec, execFile, execSync, spawn, spawnSync } from "node:child_process";
30
30
  import { promisify } from "node:util";
31
- import { DatabaseSync } from "node:sqlite";
32
31
  import * as nodeOs from "node:os";
33
32
  import { homedir, release, tmpdir } from "node:os";
34
33
  import * as vs from "zlib";
@@ -51,7 +50,7 @@ import { lookup } from "node:dns/promises";
51
50
  import { isIP } from "node:net";
52
51
  import { AsyncLocalStorage } from "node:async_hooks";
53
52
  import chalk from "chalk";
54
- import { Box, Container, Image, Input, Key, Markdown, Marked, Spacer, Text, decodeKittyPrintable, fuzzyFilter, getCapabilities, getImageDimensions, matchesKey, sliceByColumn, truncateToWidth, visibleWidth } from "@liutod-scream/pi-tui";
53
+ import { Box, Container, Image, Input, Key, Markdown, Marked, Spacer, Text, decodeKittyPrintable, fuzzyFilter, getCapabilities, getImageDimensions, matchesKey, sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@liutod-scream/pi-tui";
55
54
  import { highlight, supportsLanguage } from "cli-highlight";
56
55
  import { diagramKind, render } from "grok-mermaid";
57
56
  import { gt, valid } from "semver";
@@ -782,8 +781,52 @@ function isRetryableGenerateError(error) {
782
781
  if (error instanceof APIEmptyResponseError) return true;
783
782
  if (error instanceof APIProviderRateLimitError) return true;
784
783
  if (error instanceof APIStatusError) return error.statusCode === 429 || error.statusCode === 408 || error.statusCode >= 500 && error.statusCode < 600;
784
+ if (error instanceof ChatProviderError) return isRetryableProviderMessage(error.message);
785
785
  return false;
786
786
  }
787
+ /** Account/limit exhaustion — retrying only burns the budget. */
788
+ const NON_RETRYABLE_PROVIDER_MESSAGE_PATTERNS = [
789
+ /insufficient_quota/i,
790
+ /out of budget/i,
791
+ /\bbilling\b/i,
792
+ /usage limit/i,
793
+ /available balance/i,
794
+ /connection limit/i
795
+ ];
796
+ /**
797
+ * Clearly transient upstream or transport failures when no HTTP status
798
+ * exists. Bracketed 5xx only — bare mid-sentence numbers stay out so we do
799
+ * not invent retryability from unrelated digits (status recovery remains the
800
+ * anchored `Streaming response failed:` prefix path).
801
+ */
802
+ const RETRYABLE_PROVIDER_MESSAGE_PATTERNS = [
803
+ /\b(?:429|408)\b/,
804
+ /\[5\d{2}\]/,
805
+ /overloaded/i,
806
+ /high demand/i,
807
+ /rate.?limit/i,
808
+ /too many requests/i,
809
+ /service.?unavailable/i,
810
+ /server.?error/i,
811
+ /internal.?error/i,
812
+ /network error/i,
813
+ /connection (?:error|refused|reset|lost|closed|terminated)/i,
814
+ /timeout|timed? out|terminated/i,
815
+ /stream ended|ended without/i,
816
+ /you can retry your request/i,
817
+ /try your request again/i,
818
+ /please retry your request/i
819
+ ];
820
+ /**
821
+ * Classify a status-less provider error message. Quota/billing-style wording
822
+ * is never retryable; only conservative transient patterns are. Used for
823
+ * plain `ChatProviderError`s (typed stream events, gateway wording without
824
+ * an HTTP status). Exported for adapters and tests.
825
+ */
826
+ function isRetryableProviderMessage(message) {
827
+ if (NON_RETRYABLE_PROVIDER_MESSAGE_PATTERNS.some((pattern) => pattern.test(message))) return false;
828
+ return RETRYABLE_PROVIDER_MESSAGE_PATTERNS.some((pattern) => pattern.test(message));
829
+ }
787
830
  const CONTEXT_OVERFLOW_MESSAGE_PATTERNS = [
788
831
  /context[ _-]?length/,
789
832
  /(?:context[ _-]?window.*exceed|exceed.*context[ _-]?window)/,
@@ -44494,9 +44537,10 @@ function convertOpenAIError(error) {
44494
44537
  if (error instanceof ChatProviderError) return error;
44495
44538
  if (error instanceof APIConnectionTimeoutError) return new APITimeoutError(error.message);
44496
44539
  if (error instanceof APIConnectionError) return new APIConnectionError$3(error.message);
44497
- if (error instanceof APIError && typeof error.status === "number") {
44498
- const reqId = error.requestID ?? null;
44499
- return normalizeAPIStatusError(error.status, error.message, reqId);
44540
+ if (error instanceof APIError) {
44541
+ const streamStatus = /^Streaming response failed:\s*\[([45]\d{2})\]/.exec(error.message)?.[1];
44542
+ const status = error.status ?? (streamStatus === void 0 ? void 0 : Number(streamStatus));
44543
+ if (status !== void 0) return normalizeAPIStatusError(status, error.message, error.requestID ?? null);
44500
44544
  }
44501
44545
  if (error instanceof APIError && error.constructor === APIError && error.error === void 0) return classifyBaseApiError(error.message);
44502
44546
  if (error instanceof APIUserAbortError) throw new DOMException("The operation was aborted.", "AbortError");
@@ -53424,1723 +53468,6 @@ var WriteGoalNoteTool = class {
53424
53468
  }
53425
53469
  };
53426
53470
  //#endregion
53427
- //#region ../../packages/memory/src/scoring.ts
53428
- const TEMPORAL_STRONG = /昨天|今天|上周|本周|本月|最近|之前|以前|上次|刚刚|刚刚才|刚才|前天|大前天|前几天|这几天|这几天来|近.*[天周月年]/;
53429
- const TEMPORAL_WEAK = /什么时候|何时|多久|几时|哪天|哪一天|多长.*时间/;
53430
- const TEMPORAL_ACTION = /做了|干了|处理了|解决了|修复了|完成了|改了|写了|加了|删了|移除了|部署了|发布了/;
53431
- const CODE_SIGNAL = /[`'"#]|[{}()[\]]|\.(ts|js|tsx|jsx|py|rs|go|java|cpp|c|h)\b|function |const |import |export |class |async |await |type |interface /;
53432
- /**
53433
- * Lightweight regex-based query intent detection.
53434
- * Zero LLM cost — classifies into temporal vs factual bias to adjust scoring weights.
53435
- */
53436
- function detectQueryIntent(query) {
53437
- const q = query.trim();
53438
- if (q.length === 0) return {
53439
- temporalBias: .3,
53440
- factualBias: .6
53441
- };
53442
- let temporalBias = .3;
53443
- let factualBias = .6;
53444
- if (TEMPORAL_STRONG.test(q)) temporalBias = 1;
53445
- else if (TEMPORAL_WEAK.test(q)) temporalBias = .8;
53446
- else if (TEMPORAL_ACTION.test(q)) temporalBias = .6;
53447
- if (CODE_SIGNAL.test(q) && !TEMPORAL_STRONG.test(q) && !TEMPORAL_WEAK.test(q)) factualBias = 1;
53448
- return {
53449
- temporalBias,
53450
- factualBias
53451
- };
53452
- }
53453
- /**
53454
- * Multi-factor relevance score for a memory memo against a query.
53455
- * Pure deterministic scoring — no LLM call, no network.
53456
- *
53457
- * When `intent` is provided, keyword weight increases for factual queries
53458
- * and recency weight increases for temporal queries.
53459
- */
53460
- function computeRelevanceScore(memo, query, usageCount = 0, currentProjectDir, projectTagCloud, intent) {
53461
- const ti = intent?.temporalBias ?? .3;
53462
- const fi = intent?.factualBias ?? .6;
53463
- const factors = {
53464
- keywordOverlap: computeKeywordSimilarity(memo, query),
53465
- recency: computeRecency(memo.recordedAt),
53466
- usageBoost: Math.min(.3, usageCount * .1),
53467
- projectBoost: computeProjectBoost(memo.projectDir, currentProjectDir),
53468
- tagOverlap: computeTagOverlap(memo.tags, projectTagCloud)
53469
- };
53470
- const kwWeight = .45 * (1 + (fi - .6) * .5);
53471
- const recencyWeight = .25 * (1 + (ti - .3) * .571);
53472
- const usageWeight = .15;
53473
- const projectWeight = .1;
53474
- const tagWeight = .05;
53475
- const total = kwWeight + recencyWeight + usageWeight + projectWeight + tagWeight;
53476
- return factors.keywordOverlap * (kwWeight / total) + factors.recency * (recencyWeight / total) + factors.usageBoost * (usageWeight / total) + factors.projectBoost * (projectWeight / total) + factors.tagOverlap * (tagWeight / total);
53477
- }
53478
- /**
53479
- * Score multiple memos against a query, returning sorted results.
53480
- */
53481
- function rankMemos(memos, query, options = {}) {
53482
- const { minScore = .3, maxResults = 3, currentProjectDir, projectTagCloud, vectorScores } = options;
53483
- const intent = detectQueryIntent(query);
53484
- const hasVectorScores = vectorScores !== void 0 && vectorScores.size > 0;
53485
- return memos.map((memo) => {
53486
- const keywordScore = computeRelevanceScore(memo, query, 0, currentProjectDir, projectTagCloud, intent);
53487
- const vectorScore = vectorScores?.get(memo.id) ?? 0;
53488
- return {
53489
- memo,
53490
- score: hasVectorScores ? keywordScore * .6 + vectorScore * .4 : keywordScore
53491
- };
53492
- }).filter((s) => s.score >= minScore).toSorted((a, b) => b.score - a.score).slice(0, maxResults);
53493
- }
53494
- /**
53495
- * Build a tag cloud from memos that belong to the current project.
53496
- */
53497
- function buildProjectTagCloud(memos, projectDir) {
53498
- const cloud = /* @__PURE__ */ new Set();
53499
- for (const memo of memos) if (memo.projectDir === projectDir && memo.tags !== void 0) for (const tag of memo.tags) cloud.add(tag);
53500
- return cloud;
53501
- }
53502
- function computeProjectBoost(memoProjectDir, currentProjectDir) {
53503
- if (currentProjectDir === void 0 || currentProjectDir.length === 0) return 0;
53504
- if (memoProjectDir === currentProjectDir) return 1;
53505
- if (memoProjectDir.length === 0) return 0;
53506
- const memoParent = parentDir(memoProjectDir);
53507
- const currentParent = parentDir(currentProjectDir);
53508
- if (memoParent.length > 0 && memoParent === currentParent) return .3;
53509
- return 0;
53510
- }
53511
- function parentDir(path) {
53512
- const trimmed = path.replace(/\/+$/, "");
53513
- const lastSep = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
53514
- return lastSep > 0 ? trimmed.slice(0, lastSep) : "";
53515
- }
53516
- function computeTagOverlap(memoTags, projectTagCloud) {
53517
- if (projectTagCloud === void 0 || projectTagCloud.size === 0) return 0;
53518
- if (memoTags === void 0 || memoTags.length === 0) return 0;
53519
- const memoSet = new Set(memoTags);
53520
- let intersection = 0;
53521
- for (const tag of memoSet) if (projectTagCloud.has(tag)) intersection++;
53522
- const union = new Set([...memoSet, ...projectTagCloud]).size;
53523
- return union === 0 ? 0 : intersection / union;
53524
- }
53525
- const STOP_WORDS = new Set([
53526
- "的",
53527
- "了",
53528
- "在",
53529
- "是",
53530
- "我",
53531
- "有",
53532
- "和",
53533
- "就",
53534
- "不",
53535
- "人",
53536
- "都",
53537
- "一",
53538
- "一个",
53539
- "上",
53540
- "也",
53541
- "很",
53542
- "到",
53543
- "说",
53544
- "要",
53545
- "去",
53546
- "你",
53547
- "会",
53548
- "着",
53549
- "没有",
53550
- "看",
53551
- "好",
53552
- "自己",
53553
- "这",
53554
- "他",
53555
- "她",
53556
- "它",
53557
- "们",
53558
- "那",
53559
- "些",
53560
- "the",
53561
- "a",
53562
- "an",
53563
- "is",
53564
- "are",
53565
- "was",
53566
- "were",
53567
- "be",
53568
- "been",
53569
- "being",
53570
- "have",
53571
- "has",
53572
- "had",
53573
- "do",
53574
- "does",
53575
- "did",
53576
- "will",
53577
- "would",
53578
- "could",
53579
- "should",
53580
- "may",
53581
- "might",
53582
- "can",
53583
- "shall",
53584
- "to",
53585
- "of",
53586
- "in",
53587
- "for",
53588
- "on",
53589
- "with",
53590
- "at",
53591
- "by",
53592
- "from",
53593
- "as",
53594
- "into",
53595
- "through",
53596
- "during",
53597
- "before",
53598
- "after",
53599
- "above",
53600
- "below",
53601
- "between",
53602
- "and",
53603
- "but",
53604
- "or",
53605
- "nor",
53606
- "not",
53607
- "so",
53608
- "yet",
53609
- "both",
53610
- "either",
53611
- "neither",
53612
- "each",
53613
- "every",
53614
- "all",
53615
- "any",
53616
- "few",
53617
- "more",
53618
- "most",
53619
- "other",
53620
- "some",
53621
- "such",
53622
- "no",
53623
- "only",
53624
- "own",
53625
- "same",
53626
- "than",
53627
- "too",
53628
- "very",
53629
- "just",
53630
- "because",
53631
- "about",
53632
- "what",
53633
- "which",
53634
- "who",
53635
- "whom",
53636
- "this",
53637
- "that",
53638
- "these",
53639
- "those",
53640
- "it",
53641
- "its",
53642
- "he",
53643
- "she",
53644
- "they",
53645
- "we",
53646
- "you",
53647
- "how"
53648
- ]);
53649
- function extractKeywords(text) {
53650
- const parts = text.toLowerCase().replaceAll(/([一-鿿㐀-䶿])([a-z0-9])/g, "$1 $2").replaceAll(/([a-z0-9])([一-鿿㐀-䶿])/g, "$1 $2").split(/[^a-z0-9一-鿿㐀-䶿]+/);
53651
- const tokens = [];
53652
- for (const part of parts) {
53653
- if (part.length === 0) continue;
53654
- if (/[一-鿿㐀-䶿]/.test(part)) {
53655
- for (const ch of part) if (ch.length > 0 && !STOP_WORDS.has(ch)) tokens.push(ch);
53656
- }
53657
- if (/^[a-z0-9]+$/.test(part) && part.length >= 2 && !STOP_WORDS.has(part)) tokens.push(part);
53658
- }
53659
- return [...new Set(tokens)];
53660
- }
53661
- function computeKeywordSimilarity(memo, query) {
53662
- const memoWords = extractKeywords(`${memo.userNeed} ${memo.approach} ${memo.whatFailed} ${memo.whatWorked}`);
53663
- const queryWords = extractKeywords(query);
53664
- if (memoWords.length === 0 || queryWords.length === 0) return 0;
53665
- const intersection = memoWords.filter((w) => queryWords.includes(w)).length;
53666
- const union = new Set([...memoWords, ...queryWords]).size;
53667
- return union === 0 ? 0 : intersection / union;
53668
- }
53669
- function computeRecency(recordedAt) {
53670
- const daysSince = (Date.now() - recordedAt) / (1e3 * 60 * 60 * 24);
53671
- return Math.max(0, 1 - daysSince / 90);
53672
- }
53673
- //#endregion
53674
- //#region ../../packages/memory/src/tags.ts
53675
- /**
53676
- * Normalize a tag list: lowercase, trim, deduplicate, drop empties,
53677
- * and cap at `max` entries.
53678
- */
53679
- function normalizeTags(tags, max = 5) {
53680
- if (!Array.isArray(tags)) return [];
53681
- const seen = /* @__PURE__ */ new Set();
53682
- const result = [];
53683
- for (const raw of tags) {
53684
- if (typeof raw !== "string") continue;
53685
- const tag = raw.trim().toLowerCase();
53686
- if (tag.length === 0 || seen.has(tag)) continue;
53687
- seen.add(tag);
53688
- result.push(tag);
53689
- if (result.length >= max) break;
53690
- }
53691
- return result;
53692
- }
53693
- /**
53694
- * Generate a small set of semantic tags from free-form text.
53695
- * Falls back to keyword extraction when the caller does not supply tags.
53696
- */
53697
- function generateTags(text, max = 5) {
53698
- return normalizeTags(extractKeywords(text), max);
53699
- }
53700
- //#endregion
53701
- //#region ../../packages/memory/src/models.ts
53702
- function generateId() {
53703
- return `memo-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
53704
- }
53705
- function createMemoryMemo(partial) {
53706
- return {
53707
- id: partial.id ?? generateId(),
53708
- sourceSessionId: partial.sourceSessionId,
53709
- sourceSessionTitle: partial.sourceSessionTitle,
53710
- userNeed: partial.userNeed,
53711
- approach: partial.approach,
53712
- outcome: partial.outcome,
53713
- whatFailed: partial.whatFailed,
53714
- whatWorked: partial.whatWorked,
53715
- extractionSource: partial.extractionSource,
53716
- recordedAt: partial.recordedAt ?? Date.now(),
53717
- projectDir: partial.projectDir ?? "",
53718
- tags: normalizedTags(partial.tags),
53719
- note: partial.note
53720
- };
53721
- }
53722
- function normalizedTags(value) {
53723
- const tags = normalizeTags(value);
53724
- return tags.length > 0 ? tags : void 0;
53725
- }
53726
- function toSummary(memo) {
53727
- return {
53728
- id: memo.id,
53729
- sourceSessionTitle: memo.sourceSessionTitle,
53730
- sourceSessionId: memo.sourceSessionId,
53731
- userNeed: memo.userNeed,
53732
- approach: memo.approach,
53733
- outcome: memo.outcome,
53734
- whatFailed: memo.whatFailed,
53735
- whatWorked: memo.whatWorked,
53736
- extractionSource: memo.extractionSource,
53737
- recordedAt: memo.recordedAt,
53738
- projectDir: memo.projectDir,
53739
- tags: memo.tags,
53740
- note: memo.note
53741
- };
53742
- }
53743
- //#endregion
53744
- //#region ../../packages/memory/src/embeddings.ts
53745
- /**
53746
- * Text used to generate embeddings for a memo.
53747
- * Combines the most semantically meaningful fields.
53748
- */
53749
- function buildEmbeddingText(memo) {
53750
- return `${memo.userNeed} ${memo.approach} ${memo.whatWorked}`;
53751
- }
53752
- /**
53753
- * Cache of created engines keyed by cacheDir.
53754
- * Guarantees that all callers sharing the same cacheDir reuse the same engine
53755
- * instance and the same in-flight model download/load — avoiding duplicate
53756
- * downloads and file-corruption races when /memory and /knowledge both start
53757
- * before the model is cached.
53758
- */
53759
- const engineCache = /* @__PURE__ */ new Map();
53760
- /**
53761
- * Create an embedding engine backed by fastembed.
53762
- * Lazily loads the model on first use so startup is not blocked.
53763
- * Engines are cached by cacheDir so repeated calls with the same cacheDir
53764
- * return the same instance, sharing model state and download progress.
53765
- * @param cacheDir Absolute path for model cache (e.g. ~/.scream-code/cache/fastembed).
53766
- * Defaults to "local_cache" (CWD-relative) if not provided — prefer
53767
- * passing an explicit path so the cache doesn't duplicate across CWDs.
53768
- */
53769
- function createFastEmbedEngine(cacheDir) {
53770
- const key = cacheDir ?? "";
53771
- const cached = engineCache.get(key);
53772
- if (cached !== void 0) return cached;
53773
- const engine = createFastEmbedEngineImpl(cacheDir);
53774
- engineCache.set(key, engine);
53775
- return engine;
53776
- }
53777
- /** Stable identity of the model used by every engine created here. */
53778
- const EMBEDDING_MODEL_NAME = "bge-small-zh-v1.5";
53779
- function createFastEmbedEngineImpl(cacheDir) {
53780
- let embedder = null;
53781
- let initPromise = null;
53782
- let loadFailed = false;
53783
- let lastError;
53784
- return {
53785
- modelName: EMBEDDING_MODEL_NAME,
53786
- get available() {
53787
- return embedder !== null && !loadFailed;
53788
- },
53789
- get lastError() {
53790
- return lastError;
53791
- },
53792
- async embedBatch(texts) {
53793
- if (!this.available) return null;
53794
- if (texts.length === 0) return [];
53795
- try {
53796
- const generator = embedder.embed(texts);
53797
- const vectors = [];
53798
- for await (const batch of generator) for (const vec of batch) vectors.push(new Float32Array(vec));
53799
- return vectors.length > 0 ? vectors : null;
53800
- } catch (error) {
53801
- lastError = error instanceof Error ? error.message : String(error);
53802
- return null;
53803
- }
53804
- },
53805
- cosineSimilarity(a, b) {
53806
- if (a.length !== b.length || a.length === 0) return 0;
53807
- let dot = 0;
53808
- let normA = 0;
53809
- let normB = 0;
53810
- for (let i = 0; i < a.length; i++) {
53811
- dot += a[i] * b[i];
53812
- normA += a[i] * a[i];
53813
- normB += b[i] * b[i];
53814
- }
53815
- const denom = Math.sqrt(normA) * Math.sqrt(normB);
53816
- return denom === 0 ? 0 : dot / denom;
53817
- },
53818
- async ensureReady() {
53819
- if (embedder !== null) {
53820
- loadFailed = false;
53821
- lastError = void 0;
53822
- return true;
53823
- }
53824
- try {
53825
- if (loadFailed || initPromise === null) {
53826
- loadFailed = false;
53827
- lastError = void 0;
53828
- initPromise = loadEmbedder(cacheDir);
53829
- }
53830
- embedder = await initPromise;
53831
- if (embedder === null) {
53832
- initPromise = null;
53833
- lastError = "fastembed returned an empty model";
53834
- return false;
53835
- }
53836
- loadFailed = false;
53837
- lastError = void 0;
53838
- return true;
53839
- } catch (error) {
53840
- initPromise = null;
53841
- loadFailed = true;
53842
- lastError = error instanceof Error ? error.message : String(error);
53843
- return false;
53844
- }
53845
- }
53846
- };
53847
- }
53848
- async function loadEmbedder(cacheDir) {
53849
- const { FlagEmbedding, EmbeddingModel } = await import("fastembed");
53850
- if (cacheDir !== void 0) mkdirSync(cacheDir, { recursive: true });
53851
- const model = EmbeddingModel.BGESmallZH;
53852
- const initOpts = cacheDir !== void 0 ? {
53853
- model,
53854
- cacheDir
53855
- } : { model };
53856
- try {
53857
- return await FlagEmbedding.init(initOpts);
53858
- } catch (initError) {
53859
- const msg = initError instanceof Error ? initError.message : "";
53860
- if (!/Config file not found|Tokenizer file not found|Tokens map file not found/iu.test(msg)) throw initError;
53861
- await ensureFastembedModelSidecars(model, cacheDir);
53862
- return FlagEmbedding.init(initOpts);
53863
- }
53864
- }
53865
- /**
53866
- * Cache entry names fastembed uses for the fixed BGESmallZH model: it extracts
53867
- * into `<cacheDir>/<name>/` from the staged archive `<cacheDir>/<name>.tar.gz`.
53868
- */
53869
- const BGESMALLZH_CACHE_NAME = "fast-bge-small-zh-v1.5";
53870
- const PLATFORM_FAILURE = /@anush008\/tokenizers|Failed to load native binding|Unsupported (?:OS|platform|architecture)|ERR_DLOPEN_FAILED|invalid ELF header|Exec format error|is not a valid Win32 application|GLIBC_\d+[^"\n]*not found|Error loading shared library|cannot open shared object file/iu;
53871
- const ARCHIVE_FAILURE = /\bTAR_[A-Z_]+|incorrect header check|unexpected end of file|invalid tar|zlib/iu;
53872
- const NETWORK_FAILURE = /\b(?:ENOTFOUND|EAI_AGAIN|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EHOSTUNREACH|socket hang up|fetch failed)\b|network error|HTTP \d{3}/iu;
53873
- /**
53874
- * Classify a load failure so callers can say something true about it: the four
53875
- * causes need four different answers (unsupported platform, corrupt cache,
53876
- * network, unknown), and they used to be reported as one network problem.
53877
- */
53878
- function classifyEmbeddingFailure(message) {
53879
- if (message === void 0) return "other";
53880
- if (PLATFORM_FAILURE.test(message)) return "platform";
53881
- if (ARCHIVE_FAILURE.test(message)) return "archive";
53882
- if (NETWORK_FAILURE.test(message)) return "network";
53883
- return "other";
53884
- }
53885
- /**
53886
- * Loads only the module entry point — no model download — so callers can tell
53887
- * "this machine cannot run the local model at all" apart from "the download
53888
- * failed". fastembed imports its native tokenizer binding at module scope, which
53889
- * is exactly the step a platform without a published binary cannot pass.
53890
- */
53891
- async function probeLocalEmbeddingSupport() {
53892
- try {
53893
- await import("fastembed");
53894
- return { supported: true };
53895
- } catch (error) {
53896
- return {
53897
- supported: false,
53898
- error: error instanceof Error ? error.message : String(error)
53899
- };
53900
- }
53901
- }
53902
- /**
53903
- * Remove every trace of a previous BGESmallZH download: the extracted model
53904
- * directory *and* the staged archive it came from.
53905
- *
53906
- * The archive has to go with it. fastembed skips the download whenever
53907
- * `<cacheDir>/<name>.tar.gz` exists, and it only deletes that file after a
53908
- * *successful* extract — its download error paths (response error, file stream
53909
- * error) reject and leave whatever was written behind. So an archive sitting
53910
- * next to a failed attempt is usually a partial leftover, and keeping it makes
53911
- * every retry extract the same junk locally: no network traffic, a few
53912
- * milliseconds, the same failure forever.
53913
- *
53914
- * Never throws: a file locked by another process (on Windows a mapped ONNX file
53915
- * stays locked) must not replace the download error the caller is reporting.
53916
- */
53917
- function clearEmbeddingModelCache(cacheDir) {
53918
- for (const name of [BGESMALLZH_CACHE_NAME, `${BGESMALLZH_CACHE_NAME}.tar.gz`]) {
53919
- const target = join(cacheDir, name);
53920
- if (!existsSync(target)) continue;
53921
- try {
53922
- rmSync(target, {
53923
- recursive: true,
53924
- force: true
53925
- });
53926
- } catch {}
53927
- }
53928
- }
53929
- /**
53930
- * Small config/tokenizer files that fastembed expects alongside model.onnx.
53931
- * If these are missing (e.g. GCS download partially failed), fastembed throws.
53932
- * We download them from HuggingFace so the model can load — this covers the
53933
- * case where the GCS tarball was incomplete but the HF repo is reachable.
53934
- */
53935
- const FASTEMBED_SIDECARS = [
53936
- "config.json",
53937
- "tokenizer.json",
53938
- "tokenizer_config.json",
53939
- "special_tokens_map.json"
53940
- ];
53941
- const FASTEMBED_HF_REPOS = { "fast-bge-small-zh-v1.5": "BAAI/bge-small-zh-v1.5" };
53942
- async function ensureFastembedModelSidecars(model, cacheDir) {
53943
- const repo = FASTEMBED_HF_REPOS[model];
53944
- if (repo === void 0) return;
53945
- const modelDir = join(cacheDir ?? "local_cache", model);
53946
- mkdirSync(modelDir, { recursive: true });
53947
- for (const fileName of FASTEMBED_SIDECARS) {
53948
- const target = join(modelDir, fileName);
53949
- try {
53950
- const { access } = await import("node:fs/promises");
53951
- await access(target);
53952
- continue;
53953
- } catch {}
53954
- const hfUrl = `https://huggingface.co/${repo}/resolve/main/${fileName}`;
53955
- try {
53956
- const response = await fetch(hfUrl);
53957
- if (!response.ok) continue;
53958
- const { writeFile } = await import("node:fs/promises");
53959
- await writeFile(target, Buffer.from(await response.arrayBuffer()));
53960
- } catch {}
53961
- }
53962
- }
53963
- //#endregion
53964
- //#region ../../packages/memory/src/store.ts
53965
- const FILE_NAME = "entries.jsonl";
53966
- const MIGRATION_MARKER = ".migrated";
53967
- const SQLITE_MIGRATION_MARKER = ".migrated-to-sqlite";
53968
- var MemoryMemoStore = class MemoryMemoStore {
53969
- projectDir;
53970
- jsonlPath;
53971
- dbPath;
53972
- db;
53973
- initialized = false;
53974
- writeLock = Promise.resolve();
53975
- embeddingEngine;
53976
- embeddingQueue = /* @__PURE__ */ new Set();
53977
- embeddingTimer;
53978
- embeddingFlushing = false;
53979
- embeddingDegraded = false;
53980
- embeddingConsecutiveFailures = 0;
53981
- lastEmbeddingError;
53982
- log;
53983
- constructor(projectDir, log) {
53984
- this.projectDir = projectDir;
53985
- this.jsonlPath = join$1(projectDir, "memory", FILE_NAME);
53986
- this.dbPath = join$1(projectDir, "memory", "memos.sqlite");
53987
- this.log = log ?? {};
53988
- }
53989
- /**
53990
- * Open the SQLite database and run schema migrations. Call this once after
53991
- * construction before relying on reads/writes.
53992
- *
53993
- * Note on async SQLite: Node.js added the asynchronous `Database` class to
53994
- * `node:sqlite` in v23.4.0 (experimental). This package currently supports
53995
- * Node >=22.0.0 and uses `DatabaseSync` because the v22 type definitions do
53996
- * not yet include `Database`. Once the project baseline moves to Node 23+ and
53997
- * the types catch up, the synchronous calls here should be migrated to the
53998
- * async API to avoid blocking the event loop on large operations.
53999
- */
54000
- initPromise = null;
54001
- async init() {
54002
- if (this.initPromise) return this.initPromise;
54003
- this.initPromise = this._doInit().catch((error) => {
54004
- this.initPromise = null;
54005
- throw error;
54006
- });
54007
- return this.initPromise;
54008
- }
54009
- async _doInit() {
54010
- if (this.initialized) return;
54011
- await this.ensureDir();
54012
- this.db = new DatabaseSync(this.dbPath);
54013
- this.db.exec("PRAGMA journal_mode = WAL;");
54014
- this.db.exec("PRAGMA foreign_keys = ON;");
54015
- this.db.exec("PRAGMA busy_timeout = 5000;");
54016
- this.createSchema();
54017
- await this.migrateFromJsonl();
54018
- this.initialized = true;
54019
- }
54020
- /** Close the database handle and checkpoint WAL. */
54021
- close() {
54022
- if (this.embeddingTimer !== void 0) {
54023
- clearTimeout(this.embeddingTimer);
54024
- this.embeddingTimer = void 0;
54025
- }
54026
- this.embeddingQueue.clear();
54027
- if (this.db !== void 0) {
54028
- this.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
54029
- this.db.close();
54030
- this.db = void 0;
54031
- }
54032
- this.initialized = false;
54033
- this.initPromise = null;
54034
- }
54035
- /** Iterate all memos from the database, newest first. Optionally filter by project directory. */
54036
- async *read(options) {
54037
- await this.init();
54038
- if (this.db === void 0) return;
54039
- const projectDir = options?.projectDir;
54040
- const stmt = projectDir === void 0 ? this.db.prepare("SELECT * FROM memos ORDER BY recorded_at DESC") : this.db.prepare("SELECT * FROM memos WHERE project_dir = ? OR project_dir = '' ORDER BY recorded_at DESC");
54041
- const rows = projectDir === void 0 ? stmt.all() : stmt.all(projectDir);
54042
- for (const row of rows) yield rowToMemo(row);
54043
- }
54044
- /** Append a memo. */
54045
- async append(entry) {
54046
- return this.withWriteLock(() => this.appendInternal(entry));
54047
- }
54048
- /**
54049
- * True when a memo with the same source session + user need + approach
54050
- * already exists. Used by compaction-summary recovery to re-store memos
54051
- * idempotently after a crash between compaction apply and extraction.
54052
- * The approach field keeps two distinct memos that happen to share the
54053
- * same user need from being collapsed into one.
54054
- */
54055
- existsBySourceAndNeed(sourceSessionId, userNeed, approach) {
54056
- if (this.db === void 0) return false;
54057
- return this.db.prepare("SELECT 1 FROM memos WHERE source_session_id = ? AND user_need = ? AND approach = ? LIMIT 1").get(sourceSessionId, userNeed, approach) !== void 0;
54058
- }
54059
- /** Delete a memo by id. */
54060
- async delete(id) {
54061
- return this.withWriteLock(() => this.deleteInternal(id));
54062
- }
54063
- /** Get a single memo by ID. */
54064
- async get(id) {
54065
- await this.init();
54066
- if (this.db === void 0) return void 0;
54067
- const row = this.db.prepare("SELECT * FROM memos WHERE id = ?").get(id);
54068
- return row !== void 0 ? rowToMemo(row) : void 0;
54069
- }
54070
- /**
54071
- * Full-text search over memos using the FTS5 index.
54072
- *
54073
- * Returns raw candidates newest first. Callers that need ranking should pass
54074
- * the results to `rankMemos`. An empty or whitespace-only query returns an
54075
- * empty array.
54076
- */
54077
- async search(query, options) {
54078
- await this.init();
54079
- if (this.db === void 0) return [];
54080
- const ftsQuery = buildFtsQuery(query);
54081
- if (ftsQuery === void 0) return [];
54082
- const limit = options?.candidateLimit ?? 200;
54083
- const projectDir = options?.projectDir;
54084
- const stmt = projectDir === void 0 ? this.db.prepare(`SELECT m.* FROM memos m
54085
- JOIN memos_fts f ON m.rowid = f.rowid
54086
- WHERE f.memos_fts MATCH ?
54087
- ORDER BY m.recorded_at DESC LIMIT ?`) : this.db.prepare(`SELECT m.* FROM memos m
54088
- JOIN memos_fts f ON m.rowid = f.rowid
54089
- WHERE f.memos_fts MATCH ? AND (m.project_dir = ? OR m.project_dir = '')
54090
- ORDER BY m.recorded_at DESC LIMIT ?`);
54091
- return (projectDir === void 0 ? stmt.all(ftsQuery, limit) : stmt.all(ftsQuery, projectDir, limit)).map(rowToMemo);
54092
- }
54093
- /** List memos with optional full-text search and pagination. */
54094
- async list(options) {
54095
- await this.init();
54096
- if (this.db === void 0) return {
54097
- memos: [],
54098
- total: 0
54099
- };
54100
- const search = options?.search?.toLowerCase().trim();
54101
- const limit = options?.limit ?? 50;
54102
- const offset = options?.offset ?? 0;
54103
- const projectDir = options?.projectDir;
54104
- if (search !== void 0 && search.length > 0) {
54105
- let candidates = await this.search(search, { projectDir });
54106
- if (candidates.length === 0) for await (const memo of this.read({ projectDir })) candidates.push(memo);
54107
- const filtered = candidates.filter((memo) => memoMatchesSearch(memo, search));
54108
- const total = filtered.length;
54109
- return {
54110
- memos: filtered.slice(offset, offset + limit).map(toSummary),
54111
- total
54112
- };
54113
- }
54114
- const { rows, total } = this.listAll(limit, offset, projectDir);
54115
- return {
54116
- memos: rows.map(toSummary),
54117
- total
54118
- };
54119
- }
54120
- /**
54121
- * One-time migration from per-workDir memory stores to a global store.
54122
- * Reads `<screamHomeDir>/sessions/<workDirKey>/memory/entries.jsonl`
54123
- * and appends valid entries to the global SQLite store.
54124
- * Deletes the legacy per-session memory files afterwards and writes a marker
54125
- * file so the migration only runs once.
54126
- */
54127
- static async migrateLegacyStores(screamHomeDir) {
54128
- const target = new MemoryMemoStore(screamHomeDir);
54129
- const markerPath = join$1(screamHomeDir, "memory", MIGRATION_MARKER);
54130
- try {
54131
- await stat(markerPath);
54132
- return;
54133
- } catch {}
54134
- const sessionsDir = join$1(screamHomeDir, "sessions");
54135
- let sessionEntries;
54136
- try {
54137
- sessionEntries = await readdir(sessionsDir, { withFileTypes: true }).then((entries) => entries.filter((e) => e.isDirectory()).map((e) => e.name));
54138
- } catch {
54139
- await writeFile(markerPath, "", "utf8").catch(() => {});
54140
- return;
54141
- }
54142
- const migratedIds = /* @__PURE__ */ new Set();
54143
- for await (const memo of target.read()) migratedIds.add(memo.id);
54144
- let migratedCount = 0;
54145
- const legacyPaths = [];
54146
- for (const sessionKey of sessionEntries) {
54147
- const legacyPath = join$1(sessionsDir, sessionKey, "memory", FILE_NAME);
54148
- let stream;
54149
- try {
54150
- stream = createReadStream(legacyPath, { encoding: "utf8" });
54151
- } catch {}
54152
- stream.on("error", () => {});
54153
- let line = "";
54154
- try {
54155
- for await (const chunk of stream) {
54156
- line += chunk;
54157
- let newlineIndex = line.indexOf("\n");
54158
- while (newlineIndex !== -1) {
54159
- const rawLine = line.slice(0, newlineIndex).replace(/\r$/, "");
54160
- line = line.slice(newlineIndex + 1);
54161
- newlineIndex = line.indexOf("\n");
54162
- const memo = target.parseLine(rawLine, 0);
54163
- if (memo === void 0 || migratedIds.has(memo.id)) continue;
54164
- await target.append(memo);
54165
- migratedIds.add(memo.id);
54166
- migratedCount++;
54167
- }
54168
- }
54169
- } catch {
54170
- continue;
54171
- }
54172
- legacyPaths.push(legacyPath);
54173
- }
54174
- for (const legacyPath of legacyPaths) {
54175
- await unlink(legacyPath).catch(() => {});
54176
- await rmdir(dirname$2(legacyPath)).catch(() => {});
54177
- }
54178
- await writeFile(markerPath, `${migratedCount}\n`, "utf8").catch(() => {});
54179
- }
54180
- /** @internal */
54181
- parseLine(rawLine, _lineNumber) {
54182
- if (rawLine.length === 0) return void 0;
54183
- try {
54184
- const record = JSON.parse(rawLine);
54185
- if (record["type"] !== "memory_memo" || !record["entry"]) return void 0;
54186
- const entry = record["entry"];
54187
- if (record["version"] === 1 || entry["userRequirement"] !== void 0 && entry["userNeed"] === void 0) {
54188
- const str = (v, fallback = "") => typeof v === "string" ? v : fallback;
54189
- return {
54190
- id: str(entry["id"]),
54191
- sourceSessionId: str(entry["sourceSessionId"]),
54192
- sourceSessionTitle: str(entry["sourceSessionTitle"], void 0),
54193
- userNeed: str(entry["userRequirement"]),
54194
- approach: str(entry["solution"]),
54195
- outcome: str(entry["completionStatus"]),
54196
- whatFailed: str(entry["problemsEncountered"], "none"),
54197
- whatWorked: "none",
54198
- extractionSource: entry["extractionSource"] === "exit" ? "exit" : "compaction",
54199
- recordedAt: typeof entry["recordedAt"] === "number" ? entry["recordedAt"] : 0,
54200
- projectDir: str(entry["projectDir"])
54201
- };
54202
- }
54203
- const src = entry["extractionSource"];
54204
- if (typeof src === "string" && ![
54205
- "compaction",
54206
- "exit",
54207
- "manual"
54208
- ].includes(src)) return;
54209
- return entry;
54210
- } catch {
54211
- return;
54212
- }
54213
- }
54214
- createSchema() {
54215
- if (this.db === void 0) return;
54216
- this.db.exec(`
54217
- CREATE TABLE IF NOT EXISTS memos (
54218
- id TEXT PRIMARY KEY,
54219
- source_session_id TEXT NOT NULL,
54220
- source_session_title TEXT,
54221
- user_need TEXT NOT NULL,
54222
- approach TEXT NOT NULL,
54223
- outcome TEXT NOT NULL,
54224
- what_failed TEXT NOT NULL DEFAULT 'none',
54225
- what_worked TEXT NOT NULL DEFAULT 'none',
54226
- extraction_source TEXT NOT NULL CHECK(extraction_source IN ('compaction', 'exit', 'manual')),
54227
- recorded_at INTEGER NOT NULL,
54228
- project_dir TEXT NOT NULL DEFAULT '',
54229
- tags TEXT NOT NULL DEFAULT '[]'
54230
- );
54231
-
54232
- CREATE INDEX IF NOT EXISTS idx_memos_project_dir ON memos(project_dir);
54233
-
54234
- CREATE VIRTUAL TABLE IF NOT EXISTS memos_fts USING fts5(
54235
- user_need,
54236
- approach,
54237
- what_failed,
54238
- what_worked,
54239
- source_session_title,
54240
- content=''
54241
- );
54242
-
54243
- CREATE TABLE IF NOT EXISTS memory_embeddings (
54244
- memory_id TEXT PRIMARY KEY REFERENCES memos(id) ON DELETE CASCADE,
54245
- embedding_json TEXT NOT NULL,
54246
- model TEXT NOT NULL DEFAULT '${EMBEDDING_MODEL_NAME}',
54247
- created_at INTEGER NOT NULL
54248
- );
54249
-
54250
- CREATE INDEX IF NOT EXISTS idx_memory_embeddings_created_at ON memory_embeddings(created_at DESC);
54251
- `);
54252
- this.migrateSchema();
54253
- }
54254
- migrateSchema() {
54255
- if (this.db === void 0) return;
54256
- const info = this.db.prepare("PRAGMA table_info(memos)").all();
54257
- if (!info.some((col) => col.name === "project_dir")) this.db.exec("ALTER TABLE memos ADD COLUMN project_dir TEXT NOT NULL DEFAULT ''");
54258
- if (!info.some((col) => col.name === "tags")) this.db.exec("ALTER TABLE memos ADD COLUMN tags TEXT NOT NULL DEFAULT '[]'");
54259
- this.db.exec(`
54260
- CREATE INDEX IF NOT EXISTS idx_memos_project_dir ON memos(project_dir);
54261
- CREATE INDEX IF NOT EXISTS idx_memory_embeddings_created_at ON memory_embeddings(created_at DESC);
54262
- `);
54263
- }
54264
- async migrateFromJsonl() {
54265
- const markerPath = join$1(this.projectDir, "memory", SQLITE_MIGRATION_MARKER);
54266
- try {
54267
- await stat(markerPath);
54268
- return;
54269
- } catch {}
54270
- const memos = [];
54271
- let stream;
54272
- try {
54273
- stream = createReadStream(this.jsonlPath, { encoding: "utf8" });
54274
- } catch {
54275
- await writeFile(markerPath, "", "utf8").catch(() => {});
54276
- return;
54277
- }
54278
- stream.on("error", () => {});
54279
- let line = "";
54280
- try {
54281
- for await (const chunk of stream) {
54282
- line += chunk;
54283
- let newlineIndex = line.indexOf("\n");
54284
- while (newlineIndex !== -1) {
54285
- const rawLine = line.slice(0, newlineIndex).replace(/\r$/, "");
54286
- line = line.slice(newlineIndex + 1);
54287
- newlineIndex = line.indexOf("\n");
54288
- const memo = this.parseLine(rawLine, 0);
54289
- if (memo !== void 0) memos.push(memo);
54290
- }
54291
- }
54292
- } catch {}
54293
- if (memos.length > 0) this.insertMany(memos);
54294
- await writeFile(markerPath, "", "utf8").catch(() => {});
54295
- await rename(this.jsonlPath, `${this.jsonlPath}.bak`).catch(() => {});
54296
- await unlink(join$1(this.projectDir, "memory", "index.json")).catch(() => {});
54297
- }
54298
- insertMany(memos) {
54299
- if (this.db === void 0 || memos.length === 0) return;
54300
- const insert = this.db.prepare(`INSERT OR IGNORE INTO memos (
54301
- id, source_session_id, source_session_title, user_need, approach,
54302
- outcome, what_failed, what_worked, extraction_source, recorded_at, project_dir, tags
54303
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
54304
- RETURNING rowid`);
54305
- const insertFts = this.db.prepare(`INSERT INTO memos_fts(rowid, user_need, approach, what_failed, what_worked, source_session_title)
54306
- VALUES (?, ?, ?, ?, ?, ?)`);
54307
- this.db.exec("BEGIN TRANSACTION");
54308
- try {
54309
- for (const memo of memos) {
54310
- const row = insert.get(memo.id, memo.sourceSessionId, memo.sourceSessionTitle ?? null, memo.userNeed, memo.approach, memo.outcome, memo.whatFailed, memo.whatWorked, memo.extractionSource, memo.recordedAt, memo.projectDir ?? "", JSON.stringify(memo.tags ?? []));
54311
- if (row === void 0) continue;
54312
- insertFts.run(row.rowid, toFtsText(memo.userNeed), toFtsText(memo.approach), toFtsText(memo.whatFailed), toFtsText(memo.whatWorked), toFtsText(memo.sourceSessionTitle ?? ""));
54313
- }
54314
- this.db.exec("COMMIT");
54315
- } catch (error) {
54316
- this.db.exec("ROLLBACK");
54317
- throw new Error(`Failed to migrate memos to SQLite: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
54318
- }
54319
- }
54320
- async appendInternal(entry) {
54321
- await this.init();
54322
- if (this.db === void 0) return;
54323
- const insert = this.db.prepare(`INSERT INTO memos (
54324
- id, source_session_id, source_session_title, user_need, approach,
54325
- outcome, what_failed, what_worked, extraction_source, recorded_at, project_dir, tags
54326
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
54327
- RETURNING rowid`);
54328
- const insertFts = this.db.prepare(`INSERT INTO memos_fts(rowid, user_need, approach, what_failed, what_worked, source_session_title)
54329
- VALUES (?, ?, ?, ?, ?, ?)`);
54330
- this.db.exec("BEGIN TRANSACTION");
54331
- try {
54332
- const row = insert.get(entry.id, entry.sourceSessionId, entry.sourceSessionTitle ?? null, entry.userNeed, entry.approach, entry.outcome, entry.whatFailed, entry.whatWorked, entry.extractionSource, entry.recordedAt, entry.projectDir ?? "", JSON.stringify(entry.tags ?? []));
54333
- insertFts.run(row.rowid, toFtsText(entry.userNeed), toFtsText(entry.approach), toFtsText(entry.whatFailed), toFtsText(entry.whatWorked), toFtsText(entry.sourceSessionTitle ?? ""));
54334
- this.db.exec("COMMIT");
54335
- this.scheduleEmbedding(entry);
54336
- } catch {
54337
- this.db.exec("ROLLBACK");
54338
- throw new Error("Failed to append memo");
54339
- }
54340
- }
54341
- /** Update a memo by id. Returns true if the memo existed and was updated. */
54342
- async update(id, patch) {
54343
- return this.withWriteLock(() => this.updateInternal(id, patch));
54344
- }
54345
- /** @internal */
54346
- async updateInternal(id, patch) {
54347
- await this.init();
54348
- if (this.db === void 0) return false;
54349
- const existing = await this.get(id);
54350
- if (existing === void 0) return false;
54351
- const updated = {
54352
- ...existing,
54353
- ...patch
54354
- };
54355
- const selectRow = this.db.prepare("SELECT rowid FROM memos WHERE id = ?");
54356
- const update = this.db.prepare(`UPDATE memos SET
54357
- rowid = (SELECT COALESCE(MAX(rowid), 0) + 1 FROM memos),
54358
- source_session_id = ?,
54359
- source_session_title = ?,
54360
- user_need = ?,
54361
- approach = ?,
54362
- outcome = ?,
54363
- what_failed = ?,
54364
- what_worked = ?,
54365
- extraction_source = ?,
54366
- recorded_at = ?,
54367
- project_dir = ?,
54368
- tags = ?
54369
- WHERE id = ?
54370
- RETURNING rowid`);
54371
- const updateFts = this.db.prepare(`INSERT INTO memos_fts(rowid, user_need, approach, what_failed, what_worked, source_session_title)
54372
- VALUES (?, ?, ?, ?, ?, ?)`);
54373
- const deleteFts = this.db.prepare("INSERT INTO memos_fts(memos_fts, rowid) VALUES ('delete', ?)");
54374
- const deleteEmbedding = this.db.prepare("DELETE FROM memory_embeddings WHERE memory_id = ?");
54375
- this.db.exec("BEGIN TRANSACTION");
54376
- try {
54377
- const oldRow = selectRow.get(id);
54378
- if (oldRow === void 0) {
54379
- this.db.exec("ROLLBACK");
54380
- return false;
54381
- }
54382
- const row = update.get(updated.sourceSessionId, updated.sourceSessionTitle ?? null, updated.userNeed, updated.approach, updated.outcome, updated.whatFailed, updated.whatWorked, updated.extractionSource, updated.recordedAt, updated.projectDir ?? "", JSON.stringify(updated.tags ?? []), id);
54383
- if (row === void 0) {
54384
- this.db.exec("ROLLBACK");
54385
- return false;
54386
- }
54387
- deleteFts.run(oldRow.rowid);
54388
- updateFts.run(row.rowid, toFtsText(updated.userNeed), toFtsText(updated.approach), toFtsText(updated.whatFailed), toFtsText(updated.whatWorked), toFtsText(updated.sourceSessionTitle ?? ""));
54389
- deleteEmbedding.run(id);
54390
- this.db.exec("COMMIT");
54391
- this.scheduleEmbedding(updated);
54392
- return true;
54393
- } catch {
54394
- this.db.exec("ROLLBACK");
54395
- throw new Error("Failed to update memo");
54396
- }
54397
- }
54398
- async deleteInternal(id) {
54399
- await this.init();
54400
- if (this.db === void 0) return false;
54401
- const row = this.db.prepare("SELECT rowid FROM memos WHERE id = ?").get(id);
54402
- if (row === void 0) return true;
54403
- const deleteFts = this.db.prepare("INSERT INTO memos_fts(memos_fts, rowid) VALUES ('delete', ?)");
54404
- const deleteMemo = this.db.prepare("DELETE FROM memos WHERE id = ?");
54405
- this.db.exec("BEGIN TRANSACTION");
54406
- try {
54407
- deleteFts.run(row.rowid);
54408
- deleteMemo.run(id);
54409
- this.db.exec("COMMIT");
54410
- return true;
54411
- } catch {
54412
- this.db.exec("ROLLBACK");
54413
- throw new Error("Failed to delete memo");
54414
- }
54415
- }
54416
- /** Set the embedding engine. Call once after construction, before any writes. */
54417
- setEmbeddingEngine(engine) {
54418
- this.embeddingEngine = engine;
54419
- }
54420
- /** Check whether the store has any vector embeddings. */
54421
- hasEmbeddings() {
54422
- if (this.db === void 0) return false;
54423
- return (this.db.prepare("SELECT COUNT(*) as count FROM memory_embeddings").get()?.count ?? 0) > 0;
54424
- }
54425
- /** Access the embedding engine (may be undefined if not configured). */
54426
- getEmbeddingEngine() {
54427
- return this.embeddingEngine;
54428
- }
54429
- /**
54430
- * Search memos by vector similarity. Returns memos sorted by cosine
54431
- * similarity (highest first). Falls back to empty if no embeddings exist.
54432
- *
54433
- * NOTE: candidates are ordered by created_at DESC, so only the newest
54434
- * N embeddings are considered. Older but highly relevant memos may be
54435
- * omitted when the store has more than candidateLimit embeddings.
54436
- * This is a known limitation — see M-M5 in project review notes.
54437
- * Performance notes:
54438
- * - candidateLimit bounds the SQL query so we never load every embedding.
54439
- * - recencyCutoffDays lets callers ignore very old memos.
54440
- * - projectDir is pushed into the SQL JOIN so unrelated projects are not
54441
- * considered at all.
54442
- */
54443
- async searchByVector(queryEmbedding, options) {
54444
- await this.init();
54445
- if (this.db === void 0) return [];
54446
- const limit = options?.candidateLimit ?? 200;
54447
- const projectDir = options?.projectDir;
54448
- const recencyCutoffDays = options?.recencyCutoffDays;
54449
- const cutoffMs = recencyCutoffDays !== void 0 && recencyCutoffDays > 0 ? Date.now() - recencyCutoffDays * 24 * 60 * 60 * 1e3 : void 0;
54450
- let rows;
54451
- if (projectDir !== void 0) {
54452
- const stmt = cutoffMs === void 0 ? this.db.prepare(`
54453
- SELECT e.memory_id, e.embedding_json
54454
- FROM memory_embeddings e
54455
- JOIN memos m ON m.id = e.memory_id
54456
- WHERE (m.project_dir = ? OR m.project_dir = '')
54457
- ORDER BY e.created_at DESC
54458
- LIMIT ?
54459
- `) : this.db.prepare(`
54460
- SELECT e.memory_id, e.embedding_json
54461
- FROM memory_embeddings e
54462
- JOIN memos m ON m.id = e.memory_id
54463
- WHERE (m.project_dir = ? OR m.project_dir = '')
54464
- AND e.created_at > ?
54465
- ORDER BY e.created_at DESC
54466
- LIMIT ?
54467
- `);
54468
- rows = cutoffMs === void 0 ? stmt.all(projectDir, limit) : stmt.all(projectDir, cutoffMs, limit);
54469
- } else {
54470
- const stmt = cutoffMs === void 0 ? this.db.prepare("SELECT memory_id, embedding_json FROM memory_embeddings ORDER BY created_at DESC LIMIT ?") : this.db.prepare("SELECT memory_id, embedding_json FROM memory_embeddings WHERE created_at > ? ORDER BY created_at DESC LIMIT ?");
54471
- rows = cutoffMs === void 0 ? stmt.all(limit) : stmt.all(cutoffMs, limit);
54472
- }
54473
- if (rows.length === 0) return [];
54474
- const scored = [];
54475
- for (const row of rows) try {
54476
- const vec = new Float32Array(JSON.parse(row.embedding_json));
54477
- const similarity = this.embeddingEngine?.cosineSimilarity(queryEmbedding, vec) ?? 0;
54478
- if (similarity > 0) scored.push({
54479
- id: row.memory_id,
54480
- score: similarity
54481
- });
54482
- } catch {}
54483
- scored.sort((a, b) => b.score - a.score);
54484
- const topScored = scored.slice(0, limit);
54485
- const results = [];
54486
- for (const { id, score } of topScored) {
54487
- const row = this.db.prepare("SELECT * FROM memos WHERE id = ?").get(id);
54488
- if (row !== void 0) results.push({
54489
- memo: rowToMemo(row),
54490
- score
54491
- });
54492
- }
54493
- return results;
54494
- }
54495
- /**
54496
- * Schedule async embedding generation for a memo. Debounced — the actual
54497
- * batch flush runs after a short quiet period. Never blocks the caller.
54498
- */
54499
- scheduleEmbedding(memo) {
54500
- this.embeddingQueue.add(memo.id);
54501
- if (this.embeddingEngine === void 0) return;
54502
- if (!this.embeddingEngine.available) {
54503
- this.embeddingEngine.ensureReady().then((ready) => {
54504
- if (ready && this.embeddingQueue.size > 0 && this.embeddingTimer === void 0) this.embeddingTimer = setTimeout(() => {
54505
- this.flushEmbeddings();
54506
- }, 2e3);
54507
- }).catch(() => {});
54508
- return;
54509
- }
54510
- if (this.embeddingTimer !== void 0) clearTimeout(this.embeddingTimer);
54511
- this.embeddingTimer = setTimeout(() => {
54512
- this.flushEmbeddings();
54513
- }, 2e3);
54514
- }
54515
- /**
54516
- * Flush queued embedding generation. Retries once on failure to tolerate
54517
- * transient model-load contention, then marks embeddings as degraded and
54518
- * logs the problem. Callers can still retrieve memos through keyword search.
54519
- */
54520
- async flushEmbeddings() {
54521
- if (this.embeddingFlushing || this.embeddingEngine === void 0 || !this.embeddingEngine.available) return;
54522
- this.embeddingFlushing = true;
54523
- const ids = [...this.embeddingQueue];
54524
- this.embeddingQueue.clear();
54525
- const reenqueue = () => {
54526
- for (const id of ids) this.embeddingQueue.add(id);
54527
- };
54528
- try {
54529
- await this.init();
54530
- if (this.db === void 0) return;
54531
- const pending = [];
54532
- for (const id of ids) {
54533
- if (this.db.prepare("SELECT id FROM memory_embeddings WHERE memory_id = ?").get(id) !== void 0) continue;
54534
- const memo = await this.get(id);
54535
- if (memo !== void 0) pending.push({
54536
- id,
54537
- text: buildEmbeddingText(memo)
54538
- });
54539
- }
54540
- if (pending.length === 0) return;
54541
- const vectors = await this.tryEmbedBatch(pending.map((p) => p.text));
54542
- if (vectors === null || vectors.length !== pending.length) {
54543
- this.markEmbeddingFailure(/* @__PURE__ */ new Error(vectors === null ? "embedBatch returned null" : "embedding count mismatch"));
54544
- reenqueue();
54545
- return;
54546
- }
54547
- this.clearEmbeddingFailure();
54548
- const insert = this.db.prepare("INSERT OR REPLACE INTO memory_embeddings (memory_id, embedding_json, model, created_at) VALUES (?, ?, ?, ?)");
54549
- const now = Date.now();
54550
- this.db.exec("BEGIN TRANSACTION");
54551
- try {
54552
- for (let i = 0; i < pending.length; i++) insert.run(pending[i].id, JSON.stringify([...vectors[i]]), EMBEDDING_MODEL_NAME, now);
54553
- this.db.exec("COMMIT");
54554
- } catch (error) {
54555
- this.db.exec("ROLLBACK");
54556
- this.markEmbeddingFailure(error instanceof Error ? error : new Error(String(error)));
54557
- reenqueue();
54558
- }
54559
- } catch (error) {
54560
- this.markEmbeddingFailure(error instanceof Error ? error : new Error(String(error)));
54561
- reenqueue();
54562
- } finally {
54563
- this.embeddingFlushing = false;
54564
- this.embeddingTimer = void 0;
54565
- }
54566
- }
54567
- async tryEmbedBatch(texts) {
54568
- if (this.embeddingEngine === void 0) return null;
54569
- try {
54570
- const first = await this.embeddingEngine.embedBatch(texts);
54571
- if (first !== null) return first;
54572
- } catch {}
54573
- try {
54574
- return await this.embeddingEngine.embedBatch(texts);
54575
- } catch {
54576
- return null;
54577
- }
54578
- }
54579
- markEmbeddingFailure(error) {
54580
- this.embeddingDegraded = true;
54581
- this.embeddingConsecutiveFailures += 1;
54582
- this.lastEmbeddingError = error;
54583
- this.log.warn?.("embedding flush failed", {
54584
- error: error.message,
54585
- consecutiveFailures: this.embeddingConsecutiveFailures
54586
- });
54587
- }
54588
- clearEmbeddingFailure() {
54589
- this.embeddingDegraded = false;
54590
- this.embeddingConsecutiveFailures = 0;
54591
- this.lastEmbeddingError = void 0;
54592
- }
54593
- /**
54594
- * Runtime health of the embedding subsystem. When `degraded` is true, the
54595
- * store still serves keyword search; only vector similarity is unavailable.
54596
- */
54597
- embeddingStatus() {
54598
- return {
54599
- available: this.embeddingEngine !== void 0 && this.embeddingEngine.available,
54600
- degraded: this.embeddingDegraded,
54601
- consecutiveFailures: this.embeddingConsecutiveFailures,
54602
- lastError: this.lastEmbeddingError?.message
54603
- };
54604
- }
54605
- listAll(limit, offset, projectDir) {
54606
- if (this.db === void 0) return {
54607
- rows: [],
54608
- total: 0
54609
- };
54610
- const countStmt = projectDir === void 0 ? this.db.prepare("SELECT COUNT(*) as total FROM memos") : this.db.prepare("SELECT COUNT(*) as total FROM memos WHERE project_dir = ? OR project_dir = ''");
54611
- const total = (projectDir === void 0 ? countStmt.get() : countStmt.get(projectDir))?.total ?? 0;
54612
- const stmt = projectDir === void 0 ? this.db.prepare("SELECT * FROM memos ORDER BY recorded_at DESC LIMIT ? OFFSET ?") : this.db.prepare("SELECT * FROM memos WHERE project_dir = ? OR project_dir = '' ORDER BY recorded_at DESC LIMIT ? OFFSET ?");
54613
- return {
54614
- rows: (projectDir === void 0 ? stmt.all(limit, offset) : stmt.all(projectDir, limit, offset)).map(rowToMemo),
54615
- total
54616
- };
54617
- }
54618
- async ensureDir() {
54619
- await mkdir(dirname$2(this.dbPath), { recursive: true });
54620
- }
54621
- async withWriteLock(fn) {
54622
- const next = this.writeLock.then(fn, fn);
54623
- this.writeLock = next;
54624
- return next;
54625
- }
54626
- };
54627
- function rowToMemo(row) {
54628
- const sourceSessionTitle = row["source_session_title"];
54629
- const projectDir = row["project_dir"];
54630
- return {
54631
- id: String(row["id"]),
54632
- sourceSessionId: String(row["source_session_id"]),
54633
- sourceSessionTitle: typeof sourceSessionTitle === "string" ? sourceSessionTitle : void 0,
54634
- userNeed: String(row["user_need"]),
54635
- approach: String(row["approach"]),
54636
- outcome: String(row["outcome"]),
54637
- whatFailed: String(row["what_failed"]),
54638
- whatWorked: String(row["what_worked"]),
54639
- extractionSource: row["extraction_source"],
54640
- recordedAt: Number(row["recorded_at"]),
54641
- projectDir: typeof projectDir === "string" ? projectDir : "",
54642
- tags: parseTags(row["tags"])
54643
- };
54644
- }
54645
- function parseTags(value) {
54646
- if (typeof value !== "string" || value.length === 0) return void 0;
54647
- try {
54648
- const parsed = JSON.parse(value);
54649
- if (!Array.isArray(parsed)) return void 0;
54650
- const tags = parsed.filter((t) => typeof t === "string");
54651
- return tags.length > 0 ? tags : void 0;
54652
- } catch {
54653
- return;
54654
- }
54655
- }
54656
- function memoMatchesSearch(memo, search) {
54657
- return [
54658
- memo.userNeed,
54659
- memo.approach,
54660
- memo.whatFailed,
54661
- memo.whatWorked,
54662
- memo.sourceSessionTitle ?? "",
54663
- ...memo.tags ?? []
54664
- ].join(" ").toLowerCase().replaceAll(/\s+/g, "").includes(search.replaceAll(/\s+/g, ""));
54665
- }
54666
- /**
54667
- * Tokenize text so FTS5's unicode61 tokenizer can index mixed CJK/ASCII text.
54668
- * CJK characters are split into individual characters separated by spaces, and
54669
- * CJK/ASCII boundaries are also separated so "使用redis缓存" becomes searchable
54670
- * by "redis" as well as by individual CJK characters.
54671
- */
54672
- function toFtsText(text) {
54673
- const parts = text.toLowerCase().replaceAll(/([一-鿿㐀-䶿])([a-z0-9])/g, "$1 $2").replaceAll(/([a-z0-9])([一-鿿㐀-䶿])/g, "$1 $2").split(/[^a-z0-9一-鿿㐀-䶿]+/);
54674
- const tokens = [];
54675
- for (const part of parts) {
54676
- if (part.length === 0) continue;
54677
- if (/^[a-z0-9]+$/.test(part)) tokens.push(part);
54678
- else for (const ch of part) if (ch.length > 0) tokens.push(ch);
54679
- }
54680
- return tokens.join(" ");
54681
- }
54682
- function buildFtsQuery(search) {
54683
- const tokens = toFtsText(search).split(/\s+/).filter((t) => t.length > 0);
54684
- if (tokens.length === 0) return void 0;
54685
- return tokens.map((t) => `"${t.replaceAll("\"", "\"\"")}"`).join(" AND ");
54686
- }
54687
- //#endregion
54688
- //#region ../../packages/memory/src/extractor.ts
54689
- /**
54690
- * Parse memory-memo blocks from LLM compaction output.
54691
- */
54692
- function parseMemoryMemos(text) {
54693
- const memos = [];
54694
- const regex = /```memory-memo[\s\S]*?\n([\s\S]*?)```/g;
54695
- let match;
54696
- while ((match = regex.exec(text)) !== null) {
54697
- const jsonStr = match[1]?.trim();
54698
- if (!jsonStr) continue;
54699
- try {
54700
- const parsed = JSON.parse(jsonStr);
54701
- if (parsed["none"] === true) continue;
54702
- const userNeed = typeof parsed["userNeed"] === "string" ? parsed["userNeed"].trim() : "";
54703
- if (userNeed.length === 0) continue;
54704
- const rawTags = parsed["tags"];
54705
- const tags = Array.isArray(rawTags) ? normalizeTags(rawTags) : void 0;
54706
- const note = typeof parsed["note"] === "string" ? parsed["note"].trim() : void 0;
54707
- memos.push(createMemoryMemo({
54708
- userNeed,
54709
- approach: typeof parsed["approach"] === "string" ? parsed["approach"].trim() : "",
54710
- outcome: typeof parsed["outcome"] === "string" ? parsed["outcome"].trim() : "",
54711
- whatFailed: typeof parsed["whatFailed"] === "string" ? parsed["whatFailed"].trim() : "none",
54712
- whatWorked: typeof parsed["whatWorked"] === "string" ? parsed["whatWorked"].trim() : "none",
54713
- tags,
54714
- note: note && note.length > 0 ? note : void 0,
54715
- extractionSource: "compaction",
54716
- sourceSessionId: "",
54717
- sourceSessionTitle: ""
54718
- }));
54719
- } catch {}
54720
- }
54721
- return memos;
54722
- }
54723
- /**
54724
- * Strip injected memory-memo content from text before writing it back to the
54725
- * store, preventing feedback loops where recalled/compacted content gets
54726
- * re-stored. Removes fenced ```memory-memo blocks (the format emitted by
54727
- * compaction and exit-time extraction) and any <memories>...</memories>
54728
- * wrapper an injector might use.
54729
- */
54730
- function stripMemoryTags(text) {
54731
- return text.replaceAll(/```memory-memo[\s\S]*?```/gi, "").replaceAll(/<memories>[\s\S]*?<\/memories>/gi, "").trim();
54732
- }
54733
- /** System prompt for exit-time extraction — instructs the LLM how to extract. */
54734
- const EXIT_EXTRACTION_SYSTEM_PROMPT = "你是一个任务经验提取助手。任务是从对话记录中识别已完成的任务闭环,提炼出任务经验记录。用对话的主要语言输出(中文对话用中文,英文对话用英文)。只输出指定的 JSON 格式,不要调用任何工具。";
54735
- /** Build the user prompt for exit-time extraction, including a conversation sample. */
54736
- function buildExitExtractionPrompt(sessionId, messageCount, sampleText) {
54737
- return `以下是会话 "${sessionId}"(共 ${messageCount} 条消息)的对话记录。请提取其中所有**已完成的任务闭环**:
54738
-
54739
- 判断标准:
54740
- - 用户提出了明确的需求或问题
54741
- - 给出了解决方案或回答
54742
- - 结果明确(成功、部分完成、失败)
54743
-
54744
- 对每个已完成的任务闭环,输出一个结构化经验记录。**必须用对话的主要语言书写**:
54745
-
54746
- \`\`\`memory-memo
54747
- {
54748
- "userNeed": "<用户需求/目标,一句话概括>",
54749
- "approach": "<执行方案,做了什么,2-4 句话>",
54750
- "outcome": "<最终结果,如'完成'、'部分完成'、'失败:原因'>",
54751
- "whatFailed": "<踩坑记录:试了但不行的路,无则填 'none'>",
54752
- "whatWorked": "<成功经验:最终奏效的关键动作,无则填 'none'>",
54753
- "tags": ["<标签1>", "<标签2>", "<标签3>"],
54754
- "note": "<可选:给 AI 的补充说明/建议——让未来理解这条经验更容易,如'这类任务先查接口可用性再写代码'。无则省略>"
54755
- }
54756
- \`\`\`
54757
-
54758
- 注意:
54759
- - tags 是 3-5 个语义标签,概括任务领域/技术栈/动作类型,例如 ["react", "auth", "部署"]
54760
- - whatFailed 记录重要的错误尝试,帮助未来避免重蹈覆辙
54761
- - whatWorked 记录最终成功的关键动作,帮助未来复用经验
54762
- - note 是可选字段,写一句能让未来 AI 更快理解/复用这条经验的说明或建议(软性提示,非强制规则)
54763
- - 跳过未完成的工作,除非其中包含有价值的踩坑经验
54764
- - 将紧密相关的子任务合并为一条记录
54765
- - 严格遵守字段名和 JSON 格式(除 note 外不要添加额外字段)
54766
-
54767
- 如果没有已完成的任务闭环,输出:
54768
- \`\`\`memory-memo
54769
- {"none": true}
54770
- \`\`\`
54771
-
54772
- --- 对话记录(最近 30 条消息)---
54773
-
54774
- ${sampleText}
54775
-
54776
- --- 对话记录结束 ---`;
54777
- }
54778
- //#endregion
54779
- //#region ../../packages/memory/src/consolidator.ts
54780
- const SIMILARITY_THRESHOLD = .45;
54781
- const STALE_DAYS = 30;
54782
- /**
54783
- * Analyze all memos and produce a consolidation plan.
54784
- *
54785
- * Pure logic — no LLM call. Uses keyword similarity to find near-duplicate
54786
- * memos, flags resolved/stale entries.
54787
- */
54788
- async function buildConsolidationPlan(store, options) {
54789
- const allMemos = [];
54790
- for await (const memo of store.read(options)) allMemos.push(memo);
54791
- const summaries = allMemos.map(toSummary);
54792
- const duplicateGroups = findDuplicateGroups(summaries);
54793
- const relatedGroups = findRelatedGroups(summaries, duplicateGroups);
54794
- const resolved = findResolved(summaries);
54795
- const stale = findStale(summaries, STALE_DAYS);
54796
- const dedupedCount = duplicateGroups.reduce((acc, g) => acc + g.memos.length - 1, 0);
54797
- return {
54798
- duplicateGroups,
54799
- relatedGroups,
54800
- resolved,
54801
- stale,
54802
- summary: {
54803
- totalMemos: allMemos.length,
54804
- duplicatesFound: dedupedCount,
54805
- relatedGroupsFound: relatedGroups.length,
54806
- resolvedFound: resolved.length,
54807
- staleFound: stale.length,
54808
- memosAfterConsolidation: allMemos.length - dedupedCount - resolved.length - stale.length
54809
- }
54810
- };
54811
- }
54812
- /**
54813
- * Apply a consolidation plan: delete duplicates, resolved, and stale memos,
54814
- * appending merged replacements for duplicates.
54815
- */
54816
- async function applyConsolidation(store, plan) {
54817
- let deleted = 0;
54818
- let created = 0;
54819
- if (plan.resolved.length > 0) {
54820
- const worked = plan.resolved.filter((m) => m.whatWorked).map((m) => m.whatWorked).join("; ");
54821
- const failed = plan.resolved.filter((m) => m.whatFailed).map((m) => m.whatFailed).join("; ");
54822
- if (worked || failed) {
54823
- const archive = createMemoryMemo({
54824
- sourceSessionId: plan.resolved[0].sourceSessionId,
54825
- userNeed: "已解决任务的经验归档(dream 整理)",
54826
- approach: "多次尝试后解决",
54827
- outcome: "已完成",
54828
- whatWorked: worked || "none",
54829
- whatFailed: failed || "none",
54830
- tags: normalizeTags(plan.resolved.flatMap((m) => m.tags ?? [])),
54831
- projectDir: plan.resolved[0].projectDir,
54832
- extractionSource: "compaction"
54833
- });
54834
- await store.append(archive);
54835
- created++;
54836
- }
54837
- }
54838
- if (plan.stale.length > 0) {
54839
- const worked = plan.stale.filter((m) => m.whatWorked).map((m) => m.whatWorked).join("; ");
54840
- const failed = plan.stale.filter((m) => m.whatFailed).map((m) => m.whatFailed).join("; ");
54841
- if (worked || failed) {
54842
- const archive = createMemoryMemo({
54843
- sourceSessionId: plan.stale[0].sourceSessionId,
54844
- userNeed: "过期记忆的经验归档(dream 整理)",
54845
- approach: "距今超过30天未使用",
54846
- outcome: "已归档",
54847
- whatWorked: worked || "none",
54848
- whatFailed: failed || "none",
54849
- tags: normalizeTags(plan.stale.flatMap((m) => m.tags ?? [])),
54850
- projectDir: plan.stale[0].projectDir,
54851
- extractionSource: "compaction"
54852
- });
54853
- await store.append(archive);
54854
- created++;
54855
- }
54856
- }
54857
- for (const group of plan.duplicateGroups) {
54858
- const newest = group.memos.reduce((a, b) => a.recordedAt > b.recordedAt ? a : b);
54859
- const mergedTags = normalizeTags(group.memos.flatMap((m) => m.tags ?? []));
54860
- const merged = createMemoryMemo({
54861
- sourceSessionId: newest.sourceSessionId,
54862
- sourceSessionTitle: newest.sourceSessionTitle,
54863
- userNeed: group.merged.userNeed,
54864
- approach: group.merged.approach,
54865
- outcome: group.merged.outcome,
54866
- whatFailed: group.merged.whatFailed,
54867
- whatWorked: group.merged.whatWorked,
54868
- tags: group.merged.tags ?? mergedTags,
54869
- projectDir: newest.projectDir,
54870
- extractionSource: "compaction"
54871
- });
54872
- await store.append(merged);
54873
- created++;
54874
- }
54875
- for (const group of plan.duplicateGroups) for (const memo of group.memos) {
54876
- await store.delete(memo.id);
54877
- deleted++;
54878
- }
54879
- for (const memo of plan.resolved) {
54880
- await store.delete(memo.id);
54881
- deleted++;
54882
- }
54883
- for (const memo of plan.stale) {
54884
- await store.delete(memo.id);
54885
- deleted++;
54886
- }
54887
- return {
54888
- deleted,
54889
- created
54890
- };
54891
- }
54892
- function findDuplicateGroups(memos) {
54893
- const groups = [];
54894
- const used = /* @__PURE__ */ new Set();
54895
- for (let i = 0; i < memos.length; i++) {
54896
- const first = memos[i];
54897
- if (!first || used.has(first.id)) continue;
54898
- const cluster = [first];
54899
- for (let j = i + 1; j < memos.length; j++) {
54900
- const candidate = memos[j];
54901
- if (!candidate || used.has(candidate.id)) continue;
54902
- if (cluster.some((m) => {
54903
- return computeKeywordSimilarity(candidate, `${m.userNeed} ${m.approach}`) >= SIMILARITY_THRESHOLD;
54904
- })) cluster.push(candidate);
54905
- }
54906
- if (cluster.length > 1) {
54907
- for (const m of cluster) used.add(m.id);
54908
- groups.push(buildDuplicateGroup(cluster));
54909
- }
54910
- }
54911
- return groups;
54912
- }
54913
- /**
54914
- * Split a whatFailed / whatWorked field into individual claims.
54915
- * Handles both `;` and `;` separators.
54916
- */
54917
- function splitClaims(text) {
54918
- if (!text || text === "none" || text === "无") return [];
54919
- return text.split(/[;;]/).map((s) => s.trim()).filter((s) => s.length > 0);
54920
- }
54921
- /**
54922
- * Extract significant words for contradiction detection:
54923
- * ASCII words >= 3 chars + CJK 2-grams.
54924
- */
54925
- function extractSignificantWords(text) {
54926
- const words = [];
54927
- const lower = text.toLowerCase();
54928
- for (const m of lower.matchAll(/[a-z0-9]+/g)) if (m[0].length >= 3) words.push(m[0]);
54929
- for (const m of lower.matchAll(/[一-鿿]+/g)) {
54930
- const run = m[0];
54931
- for (let i = 0; i < run.length - 1; i++) words.push(run.slice(i, i + 2));
54932
- }
54933
- return words;
54934
- }
54935
- /**
54936
- * Check whether `claim` overlaps with any claim in `against`.
54937
- * 2+ shared significant words = overlap; 1 word is enough for single-word claims.
54938
- */
54939
- function claimsOverlap(claim, against) {
54940
- const words = extractSignificantWords(claim);
54941
- if (words.length === 0) return false;
54942
- for (const other of against) {
54943
- const otherLower = other.toLowerCase();
54944
- const matched = words.filter((w) => otherLower.includes(w)).length;
54945
- if (matched >= 2 || matched >= 1 && words.length === 1) return true;
54946
- }
54947
- return false;
54948
- }
54949
- function buildDuplicateGroup(cluster) {
54950
- const sorted = [...cluster].toSorted((a, b) => b.recordedAt - a.recordedAt);
54951
- const newest = sorted[0];
54952
- const mid = Math.ceil(sorted.length / 2);
54953
- const newer = sorted.slice(0, mid);
54954
- const newerFailedClaims = new Set(newer.flatMap((m) => splitClaims(m.whatFailed)));
54955
- const newerWorkedClaims = new Set(newer.flatMap((m) => splitClaims(m.whatWorked)));
54956
- const failures = /* @__PURE__ */ new Set();
54957
- const successes = /* @__PURE__ */ new Set();
54958
- for (const memo of cluster) {
54959
- for (const claim of splitClaims(memo.whatFailed)) if (!claimsOverlap(claim, newerWorkedClaims)) failures.add(claim);
54960
- for (const claim of splitClaims(memo.whatWorked)) if (!claimsOverlap(claim, newerFailedClaims)) successes.add(claim);
54961
- }
54962
- const bestOutcome = cluster.map((m) => m.outcome).some((o) => o.includes("完成") || o.toLowerCase().includes("done")) ? "完成" : newest.outcome;
54963
- return {
54964
- memos: cluster,
54965
- merged: {
54966
- userNeed: newest.userNeed,
54967
- approach: `合并 ${cluster.length} 条相关记录。最新方案: ${newest.approach}`,
54968
- outcome: bestOutcome,
54969
- whatFailed: failures.size > 0 ? [...failures].join("; ") : "none",
54970
- whatWorked: successes.size > 0 ? [...successes].join("; ") : "none"
54971
- },
54972
- reason: `发现 ${cluster.length} 条相似记录(关键词重叠 > ${Math.round(SIMILARITY_THRESHOLD * 100)}%)`
54973
- };
54974
- }
54975
- /**
54976
- * Extract topic anchors from memo text.
54977
- *
54978
- * Compound identifiers like `sample-project` are strong signals and kept whole.
54979
- * ASCII words >= 3 chars are kept when not stopwords. CJK runs are tokenized
54980
- * into 2-grams so that pure-Chinese themes like "用户认证" can still form
54981
- * groups without relying on noisy single-character Jaccard overlap.
54982
- */
54983
- function extractTopicAnchors(text) {
54984
- const lower = text.toLowerCase();
54985
- const anchors = /* @__PURE__ */ new Set();
54986
- for (const match of lower.matchAll(/[a-z0-9]+[-_][a-z0-9]+(?:[-_][a-z0-9]+)*/g)) {
54987
- const token = match[0];
54988
- if (token.length >= 5) anchors.add(token);
54989
- }
54990
- for (const match of lower.matchAll(/[a-z0-9]+/g)) {
54991
- const token = match[0];
54992
- if (token.length >= 3 && !STOP_WORDS.has(token)) anchors.add(token);
54993
- }
54994
- for (const match of lower.matchAll(/[一-鿿㐀-䶿]+/g)) {
54995
- const run = match[0];
54996
- for (let i = 0; i < run.length - 1; i++) anchors.add(run.slice(i, i + 2));
54997
- }
54998
- return [...anchors];
54999
- }
55000
- function findRelatedGroups(memos, duplicateGroups) {
55001
- const used = /* @__PURE__ */ new Set();
55002
- for (const group of duplicateGroups) for (const memo of group.memos) used.add(memo.id);
55003
- const available = memos.filter((m) => !used.has(m.id));
55004
- const anchorIndex = /* @__PURE__ */ new Map();
55005
- for (const memo of available) {
55006
- const anchors = extractTopicAnchors(`${memo.userNeed} ${memo.approach} ${memo.whatFailed} ${memo.whatWorked}`);
55007
- for (const anchor of anchors) {
55008
- const list = anchorIndex.get(anchor) ?? [];
55009
- if (!list.some((m) => m.id === memo.id)) list.push(memo);
55010
- anchorIndex.set(anchor, list);
55011
- }
55012
- }
55013
- const groups = [];
55014
- const assigned = /* @__PURE__ */ new Set();
55015
- const sortedAnchors = [...anchorIndex.entries()].filter(([, list]) => list.length >= 2).toSorted((a, b) => b[1].length - a[1].length);
55016
- for (const [anchor, candidates] of sortedAnchors) {
55017
- const groupMemos = candidates.filter((m) => !assigned.has(m.id));
55018
- if (groupMemos.length >= 2) {
55019
- groups.push({
55020
- memos: groupMemos,
55021
- topic: anchor,
55022
- reason: `发现 ${groupMemos.length} 条围绕 ${anchor} 的记录`
55023
- });
55024
- for (const m of groupMemos) assigned.add(m.id);
55025
- }
55026
- }
55027
- return groups;
55028
- }
55029
- function isOutcomeCompleted(outcome) {
55030
- const lower = outcome.toLowerCase();
55031
- return lower.includes("完成") || lower.includes("done") || lower.includes("completed") || lower.includes("成功") || lower.includes("success");
55032
- }
55033
- function findResolved(memos) {
55034
- return memos.filter((m) => isOutcomeCompleted(m.outcome) && Date.now() - m.recordedAt > 10080 * 60 * 1e3);
55035
- }
55036
- function findStale(memos, staleDays) {
55037
- const threshold = Date.now() - staleDays * 24 * 60 * 60 * 1e3;
55038
- return memos.filter((m) => m.recordedAt < threshold && !isOutcomeCompleted(m.outcome) && !m.outcome.includes("blocked"));
55039
- }
55040
- //#endregion
55041
- //#region ../../packages/memory/src/dream.ts
55042
- const LOCK_FILE = "dream-lock.json";
55043
- const MIN_HOURS_BETWEEN_DREAMS = 24;
55044
- const MIN_SESSIONS_BETWEEN_DREAMS = 5;
55045
- /**
55046
- * Tracks dream consolidation state and decides when to suggest running
55047
- * another dream. Persisted to `<screamHomeDir>/dream-lock.json`.
55048
- */
55049
- var DreamTracker = class {
55050
- state;
55051
- lockPath;
55052
- screamHomeDir;
55053
- initialized = false;
55054
- constructor(screamHomeDir) {
55055
- this.screamHomeDir = screamHomeDir;
55056
- this.lockPath = join$1(screamHomeDir, LOCK_FILE);
55057
- this.state = {
55058
- lastDreamAt: (/* @__PURE__ */ new Date()).toISOString(),
55059
- sessionsSinceLastDream: 0
55060
- };
55061
- }
55062
- /** Load persisted state (call once at startup). */
55063
- async init() {
55064
- if (this.initialized) return;
55065
- this.initialized = true;
55066
- try {
55067
- const raw = await readFile(this.lockPath, "utf8");
55068
- const parsed = JSON.parse(raw);
55069
- if (parsed.version === 1 && parsed.state) {
55070
- this.state = parsed.state;
55071
- return;
55072
- }
55073
- } catch {}
55074
- await this.migrateLegacyLockFiles();
55075
- }
55076
- /** Record that a dream completed successfully. */
55077
- async recordDream() {
55078
- this.state = {
55079
- lastDreamAt: (/* @__PURE__ */ new Date()).toISOString(),
55080
- sessionsSinceLastDream: 0
55081
- };
55082
- await this.persist();
55083
- }
55084
- /** Call on each new session to bump the session counter. */
55085
- async recordNewSession() {
55086
- if (!this.initialized) await this.init();
55087
- this.state.sessionsSinceLastDream += 1;
55088
- await this.persist();
55089
- }
55090
- /** Check whether it's time to suggest another dream. */
55091
- shouldSuggest() {
55092
- return (Date.now() - new Date(this.state.lastDreamAt).getTime()) / (1e3 * 60 * 60) >= MIN_HOURS_BETWEEN_DREAMS && this.state.sessionsSinceLastDream >= MIN_SESSIONS_BETWEEN_DREAMS;
55093
- }
55094
- /** Get a human-readable suggestion message when conditions are met. */
55095
- getSuggestionMessage() {
55096
- const hoursSince = (Date.now() - new Date(this.state.lastDreamAt).getTime()) / (1e3 * 60 * 60);
55097
- return `距离上次记忆整理已过去 ${Math.floor(hoursSince / 24)} 天、${this.state.sessionsSinceLastDream} 个会话。建议运行 /dream 来合并重复记忆、清理过期条目、解决矛盾信息。`;
55098
- }
55099
- async persist() {
55100
- const data = {
55101
- version: 1,
55102
- state: this.state
55103
- };
55104
- try {
55105
- await mkdir(dirname$2(this.lockPath), { recursive: true });
55106
- await writeFile(this.lockPath, JSON.stringify(data, null, 2), "utf8");
55107
- } catch {}
55108
- }
55109
- /**
55110
- * One-time migration for legacy dream-lock.json locations:
55111
- * - <screamHomeDir>/.scream-code/dream-lock.json (buggy double directory)
55112
- * - <screamHomeDir>/sessions/<sessionKey>/.scream-code/dream-lock.json (old per-session)
55113
- *
55114
- * Picks the most recent state, writes it to the new global location, and
55115
- * deletes the legacy files/directories.
55116
- */
55117
- async migrateLegacyLockFiles() {
55118
- const candidates = [join$1(this.screamHomeDir, ".scream-code", LOCK_FILE)];
55119
- const sessionsDir = join$1(this.screamHomeDir, "sessions");
55120
- try {
55121
- const entries = await readdir(sessionsDir, { withFileTypes: true });
55122
- for (const entry of entries) if (entry.isDirectory()) candidates.push(join$1(sessionsDir, entry.name, ".scream-code", LOCK_FILE));
55123
- } catch {}
55124
- let bestState;
55125
- for (const candidate of candidates) try {
55126
- await stat(candidate);
55127
- const raw = await readFile(candidate, "utf8");
55128
- const parsed = JSON.parse(raw);
55129
- if (parsed.version === 1 && parsed.state) {
55130
- if (bestState === void 0 || new Date(parsed.state.lastDreamAt).getTime() > new Date(bestState.lastDreamAt).getTime()) bestState = parsed.state;
55131
- }
55132
- } catch {}
55133
- if (bestState !== void 0) {
55134
- this.state = bestState;
55135
- await this.persist();
55136
- }
55137
- for (const candidate of candidates) try {
55138
- await rm(candidate);
55139
- await rmdir(dirname$2(candidate)).catch(() => {});
55140
- } catch {}
55141
- }
55142
- };
55143
- //#endregion
55144
53471
  //#region ../../packages/agent-core/src/tools/builtin/memory/memory-consolidate.ts
55145
53472
  const MemoryMemoSummarySchema = z.object({
55146
53473
  id: z.string(),
@@ -55520,7 +53847,8 @@ const DEFAULT_TOP_K = 5;
55520
53847
  const MAX_TOP_K = 20;
55521
53848
  const KnowledgeLookupInputSchema = z.object({
55522
53849
  query: z.string().min(1).describe("Search query describing concepts, definitions, or background knowledge to look up in the local knowledge base."),
55523
- top_k: z.number().int().min(1).max(MAX_TOP_K).optional().describe(`Maximum number of chunks to return (default ${DEFAULT_TOP_K}, max ${MAX_TOP_K}).`)
53850
+ top_k: z.number().int().min(1).max(MAX_TOP_K).optional().describe(`Maximum number of chunks to return (default ${DEFAULT_TOP_K}, max ${MAX_TOP_K}).`),
53851
+ deep: z.boolean().optional().describe("Omitted/false = fast local ranking (no LLM calls, much faster). true = LLM entity recall + rerank — use when fast results look weak or off-topic.")
55524
53852
  });
55525
53853
  /**
55526
53854
  * Search the local knowledge base (library) for relevant information. Use when
@@ -55530,7 +53858,7 @@ const KnowledgeLookupInputSchema = z.object({
55530
53858
  var KnowledgeLookupTool = class {
55531
53859
  agent;
55532
53860
  name = "KnowledgeLookup";
55533
- description = "Search the local knowledge library — a reference collection of documents the user ingested via /knowledge. Use when the user asks about a concept, definition, or background topic that may be in the library, OR when the user explicitly asks to \"查知识库\" / \"search the knowledge base\". Returns ranked chunks with source document and section heading. Do NOT use for personal task experience (use MemoryLookup) or current code (use Read/Grep). Prefer this over web search when the topic is likely covered by ingested docs — local sources are faster and more relevant.";
53861
+ description = "Search the local knowledge library — a reference collection of documents the user ingested via /knowledge. Use when the user asks about a concept, definition, or background topic that may be in the library, OR when the user explicitly asks to \"查知识库\" / \"search the knowledge base\". Returns ranked chunks with source document and section heading. Do NOT use for personal task experience (use MemoryLookup) or current code (use Read/Grep). Prefer this over web search when the topic is likely covered by ingested docs — local sources are faster and more relevant. The default fast path makes no LLM calls; pass deep=true only when the fast results look weak.";
55534
53862
  parameters = toInputJsonSchema(KnowledgeLookupInputSchema);
55535
53863
  constructor(agent) {
55536
53864
  this.agent = agent;
@@ -55552,19 +53880,29 @@ var KnowledgeLookupTool = class {
55552
53880
  };
55553
53881
  if ((await store.stats()).chunks === 0) return {
55554
53882
  isError: false,
55555
- output: "知识库为空,请先用 /knowledge 摄入文档后再使用 KnowledgeLookup。"
53883
+ output: "Knowledge base is empty — ingest documents via /knowledge first, then use KnowledgeLookup."
55556
53884
  };
53885
+ const engine = store.getEmbeddingEngine();
53886
+ if (engine !== void 0 && !engine.available && this.agent.embeddingCacheDir !== void 0 && hasEmbeddingModelCache$1(this.agent.embeddingCacheDir)) await engine.ensureReady();
55557
53887
  const topK = Math.min(args.top_k ?? DEFAULT_TOP_K, MAX_TOP_K);
55558
53888
  const llm = { generate: async (systemPrompt, userPrompt) => {
55559
53889
  return this.agent.generateText(systemPrompt, userPrompt);
55560
53890
  } };
55561
- const { multiSearchWithTrace } = await import("./src-h6ZExh4A.mjs");
55562
- const { results, trace } = await multiSearchWithTrace(store, llm, query, { topK });
53891
+ const { multiSearchWithTrace } = await import("./src-DIAKpkXO.mjs");
53892
+ const { results, trace } = await multiSearchWithTrace(store, llm, query, {
53893
+ topK,
53894
+ skipLlm: args.deep !== true
53895
+ });
55563
53896
  if (results.length === 0) return {
55564
53897
  isError: false,
55565
53898
  output: `No knowledge base entries found for query "${query}".`
55566
53899
  };
55567
- const lines = [`Found ${results.length} relevant knowledge chunk${results.length === 1 ? "" : "s"} for query "${query}":`, ""];
53900
+ const degraded = store.getEmbeddingEngine()?.available !== true;
53901
+ const lines = [
53902
+ `Found ${results.length} relevant knowledge chunk${results.length === 1 ? "" : "s"} for query "${query}":`,
53903
+ ...degraded ? ["⚠ Vector model not loaded — run /knowledge → download model for semantic search. Ranking below is keyword-based."] : [],
53904
+ ""
53905
+ ];
55568
53906
  for (const [i, result] of results.entries()) {
55569
53907
  const heading = result.heading ?? "(untitled)";
55570
53908
  const eventLine = result.eventTitle !== null ? ` Event: ${result.eventTitle}` : void 0;
@@ -67411,6 +65749,8 @@ const OAuthRefSchema = z.object({
67411
65749
  key: z.string().min(1)
67412
65750
  });
67413
65751
  const StringRecordSchema = z.record(z.string(), z.string());
65752
+ /** HTTP header name (RFC 7230 token) — rejects spaces, CR/LF, and empties. */
65753
+ const HeaderNameSchema = z.string().min(1).max(128).regex(/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/, "must be an HTTP header name");
67414
65754
  const ProviderConfigSchema = z.object({
67415
65755
  type: ProviderTypeSchema,
67416
65756
  apiKey: z.string().optional(),
@@ -67418,7 +65758,14 @@ const ProviderConfigSchema = z.object({
67418
65758
  defaultModel: z.string().optional(),
67419
65759
  oauth: OAuthRefSchema.optional(),
67420
65760
  env: StringRecordSchema.optional(),
67421
- customHeaders: StringRecordSchema.optional()
65761
+ customHeaders: StringRecordSchema.optional(),
65762
+ /**
65763
+ * Optional request header that receives the stable per-conversation session
65764
+ * id at dispatch time. Unset providers never send an extra header (and do
65765
+ * not change the default User-Agent). When set, the product's own
65766
+ * User-Agent is attached alongside unless `customHeaders` overrides it.
65767
+ */
65768
+ sessionHeader: HeaderNameSchema.optional()
67422
65769
  });
67423
65770
  const ThinkingEffortSchema = z.enum([
67424
65771
  "off",
@@ -95369,31 +93716,67 @@ async function runToolCallBatch(step, response) {
95369
93716
  try {
95370
93717
  for (let index = 0; index < calls.length; index += 1) {
95371
93718
  const call = calls[index];
95372
- if (effectiveStep.signal.aborted) {
95373
- await dispatchToolCall(step, call, call.args);
95374
- pendingResults.push(Promise.resolve(makeErrorToolResult(call, call.args, abortedToolOutput(call.toolName, effectiveStep.signal))));
95375
- continue;
95376
- }
95377
- const prepared = await prepareToolCall(effectiveStep, call);
95378
- pendingResults.push(scheduler.add(prepared.task));
95379
- if (prepared.stopBatchAfterThis === true) {
95380
- stopTurn = true;
95381
- for (const skippedCall of calls.slice(index + 1)) {
95382
- const skippedTask = await prepareSkippedToolCall(step, skippedCall);
95383
- pendingResults.push(scheduler.add(skippedTask));
93719
+ try {
93720
+ if (effectiveStep.signal.aborted) {
93721
+ await dispatchToolCall(step, call, call.args);
93722
+ pendingResults.push(Promise.resolve(makeErrorToolResult(call, call.args, abortedToolOutput(call.toolName, effectiveStep.signal))));
93723
+ continue;
95384
93724
  }
95385
- break;
93725
+ const prepared = await prepareToolCall(effectiveStep, call);
93726
+ pendingResults.push(scheduler.add(prepared.task).catch((error) => makeErrorToolResult(call, call.args, errorMessage$5(error))));
93727
+ if (prepared.stopBatchAfterThis === true) {
93728
+ stopTurn = true;
93729
+ for (const skippedCall of calls.slice(index + 1)) try {
93730
+ const skippedTask = await prepareSkippedToolCall(step, skippedCall);
93731
+ pendingResults.push(scheduler.add(skippedTask).catch((error) => makeErrorToolResult(skippedCall, skippedCall.args, errorMessage$5(error))));
93732
+ } catch (error) {
93733
+ step.log?.warn("skipped tool.call dispatch failed, synthesizing error result", {
93734
+ toolCallId: skippedCall.toolCall.id,
93735
+ toolName: skippedCall.toolName,
93736
+ error
93737
+ });
93738
+ pendingResults.push(Promise.resolve(makeErrorToolResult(skippedCall, skippedCall.args, `Tool preparation failed: ${errorMessage$5(error)}`)));
93739
+ }
93740
+ break;
93741
+ }
93742
+ } catch (error) {
93743
+ step.log?.warn("tool call preparation failed, synthesizing error result", {
93744
+ toolCallId: call.toolCall.id,
93745
+ toolName: call.toolName,
93746
+ error
93747
+ });
93748
+ pendingResults.push(Promise.resolve(makeErrorToolResult(call, call.args, `Tool preparation failed: ${errorMessage$5(error)}`)));
95386
93749
  }
95387
93750
  }
95388
93751
  for (const pendingResult of pendingResults) {
95389
- const result = await finalizePendingToolResult(effectiveStep, await pendingResult);
93752
+ let raw;
93753
+ try {
93754
+ raw = await pendingResult;
93755
+ } catch (error) {
93756
+ step.log?.warn("tool task rejected before producing a result", { error });
93757
+ continue;
93758
+ }
93759
+ let result;
93760
+ try {
93761
+ result = await finalizePendingToolResult(effectiveStep, raw);
93762
+ } catch (error) {
93763
+ step.log?.warn("tool result finalize failed, dispatching raw result", { error });
93764
+ result = raw;
93765
+ }
95390
93766
  if (result.stopTurn === true) stopTurn = true;
95391
- await step.dispatchEvent({
95392
- type: "tool.result",
95393
- parentUuid: result.toolCall.id,
95394
- toolCallId: result.toolCall.id,
95395
- result: result.result
95396
- });
93767
+ try {
93768
+ await step.dispatchEvent({
93769
+ type: "tool.result",
93770
+ parentUuid: result.toolCall.id,
93771
+ toolCallId: result.toolCall.id,
93772
+ result: result.result
93773
+ });
93774
+ } catch (error) {
93775
+ step.log?.warn("tool.result dispatch failed", {
93776
+ toolCallId: result.toolCall.id,
93777
+ error
93778
+ });
93779
+ }
95397
93780
  }
95398
93781
  } finally {
95399
93782
  if (steerPoll !== void 0) clearInterval(steerPoll);
@@ -102110,6 +100493,7 @@ var TurnFlow = class {
102110
100493
  currentStepByTurn = /* @__PURE__ */ new Map();
102111
100494
  currentStep = 0;
102112
100495
  todoSeenThisTurn = false;
100496
+ todoReconcileInjected = false;
102113
100497
  convergenceInjections = 0;
102114
100498
  currentStepHadContent = false;
102115
100499
  lastToolFailure = null;
@@ -102126,8 +100510,25 @@ var TurnFlow = class {
102126
100510
  input,
102127
100511
  origin
102128
100512
  });
100513
+ if (this.activeTurn === "resuming") {
100514
+ this.emitAgentBusy();
100515
+ return null;
100516
+ }
100517
+ if (this.activeTurn) {
100518
+ this.steerBuffer.push({
100519
+ input,
100520
+ origin
100521
+ });
100522
+ return null;
100523
+ }
102129
100524
  return this.launch(input, origin);
102130
100525
  }
100526
+ emitAgentBusy() {
100527
+ this.agent.emitEvent({
100528
+ type: "error",
100529
+ ...makeErrorPayload("turn.agent_busy", `Cannot launch a new turn while another turn (ID ${this.turnId}) is active`, { details: { turnId: this.turnId } })
100530
+ });
100531
+ }
102131
100532
  steer(input, origin = USER_PROMPT_ORIGIN, opts) {
102132
100533
  this.agent.records.logRecord({
102133
100534
  type: "turn.steer",
@@ -102146,10 +100547,7 @@ var TurnFlow = class {
102146
100547
  }
102147
100548
  launch(input, origin) {
102148
100549
  if (this.activeTurn) {
102149
- this.agent.emitEvent({
102150
- type: "error",
102151
- ...makeErrorPayload("turn.agent_busy", `Cannot launch a new turn while another turn (ID ${this.turnId}) is active`, { details: { turnId: this.turnId } })
102152
- });
100550
+ this.emitAgentBusy();
102153
100551
  return null;
102154
100552
  }
102155
100553
  if (this.turnId === -1) this.agent.dreamTracker.init().then(() => this.agent.dreamTracker.recordNewSession()).catch(() => {});
@@ -102338,6 +100736,7 @@ var TurnFlow = class {
102338
100736
  */
102339
100737
  async runOneTurn(turnId, input, origin, signal, standalone) {
102340
100738
  this.todoSeenThisTurn = false;
100739
+ this.todoReconcileInjected = false;
102341
100740
  this.convergenceInjections = 0;
102342
100741
  this.currentStepHadContent = false;
102343
100742
  this.lastToolFailure = null;
@@ -102357,6 +100756,7 @@ var TurnFlow = class {
102357
100756
  turnId,
102358
100757
  origin
102359
100758
  });
100759
+ if (this.steerBuffer.length > 0) this.flushSteerBuffer();
102360
100760
  this.agent.context.appendUserMessage(input, origin);
102361
100761
  let ended;
102362
100762
  let completedStopReason;
@@ -102578,24 +100978,36 @@ var TurnFlow = class {
102578
100978
  });
102579
100979
  return { continue: true };
102580
100980
  }
102581
- if (stopHookContinuationUsed) return { continue: false };
102582
- const stopBlock = await this.agent.hooks?.triggerBlock("Stop", {
102583
- signal,
102584
- inputData: { stopHookActive: stopHookContinuationUsed }
102585
- });
102586
- signal.throwIfAborted();
102587
- if (stopBlock !== void 0) {
102588
- stopHookContinuationUsed = true;
102589
- this.agent.context.appendUserMessage([{
102590
- type: "text",
102591
- text: stopBlock.reason
102592
- }], {
102593
- kind: "system_trigger",
102594
- name: "stop_hook"
100981
+ if (!stopHookContinuationUsed) {
100982
+ const stopBlock = await this.agent.hooks?.triggerBlock("Stop", {
100983
+ signal,
100984
+ inputData: { stopHookActive: stopHookContinuationUsed }
102595
100985
  });
102596
- return { continue: true };
100986
+ signal.throwIfAborted();
100987
+ if (stopBlock !== void 0) {
100988
+ stopHookContinuationUsed = true;
100989
+ this.agent.context.appendUserMessage([{
100990
+ type: "text",
100991
+ text: stopBlock.reason
100992
+ }], {
100993
+ kind: "system_trigger",
100994
+ name: "stop_hook"
100995
+ });
100996
+ return { continue: true };
100997
+ }
102597
100998
  }
102598
100999
  if (this.flushSteerBuffer()) return { continue: true };
101000
+ if (!this.todoReconcileInjected && stopReason !== "max_tokens" && this.agent.background.list().length === 0) {
101001
+ const todos = this.agent.tools.getTodos();
101002
+ if (todos.some((todo) => todo.status !== "done") && todos.length > 0) {
101003
+ this.todoReconcileInjected = true;
101004
+ this.agent.context.appendSystemReminder("Before ending the turn, reconcile the TodoList with reality: mark only work that is actually finished as done, leave unfinished items pending or in_progress, and keep blockers accurate. Do not mark incomplete work as done. Prefer updating TodoList only on this continuation.", {
101005
+ kind: "system_trigger",
101006
+ name: "todo_reconcile"
101007
+ });
101008
+ return { continue: true };
101009
+ }
101010
+ }
102599
101011
  return { continue: false };
102600
101012
  },
102601
101013
  prepareToolExecution: async (ctx) => {
@@ -103141,6 +101553,8 @@ var Agent = class {
103141
101553
  screamConfig;
103142
101554
  homedir;
103143
101555
  screamHomeDir;
101556
+ /** Cache dir of the shared embedding engine (undefined when no scream home). */
101557
+ embeddingCacheDir;
103144
101558
  rpc;
103145
101559
  toolServices;
103146
101560
  pluginSessionStarts;
@@ -103205,6 +101619,7 @@ var Agent = class {
103205
101619
  this.lspSupervisor = options.lspSupervisor;
103206
101620
  this.eventBus = new EventSubscriptionBus();
103207
101621
  const embedCacheDir = options.screamHomeDir !== void 0 ? join$1(options.screamHomeDir, "cache", "fastembed") : void 0;
101622
+ this.embeddingCacheDir = embedCacheDir;
103208
101623
  this.sharedEmbeddingEngine = createFastEmbedEngine(embedCacheDir);
103209
101624
  this.log = options.log ?? log;
103210
101625
  this.blobStore = options.homedir ? new BlobStore({ blobsDir: join$1(options.homedir, "blobs") }) : void 0;
@@ -103232,8 +101647,10 @@ var Agent = class {
103232
101647
  const screamHomeDir = options.screamHomeDir;
103233
101648
  this.memoStore = screamHomeDir ? new MemoryMemoStore(screamHomeDir, this.log) : void 0;
103234
101649
  this.memoStoreReady = this.initMemoStore(screamHomeDir);
103235
- this.knowledgeStore = screamHomeDir !== void 0 && this.type === "main" ? new KnowledgeStore(screamHomeDir) : void 0;
103236
- this.knowledgeStoreReady = this.initKnowledgeStore(this.knowledgeStore);
101650
+ this.knowledgeStore = screamHomeDir !== void 0 && this.type === "main" ? sharedKnowledgeStore(screamHomeDir) : void 0;
101651
+ this.knowledgeStoreReady = this.knowledgeStore !== void 0 && screamHomeDir !== void 0 ? sharedKnowledgeStoreReady(screamHomeDir).catch((error) => {
101652
+ this.log.error("knowledge store init failed", error);
101653
+ }) : Promise.resolve();
103237
101654
  this.sessionMemory = new SessionMemory(this);
103238
101655
  this.workingSet = new WorkingSet();
103239
101656
  this.dreamTracker = new DreamTracker(screamHomeDir ?? "");
@@ -103399,21 +101816,6 @@ var Agent = class {
103399
101816
  if (withoutPython.length !== current.length) this.tools.setActiveTools(withoutPython);
103400
101817
  }
103401
101818
  }
103402
- initKnowledgeStore(store) {
103403
- if (store === void 0) return Promise.resolve();
103404
- return (async () => {
103405
- try {
103406
- await store.init();
103407
- } catch (error) {
103408
- this.log.error("knowledge store init failed", error);
103409
- }
103410
- try {
103411
- store.setEmbeddingEngine(this.sharedEmbeddingEngine);
103412
- } catch (error) {
103413
- this.log.warn("knowledge embedding engine init failed", error);
103414
- }
103415
- })();
103416
- }
103417
101819
  get generate() {
103418
101820
  return async (provider, systemPrompt, tools, history, callbacks, options) => {
103419
101821
  if (options?.auth !== void 0) {
@@ -124267,6 +122669,21 @@ function resolveModelCapabilities(alias, provider) {
124267
122669
  max_context_tokens: alias.maxContextSize
124268
122670
  };
124269
122671
  }
122672
+ function sessionHeaderFields(provider, sessionId, screamRequestHeaders) {
122673
+ const name = provider.sessionHeader;
122674
+ if (name === void 0 || sessionId === void 0 || sessionId.length === 0) return {};
122675
+ const out = { [name]: sessionId };
122676
+ const userAgent = screamRequestHeaders?.["User-Agent"];
122677
+ if (userAgent !== void 0) out["User-Agent"] = userAgent;
122678
+ return out;
122679
+ }
122680
+ function resolvedHeaderFields(provider, sessionId, screamRequestHeaders) {
122681
+ const headers = {
122682
+ ...sessionHeaderFields(provider, sessionId, screamRequestHeaders),
122683
+ ...provider.customHeaders
122684
+ };
122685
+ return Object.keys(headers).length > 0 ? headers : void 0;
122686
+ }
124270
122687
  function toLtodProviderConfig(provider, model, screamRequestHeaders, maxOutputSize, reasoningKey, promptCacheKey, adaptiveThinking, forceThinking) {
124271
122688
  switch (provider.type) {
124272
122689
  case "anthropic": return {
@@ -124276,7 +122693,7 @@ function toLtodProviderConfig(provider, model, screamRequestHeaders, maxOutputSi
124276
122693
  apiKey: providerApiKey(provider),
124277
122694
  ...maxOutputSize !== void 0 ? { defaultMaxTokens: maxOutputSize } : {},
124278
122695
  ...adaptiveThinking !== void 0 ? { adaptiveThinking } : {},
124279
- ...defaultHeadersField(provider.customHeaders)
122696
+ ...defaultHeadersField(resolvedHeaderFields(provider, promptCacheKey, screamRequestHeaders))
124280
122697
  };
124281
122698
  case "openai": return {
124282
122699
  type: "openai",
@@ -124285,7 +122702,7 @@ function toLtodProviderConfig(provider, model, screamRequestHeaders, maxOutputSi
124285
122702
  apiKey: providerApiKey(provider),
124286
122703
  reasoningKey,
124287
122704
  ...forceThinking !== void 0 ? { forceThinking } : {},
124288
- ...defaultHeadersField(provider.customHeaders)
122705
+ ...defaultHeadersField(resolvedHeaderFields(provider, promptCacheKey, screamRequestHeaders))
124289
122706
  };
124290
122707
  case "scream": return {
124291
122708
  type: "scream",
@@ -124310,7 +122727,7 @@ function toLtodProviderConfig(provider, model, screamRequestHeaders, maxOutputSi
124310
122727
  model,
124311
122728
  baseUrl: providerValue(provider.baseUrl, provider.env, "OPENAI_BASE_URL"),
124312
122729
  apiKey: providerApiKey(provider),
124313
- ...defaultHeadersField(provider.customHeaders)
122730
+ ...defaultHeadersField(resolvedHeaderFields(provider, promptCacheKey, screamRequestHeaders))
124314
122731
  };
124315
122732
  case "vertexai": {
124316
122733
  const useServiceAccount = hasVertexAIServiceEnv(provider);
@@ -128527,6 +126944,28 @@ function catalogModelToAlias(providerId, model) {
128527
126944
  };
128528
126945
  }
128529
126946
  /**
126947
+ * Catalog stamp: models.dev entries whose gateway requires a stable
126948
+ * per-conversation session header. Applied only while materializing the
126949
+ * provider into config — never during request dispatch (so the hot path
126950
+ * stays free of hostnames). Keys are catalog provider ids and exact
126951
+ * models.dev `api` base URLs.
126952
+ */
126953
+ const CATALOG_SESSION_HEADER_BY_ID = {
126954
+ opencode: "x-opencode-session",
126955
+ "opencode-go": "x-opencode-session"
126956
+ };
126957
+ const CATALOG_SESSION_HEADER_BY_BASE_URL = {
126958
+ "https://opencode.ai/zen/v1": "x-opencode-session",
126959
+ "https://opencode.ai/zen/go/v1": "x-opencode-session"
126960
+ };
126961
+ /** Resolves the session-header name to stamp for a catalog/DIY apply, if any. */
126962
+ function resolveCatalogSessionHeader(providerId, baseUrl) {
126963
+ const byId = CATALOG_SESSION_HEADER_BY_ID[providerId];
126964
+ if (byId !== void 0) return byId;
126965
+ if (baseUrl === void 0) return void 0;
126966
+ return CATALOG_SESSION_HEADER_BY_BASE_URL[baseUrl.replace(/\/+$/, "")];
126967
+ }
126968
+ /**
128530
126969
  * Parses an optional pruned models.dev catalog string — typically the
128531
126970
  * `__SCREAM_CODE_BUILT_IN_CATALOG__` constant injected by tsdown at build
128532
126971
  * time. Returns `undefined` when the argument is missing or invalid.
@@ -128552,10 +126991,12 @@ function loadBuiltInCatalog(text) {
128552
126991
  * after the merge.
128553
126992
  */
128554
126993
  function applyCatalogProvider(config, options) {
126994
+ const sessionHeader = options.sessionHeader ?? resolveCatalogSessionHeader(options.providerId, options.baseUrl);
128555
126995
  config.providers[options.providerId] = {
128556
126996
  type: options.wire,
128557
126997
  baseUrl: options.baseUrl,
128558
- apiKey: options.apiKey
126998
+ apiKey: options.apiKey,
126999
+ ...sessionHeader !== void 0 ? { sessionHeader } : {}
128559
127000
  };
128560
127001
  const models = config.models ?? {};
128561
127002
  for (const [key, alias] of Object.entries(models)) if (alias.provider === options.providerId) delete models[key];
@@ -128913,14 +127354,13 @@ const PULSE_WAVE_FRAMES = [...forwardFrames, ...backwardFrames];
128913
127354
  //#region src/tui/commands/knowledge-store.ts
128914
127355
  let knowledgeStorePromise;
128915
127356
  let embeddingEngineInstance;
128916
- let embeddingStatus = "idle";
127357
+ /** Whether the last manual download attempt failed (drives the 'failed' status). */
127358
+ let lastManualFailed = false;
128917
127359
  function getEmbeddingCacheDir() {
128918
127360
  const dir = join(getDataDir(), "cache", "fastembed");
128919
127361
  mkdirSync(dir, { recursive: true });
128920
127362
  return dir;
128921
127363
  }
128922
- /** Subdirectory fastembed uses for the BGESmallZH model inside the cache dir. */
128923
- const EMBEDDING_MODEL_DIR = "fast-bge-small-zh-v1.5";
128924
127364
  /**
128925
127365
  * Any usable cache: the model directory with its weights present.
128926
127366
  *
@@ -128928,57 +127368,58 @@ const EMBEDDING_MODEL_DIR = "fast-bge-small-zh-v1.5";
128928
127368
  * the Hub while loading — so callers must not treat it as "no model here".
128929
127369
  * Flagging it as missing would block the very path that repairs it, and
128930
127370
  * fastembed itself skips downloading whenever the model directory exists.
127371
+ * The existence check itself lives in @scream-code/memory next to the cache
127372
+ * layout it inspects.
128931
127373
  */
128932
127374
  function hasEmbeddingModelCache() {
128933
- const modelDir = join(getEmbeddingCacheDir(), EMBEDDING_MODEL_DIR);
128934
- return existsSync(modelDir) && existsSync(join(modelDir, "model_optimized.onnx"));
127375
+ return hasEmbeddingModelCache$1(getEmbeddingCacheDir());
128935
127376
  }
128936
127377
  /**
128937
- * The store and its embedding engine are built together and published only once
128938
- * both exist. Publishing the store first meant a single transient failure (a
128939
- * locked knowledge.db, a full disk, a denied directory) left a store with no
128940
- * engine that no later call could repair: every download then failed instantly
128941
- * with "embedding engine not initialized" for the rest of the process.
128942
- *
128943
- * Concurrent callers share one build, and a failed build clears the slot so the
128944
- * next caller retries from scratch.
127378
+ * Get the singleton KnowledgeStore, initializing it on first access.
127379
+ * Delegates to the shared per-homeDir provider in @scream-code/knowledge so
127380
+ * this process keeps ONE store handle and ONE engine instance no matter how
127381
+ * many hosts (TUI command, agent session) ask for it. Concurrent callers share
127382
+ * one build, and a failed build clears the slot so the next caller retries.
128945
127383
  */
128946
127384
  function getKnowledgeStore() {
128947
- knowledgeStorePromise ??= (async () => {
128948
- const store = new KnowledgeStore(getDataDir());
128949
- await store.init();
128950
- const engine = createFastEmbedEngine(getEmbeddingCacheDir());
128951
- store.setEmbeddingEngine(engine);
128952
- embeddingEngineInstance = engine;
127385
+ knowledgeStorePromise ??= sharedKnowledgeStoreReady(getDataDir()).then(() => {
127386
+ const store = sharedKnowledgeStore(getDataDir());
127387
+ embeddingEngineInstance = store.getEmbeddingEngine();
128953
127388
  return store;
128954
- })().catch((error) => {
127389
+ }).catch((error) => {
128955
127390
  knowledgeStorePromise = void 0;
128956
127391
  throw error;
128957
127392
  });
128958
127393
  return knowledgeStorePromise;
128959
127394
  }
127395
+ /**
127396
+ * Derived from live facts instead of a mirrored flag: a download in flight,
127397
+ * the engine's own availability, and whether the last manual attempt failed.
127398
+ * Engine construction failures are reported separately via
127399
+ * getEmbeddingFailureKind and are intentionally not collapsed into 'failed'
127400
+ * — a missing download is not the only reason the model might not be ready.
127401
+ */
128960
127402
  function getEmbeddingStatus() {
128961
- return embeddingStatus;
127403
+ if (downloadPromise !== void 0) return "downloading";
127404
+ if (embeddingEngineInstance?.available === true) return "ready";
127405
+ if (lastManualFailed) return "failed";
127406
+ return "idle";
128962
127407
  }
128963
127408
  /**
128964
127409
  * Manually trigger the embedding model download/load.
128965
- * Only mutates embeddingStatus; the actual download is delegated to
128966
- * EmbeddingEngine.ensureReady() and saves the model to the shared cache dir.
127410
+ * Only mutates the inputs behind the derived status; the actual download is
127411
+ * delegated to EmbeddingEngine.ensureReady() and saves the model to the shared cache dir.
128967
127412
  * Concurrent calls join the in-flight download and share its result instead
128968
127413
  * of failing — the startup warm-up and a user-initiated download must never
128969
127414
  * race into a spurious "download already in progress" error.
128970
127415
  */
128971
127416
  async function startManualEmbeddingDownload() {
128972
127417
  if (embeddingEngineInstance === void 0) return failureResult("embedding engine not initialized");
128973
- if (embeddingEngineInstance.available) {
128974
- embeddingStatus = "ready";
128975
- return {
128976
- ok: true,
128977
- alreadyReady: true
128978
- };
128979
- }
127418
+ if (embeddingEngineInstance.available) return {
127419
+ ok: true,
127420
+ alreadyReady: true
127421
+ };
128980
127422
  if (downloadPromise !== void 0) return downloadPromise;
128981
- embeddingStatus = "downloading";
128982
127423
  downloadPromise = performDownload().finally(() => {
128983
127424
  downloadPromise = void 0;
128984
127425
  });
@@ -128996,7 +127437,7 @@ async function performDownload() {
128996
127437
  try {
128997
127438
  const support = await probeLocalEmbeddingSupport();
128998
127439
  if (!support.supported) {
128999
- embeddingStatus = "failed";
127440
+ lastManualFailed = true;
129000
127441
  return {
129001
127442
  ok: false,
129002
127443
  error: support.error,
@@ -129012,10 +127453,10 @@ async function performDownload() {
129012
127453
  error = ok ? void 0 : embeddingEngineInstance.lastError;
129013
127454
  }
129014
127455
  }
129015
- embeddingStatus = ok ? "ready" : "failed";
127456
+ lastManualFailed = !ok;
129016
127457
  return ok ? { ok } : failureResult(error);
129017
127458
  } catch (error) {
129018
- embeddingStatus = "failed";
127459
+ lastManualFailed = true;
129019
127460
  return failureResult(error instanceof Error ? error.message : String(error));
129020
127461
  }
129021
127462
  }
@@ -129698,6 +128139,18 @@ function formatErrorMessage(error) {
129698
128139
  return error instanceof Error ? error.message : String(error);
129699
128140
  }
129700
128141
  /**
128142
+ * When a gateway rejects a request for a missing session header, the error
128143
+ * text names the header. Echo an actionable config line so the user does not
128144
+ * have to look up the field name. Returns the input unchanged when the
128145
+ * message does not match the generic "missing … header" shape.
128146
+ */
128147
+ function appendSessionHeaderHint(message, hintText) {
128148
+ const match = /missing\s+(?:(x-[A-Za-z0-9-]+)|([A-Za-z0-9-]*session[A-Za-z0-9-]*)\s+header\b)/i.exec(message);
128149
+ const name = match?.[1] ?? match?.[2];
128150
+ if (name === void 0) return message;
128151
+ return `${message}\n${hintText(name)}`;
128152
+ }
128153
+ /**
129701
128154
  * Cap an error message body for inline transcript display. Drops blank lines,
129702
128155
  * keeps the first `maxLines` non-blank lines, and appends a `… (N more lines)`
129703
128156
  * hint when truncated. Full text is preserved in the persisted session — this
@@ -133462,18 +131915,23 @@ function createMarkdownTheme(colors) {
133462
131915
  codeBlockBorder: () => null,
133463
131916
  codeBlockIndent: " ",
133464
131917
  codeBlockLine: (line, { index, lang, width }) => {
133465
- const content = visibleWidth(line) > width - 1 ? truncateToWidth(line, Math.max(1, width - 1), "…") : line;
133466
- const contentCells = visibleWidth(content);
131918
+ const gutter = 1;
133467
131919
  const rawLabel = index === 0 && lang !== void 0 && lang.length > 0 ? ` ${lang}` : "";
133468
131920
  const labelCells = visibleWidth(rawLabel);
133469
- const showLabel = labelCells > 0 && 1 + contentCells + labelCells + 1 <= width;
133470
- const filler = showLabel ? Math.max(0, width - contentCells - labelCells - 2) : 0;
133471
- const padCells = showLabel ? 1 : Math.max(0, width - contentCells - 1);
131921
+ const labelFits = labelCells > 0 && 2 + labelCells + 1 <= width;
131922
+ const activeLabelCells = labelFits ? labelCells : 0;
131923
+ const rows = wrapTextWithAnsi(line, Math.max(1, width - gutter - (activeLabelCells > 0 ? activeLabelCells + 1 : 0)));
133472
131924
  const panelled = isCodeBlockPanelEnabled();
133473
131925
  const paint = panelled ? chalk.bgHex(colors.mdCodeBlockBg) : (text) => text;
133474
131926
  const labelPaint = panelled ? chalk.bgHex(colors.mdCodeBlockBg).hex(colors.mdCodeBlock) : chalk.hex(colors.mdCodeBlock);
133475
- const label = showLabel ? chalk.italic(labelPaint(rawLabel)) : "";
133476
- return `${paint(" ")}${paint(content)}${paint(" ".repeat(filler))}${label}${paint(" ".repeat(padCells))}`;
131927
+ return rows.map((row, rowIndex) => {
131928
+ const contentCells = visibleWidth(row);
131929
+ const showLabel = rowIndex === 0 && labelFits;
131930
+ const filler = showLabel ? Math.max(0, width - contentCells - activeLabelCells - 2) : 0;
131931
+ const padCells = showLabel ? 1 : Math.max(0, width - contentCells - 1);
131932
+ const label = showLabel ? chalk.italic(labelPaint(rawLabel)) : "";
131933
+ return `${paint(" ")}${paint(row)}${paint(" ".repeat(filler))}${label}${paint(" ".repeat(padCells))}`;
131934
+ }).join("\n");
133477
131935
  },
133478
131936
  quote: (text) => chalk.hex(colors.mdQuote)(text),
133479
131937
  quoteBorder: (text) => chalk.hex(colors.mdQuote)(text),
@@ -135469,7 +133927,7 @@ async function guidedGoalSetup(host) {
135469
133927
  host.showNotice(t("goal.storm_breaker"), t("goal.conflict_loop"));
135470
133928
  return;
135471
133929
  }
135472
- const { TextInputDialogComponent } = await import("./text-input-dialog-B-6Pgo6m.mjs");
133930
+ const { TextInputDialogComponent } = await import("./text-input-dialog-DDUarM0_.mjs");
135473
133931
  const initialDesc = await promptText(host, TextInputDialogComponent, {
135474
133932
  title: t("goal.setup_title_initial"),
135475
133933
  subtitle: t("goal.setup_desc_hint"),
@@ -135490,7 +133948,7 @@ async function guidedGoalSetup(host) {
135490
133948
  await showGoalConfigWizard(host, session, confirmed.trim() || objective, false);
135491
133949
  }
135492
133950
  async function showGoalConfigWizard(host, session, objective, replace) {
135493
- const { TextInputDialogComponent } = await import("./text-input-dialog-B-6Pgo6m.mjs");
133951
+ const { TextInputDialogComponent } = await import("./text-input-dialog-DDUarM0_.mjs");
135494
133952
  const turnInput = await promptNumber(host, TextInputDialogComponent, {
135495
133953
  title: t("goal.wizard_title", { objective }),
135496
133954
  subtitle: t("goal.budget_turns_hint"),
@@ -137111,7 +135569,7 @@ var ActivityGroupComponent = class extends Container {
137111
135569
  shown.forEach((segment, index) => {
137112
135570
  const isLast = index === shown.length - 1;
137113
135571
  if (segment.kind === "tool") {
137114
- const row = this.toolRow(segment.tc, width, 3);
135572
+ const row = this.toolRow(segment.tc, width, 3, !this.running);
137115
135573
  this.bodyContainer.addChild(new Text(`${isLast ? BRANCH_LAST : BRANCH_FIRST}${this.statusGlyph(row)} ${row.header}`, 0, 0));
137116
135574
  return;
137117
135575
  }
@@ -137155,7 +135613,7 @@ var ActivityGroupComponent = class extends Container {
137155
135613
  /** Renders one step of the timeline: its branch row plus its own body rows. */
137156
135614
  segmentRows(segment, width, isLast) {
137157
135615
  if (segment.kind === "tool") {
137158
- const row = this.toolRow(segment.tc, width, getActivityLines().expandedTool);
135616
+ const row = this.toolRow(segment.tc, width, getActivityLines().expandedTool, !this.running);
137159
135617
  const components = [new Text(`${isLast ? BRANCH_LAST : BRANCH_FIRST}${this.statusGlyph(row)} ${row.header}`, 0, 0)];
137160
135618
  const continuation = isLast ? THINKING_BODY_PREFIX : BRANCH_PIPE;
137161
135619
  for (const line of row.body) components.push(new Text(`${continuation}${line}`, 0, 0));
@@ -137245,7 +135703,7 @@ var ActivityGroupComponent = class extends Container {
137245
135703
  * result preview. The card's own status marker is captured (not just dropped)
137246
135704
  * so the row can mirror running / failed / aborted states.
137247
135705
  */
137248
- toolRow(tc, width, maxLines) {
135706
+ toolRow(tc, width, maxLines, settled = false) {
137249
135707
  const inner = Math.max(1, width - BRANCH_WIDTH - STATUS_WIDTH);
137250
135708
  const rendered = tc.render(inner);
137251
135709
  const rawHeader = rendered[1] ?? "";
@@ -137260,7 +135718,7 @@ var ActivityGroupComponent = class extends Container {
137260
135718
  return {
137261
135719
  header: condenseRowLabel(stripCardMarker(rawHeader), tc.toolCallView.name, tc.toolCallView.args, Math.max(1, width - BRANCH_WIDTH - STATUS_WIDTH)),
137262
135720
  body,
137263
- done: result !== void 0,
135721
+ done: result !== void 0 || settled,
137264
135722
  failed: result?.is_error === true || cardMarkerChar(rawHeader) === "✗",
137265
135723
  aborted: cardMarkerChar(rawHeader) === "⊙"
137266
135724
  };
@@ -144352,33 +142810,14 @@ function openUrl(url) {
144352
142810
  execFile(command[0], command[1], () => {});
144353
142811
  }
144354
142812
  //#endregion
144355
- //#region src/tui/commands/knowledge-web.ts
142813
+ //#region src/tui/commands/knowledge-web-template.ts
144356
142814
  /**
144357
- * /knowledge web — 知识图谱可视化
144358
- * 纯 Canvas 2D 自渲染(无任何外部依赖/CDN),打开即用。
144359
- * 白底简约科技风:灰色小点星云 + 细黑线条 + 放大显示文字。
142815
+ * /knowledge web — HTML shell for the knowledge-graph canvas page.
142816
+ * Pure string, no template interpolation: serveHTML injects the i18n dict
142817
+ * and locale via __DICT_INJECT__ / __LOCALE_INJECT__ replacements.
142818
+ * Split out of knowledge-web.ts so server code and page markup live apart.
144360
142819
  */
144361
- const activeServers = /* @__PURE__ */ new Set();
144362
- const activeTimers = /* @__PURE__ */ new Set();
144363
- /** Idle watchdog: closes all graph servers when no viewer kept them alive. */
144364
- let idleTimer = null;
144365
- function registerServer(server) {
144366
- activeServers.add(server);
144367
- server.on("close", () => {
144368
- activeServers.delete(server);
144369
- });
144370
- }
144371
- function closeAllServers() {
144372
- if (idleTimer !== null) {
144373
- clearTimeout(idleTimer);
144374
- idleTimer = null;
144375
- }
144376
- for (const timer of activeTimers) clearInterval(timer);
144377
- activeTimers.clear();
144378
- for (const server of activeServers) server.close();
144379
- }
144380
- process.on("exit", closeAllServers);
144381
- const HTML = `<!doctype html>
142820
+ const KNOWLEDGE_WEB_HTML = `<!doctype html>
144382
142821
  <html lang="zh-CN">
144383
142822
  <head>
144384
142823
  <meta charset="utf-8">
@@ -145471,6 +143910,33 @@ document.getElementById('btn-lang').addEventListener('click',toggleLang);
145471
143910
  <\/script>
145472
143911
  </body>
145473
143912
  </html>`;
143913
+ //#endregion
143914
+ //#region src/tui/commands/knowledge-web.ts
143915
+ /**
143916
+ * /knowledge web — 知识图谱可视化
143917
+ * 纯 Canvas 2D 自渲染(无任何外部依赖/CDN),打开即用。
143918
+ * 白底简约科技风:灰色小点星云 + 细黑线条 + 放大显示文字。
143919
+ */
143920
+ const activeServers = /* @__PURE__ */ new Set();
143921
+ const activeTimers = /* @__PURE__ */ new Set();
143922
+ /** Idle watchdog: closes all graph servers when no viewer kept them alive. */
143923
+ let idleTimer = null;
143924
+ function registerServer(server) {
143925
+ activeServers.add(server);
143926
+ server.on("close", () => {
143927
+ activeServers.delete(server);
143928
+ });
143929
+ }
143930
+ function closeAllServers() {
143931
+ if (idleTimer !== null) {
143932
+ clearTimeout(idleTimer);
143933
+ idleTimer = null;
143934
+ }
143935
+ for (const timer of activeTimers) clearInterval(timer);
143936
+ activeTimers.clear();
143937
+ for (const server of activeServers) server.close();
143938
+ }
143939
+ process.on("exit", closeAllServers);
145474
143940
  async function serveGraphJSON(store, res) {
145475
143941
  try {
145476
143942
  const [entities, events, edges, sources] = await Promise.all([
@@ -145569,7 +144035,7 @@ function serveHTML(res, locale) {
145569
144035
  kw_lang_toggle: "中文"
145570
144036
  }
145571
144037
  });
145572
- const injected = HTML.replace("__DICT_INJECT__", dictJson).replace("__LOCALE_INJECT__", locale);
144038
+ const injected = KNOWLEDGE_WEB_HTML.replace("__DICT_INJECT__", dictJson).replace("__LOCALE_INJECT__", locale);
145573
144039
  res.end(injected);
145574
144040
  }
145575
144041
  async function handleWeb(host) {
@@ -146182,7 +144648,7 @@ async function handleList(host) {
146182
144648
  host.mountEditorReplacement(tree);
146183
144649
  await promise;
146184
144650
  }
146185
- async function handleSearch(host) {
144651
+ async function handleSearch(host, opts) {
146186
144652
  const query = await promptTextInput(host, t("knowledge.search"), {
146187
144653
  subtitle: t("knowledge.search_desc"),
146188
144654
  placeholder: t("knowledge.search_placeholder"),
@@ -146192,9 +144658,13 @@ async function handleSearch(host) {
146192
144658
  const store = await getKnowledgeStore();
146193
144659
  const llm = makeLlmCaller(host);
146194
144660
  const spinner = host.showProgressSpinner(t("knowledge.searching"));
144661
+ const startedAt = Date.now();
146195
144662
  let results;
146196
144663
  try {
146197
- results = await multiSearch(store, llm, query, { topK: 5 });
144664
+ results = await multiSearch(store, llm, query, {
144665
+ topK: 5,
144666
+ skipLlm: opts?.skipLlm ?? true
144667
+ });
146198
144668
  } catch (error) {
146199
144669
  spinner.stop({
146200
144670
  ok: false,
@@ -146204,7 +144674,7 @@ async function handleSearch(host) {
146204
144674
  }
146205
144675
  spinner.stop({
146206
144676
  ok: true,
146207
- label: t("knowledge.search_done")
144677
+ label: `${t("knowledge.search_done")} · ${Date.now() - startedAt}ms`
146208
144678
  });
146209
144679
  const engine = store.getEmbeddingEngine();
146210
144680
  const degraded = engine === void 0 || !engine.available;
@@ -146466,6 +144936,11 @@ async function handleKnowledgeCommand(host, _args) {
146466
144936
  label: "🔍 " + t("knowledge.search"),
146467
144937
  description: t("knowledge.search_effect")
146468
144938
  },
144939
+ {
144940
+ value: "search-deep",
144941
+ label: "🧠 " + t("knowledge.search_deep"),
144942
+ description: t("knowledge.search_deep_desc")
144943
+ },
146469
144944
  {
146470
144945
  value: "delete",
146471
144946
  label: "🗑️ " + t("knowledge.delete"),
@@ -146501,6 +144976,7 @@ async function handleKnowledgeCommand(host, _args) {
146501
144976
  else if (value === "ingest") await handleIngest(host);
146502
144977
  else if (value === "list") await handleList(host);
146503
144978
  else if (value === "search") await handleSearch(host);
144979
+ else if (value === "search-deep") await handleSearch(host, { skipLlm: false });
146504
144980
  else if (value === "delete") await handleDelete(host);
146505
144981
  else if (value === "reembed") await handleReembed(host);
146506
144982
  else if (value === "stats") await handleStats(host);
@@ -147176,4 +145652,4 @@ async function handleBuiltInSlashCommand(host, name, args) {
147176
145652
  }
147177
145653
  }
147178
145654
  //#endregion
147179
- export { handleExportDebugZipCommand as $, normalizeWorkDir as $n, lerpGradient as $t, highlightLines as A, getKnowledgeStore as An, isTurnElapsedEnabled as At, ActivityGroupComponent as B, getDataDir as Bn, ENABLE_TERMINAL_THEME_REPORTING as Bt, handleCcCommand as C, getNoActiveSessionMessage as Cn, clearInfoPanelState as Ct, ToolCallComponent as D, isExperimentalFlagEnabled as Dn, createEditorTheme as Dt, UserMessageComponent as E, sortSlashCommands as En, resolveThemeSync as Et, StatusMessageComponent as F, TuiConfigParseError as Fn, detectTerminalTheme as Ft, AgentGroupComponent as G, CLI_UI_MODE as Gn, QUERY_TERMINAL_THEME as Gt, estimateTokens as H, getLogDir as Hn, OSC11_RESPONSE as Ht, replaceTabs as I, TuiLikePreferencesSchema as In, parseOsc11BackgroundTheme as It, getBreathingFrame as J, DEFAULT_CATALOG_URL as Jn, TERMINAL_THEME_DARK as Jt, WelcomeComponent as K, CLI_USER_AGENT_PRODUCT as Kn, TERMINAL_FOCUS_IN as Kt, sanitizeShellOutput as L, loadTuiConfig as Ln, DISABLE_TERMINAL_FOCUS_REPORTING as Lt, CachedContainer as M, startManualEmbeddingDownload as Mn, createThemeStyles as Mt, ThinkingComponent as N, PIXEL_PULSE_FRAMES as Nn, contrastTextHex as Nt, renderDiffLines as O, setExperimentalFlags as On, createMarkdownTheme as Ot, NoticeMessageComponent as P, PULSE_WAVE_FRAMES as Pn, getColorPalette as Pt, refineGoal as Q, encodeWorkDirKey as Qn, FooterComponent as Qt, SkillActivationComponent as R, saveTuiConfig as Rn, DISABLE_TERMINAL_THEME_REPORTING as Rt, readJsonlFile as S, getLlmNotSetMessage as Sn, supportsBalance as St, handleRevokeCommand as T, BUILTIN_SLASH_COMMANDS as Tn, showUsage as Tt, getSharedSpeedTracker as U, detectInstallSource as Un, OSC11_RESPONSE_PREFIX as Ut, charsForTokenBudget as V, getInputHistoryFile as Vn, OSC11_QUERY as Vt, BackgroundAgentStatusComponent as W, CLI_COMMAND_NAME as Wn, OSC11_RESPONSE_PREFIX_NO_ESC as Wt, CompactionComponent as X, saveCatalogCache as Xn, isBusy as Xt, resetBreathingClock as Y, fetchCatalog as Yn, TERMINAL_THEME_LIGHT as Yt, clearGoalState as Z, ScreamHarness as Zn, isStreaming as Zt, handleUpdateCommand as _, MAIN_AGENT_ID$1 as _n, handleYoloCommand as _t, syncDiagramCapabilityPrompt as a, printableChar as an, MemoryMemoStore as ar, toTerminalHyperlink as at, readUpdateCache as b, getCtrlCHint as bn, showSettingsSelector as bt, disposeChildren as c, argsRecord as cn, resolveGlobalLogPath as cr, handleAutoCommand as ct, isPlanExpandable as d, parseStreamingArgs as dn, ErrorCodes as dr, handleEditorCommand as dt, SubagentSlots as en, appendSessionIndexEntry as er, handleExportMdCommand as et, MoonLoader as f, serializeToolResultOutput as fn, SCREAM_ERROR_INFO as fr, handleFusionPlanCommand as ft, handleMcpCommand as g, EXIT_CONFIRM_WINDOW_MS as gn, handleWolfpackCommand as gt, handleChannelCommand as h, EMPTY_SESSION_HINT_URL as hn, handleThemeCommand as ht, buildRoleAdditionalText as i, handleLogoutCommand as in, resolveScreamHome as ir, isInternalMessage as it, langFromPath as j, hasEmbeddingModelCache as jn, toggleEmptySessionHint as jt, renderDiffLinesClustered as k, embeddingFailureDetail as kn, isEmptySessionHintDismissed as kt, hasDispose as l, formatErrorMessage as ln, isScreamError as lr, handleBotCommand as lt, handleMemoryCommand as m, truncateErrorMessage as mn, handlePlanCommand as mt, clearEvalPanelState as n, handleSearchCommand as nn, removeSessionIndexEntry as nr, handleInitCommand as nt, handleExtensionCommand as o, STATUS_BULLET as on, flushDiagnosticLogs as or, changeThinkingLevel as ot, formatMemoryMemoForInjection as p, stringValue as pn, handleModelCommand as pt, BREATHE_CYCLE_MS as q, PRODUCT_NAME as qn, TERMINAL_FOCUS_OUT as qt, openUrl as r, handleConnectCommand as rn, MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE as rr, handleTitleCommand as rt, handleSkillCommand as s, appendStreamingArgsPreview as sn, log as sr, getModelCycleLevel as st, dispatchInput as t, handleTraceCommand as tn, readSessionIndex as tr, handleForkCommand as tt, isExpandable as u, isTodoItemShape as un, isOrphanedToolCallError as ur, handleCompactCommand as ut, selectUpdateTarget as v, SESSION_TIPS as vn, showModelPicker as vt, getDaemonInstructions as w, buildSkillSlashCommands as wn, showStatusReport as wt, appendJsonlLine as x, getCtrlDHint as xn, refreshProviderBalance as xt, refreshUpdateCache as y, TIP_ROTATION_INTERVAL_MS as yn, showPermissionPicker as yt, AssistantMessageComponent as z, detectShellEnvironment as zn, ENABLE_TERMINAL_FOCUS_REPORTING as zt };
145655
+ export { handleExportDebugZipCommand as $, encodeWorkDirKey as $n, lerpGradient as $t, highlightLines as A, embeddingFailureDetail as An, isTurnElapsedEnabled as At, ActivityGroupComponent as B, detectShellEnvironment as Bn, ENABLE_TERMINAL_THEME_REPORTING as Bt, handleCcCommand as C, getLlmNotSetMessage as Cn, clearInfoPanelState as Ct, ToolCallComponent as D, sortSlashCommands as Dn, createEditorTheme as Dt, UserMessageComponent as E, BUILTIN_SLASH_COMMANDS as En, resolveThemeSync as Et, StatusMessageComponent as F, PULSE_WAVE_FRAMES as Fn, detectTerminalTheme as Ft, AgentGroupComponent as G, CLI_COMMAND_NAME as Gn, QUERY_TERMINAL_THEME as Gt, estimateTokens as H, getInputHistoryFile as Hn, OSC11_RESPONSE as Ht, replaceTabs as I, TuiConfigParseError as In, parseOsc11BackgroundTheme as It, getBreathingFrame as J, PRODUCT_NAME as Jn, TERMINAL_THEME_DARK as Jt, WelcomeComponent as K, CLI_UI_MODE as Kn, TERMINAL_FOCUS_IN as Kt, sanitizeShellOutput as L, TuiLikePreferencesSchema as Ln, DISABLE_TERMINAL_FOCUS_REPORTING as Lt, CachedContainer as M, hasEmbeddingModelCache as Mn, createThemeStyles as Mt, ThinkingComponent as N, startManualEmbeddingDownload as Nn, contrastTextHex as Nt, renderDiffLines as O, isExperimentalFlagEnabled as On, createMarkdownTheme as Ot, NoticeMessageComponent as P, PIXEL_PULSE_FRAMES as Pn, getColorPalette as Pt, refineGoal as Q, ScreamHarness as Qn, FooterComponent as Qt, SkillActivationComponent as R, loadTuiConfig as Rn, DISABLE_TERMINAL_THEME_REPORTING as Rt, readJsonlFile as S, getCtrlDHint as Sn, supportsBalance as St, handleRevokeCommand as T, buildSkillSlashCommands as Tn, showUsage as Tt, getSharedSpeedTracker as U, getLogDir as Un, OSC11_RESPONSE_PREFIX as Ut, charsForTokenBudget as V, getDataDir as Vn, OSC11_QUERY as Vt, BackgroundAgentStatusComponent as W, detectInstallSource as Wn, OSC11_RESPONSE_PREFIX_NO_ESC as Wt, CompactionComponent as X, fetchCatalog as Xn, isBusy as Xt, resetBreathingClock as Y, DEFAULT_CATALOG_URL as Yn, TERMINAL_THEME_LIGHT as Yt, clearGoalState as Z, saveCatalogCache as Zn, isStreaming as Zt, handleUpdateCommand as _, EXIT_CONFIRM_WINDOW_MS as _n, handleYoloCommand as _t, syncDiagramCapabilityPrompt as a, printableChar as an, resolveScreamHome as ar, toTerminalHyperlink as at, readUpdateCache as b, TIP_ROTATION_INTERVAL_MS as bn, showSettingsSelector as bt, disposeChildren as c, appendStreamingArgsPreview as cn, resolveGlobalLogPath as cr, handleAutoCommand as ct, isPlanExpandable as d, isTodoItemShape as dn, ErrorCodes as dr, handleEditorCommand as dt, SubagentSlots as en, normalizeWorkDir as er, handleExportMdCommand as et, MoonLoader as f, parseStreamingArgs as fn, SCREAM_ERROR_INFO as fr, handleFusionPlanCommand as ft, handleMcpCommand as g, EMPTY_SESSION_HINT_URL as gn, handleWolfpackCommand as gt, handleChannelCommand as h, truncateErrorMessage as hn, handleThemeCommand as ht, buildRoleAdditionalText as i, handleLogoutCommand as in, MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE as ir, isInternalMessage as it, langFromPath as j, getKnowledgeStore as jn, toggleEmptySessionHint as jt, renderDiffLinesClustered as k, setExperimentalFlags as kn, isEmptySessionHintDismissed as kt, hasDispose as l, argsRecord as ln, isScreamError as lr, handleBotCommand as lt, handleMemoryCommand as m, stringValue as mn, handlePlanCommand as mt, clearEvalPanelState as n, handleSearchCommand as nn, readSessionIndex as nr, handleInitCommand as nt, handleExtensionCommand as o, STATUS_BULLET as on, flushDiagnosticLogs as or, changeThinkingLevel as ot, formatMemoryMemoForInjection as p, serializeToolResultOutput as pn, handleModelCommand as pt, BREATHE_CYCLE_MS as q, CLI_USER_AGENT_PRODUCT as qn, TERMINAL_FOCUS_OUT as qt, openUrl as r, handleConnectCommand as rn, removeSessionIndexEntry as rr, handleTitleCommand as rt, handleSkillCommand as s, appendSessionHeaderHint as sn, log as sr, getModelCycleLevel as st, dispatchInput as t, handleTraceCommand as tn, appendSessionIndexEntry as tr, handleForkCommand as tt, isExpandable as u, formatErrorMessage as un, isOrphanedToolCallError as ur, handleCompactCommand as ut, selectUpdateTarget as v, MAIN_AGENT_ID$1 as vn, showModelPicker as vt, getDaemonInstructions as w, getNoActiveSessionMessage as wn, showStatusReport as wt, appendJsonlLine as x, getCtrlCHint as xn, refreshProviderBalance as xt, refreshUpdateCache as y, SESSION_TIPS as yn, showPermissionPicker as yt, AssistantMessageComponent as z, saveTuiConfig as zn, ENABLE_TERMINAL_FOCUS_REPORTING as zt };