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.
- package/dist/{app-DDepBvBx.mjs → app-GeQQHNLx.mjs} +93 -14
- package/dist/{dispatch-Vw9ccFm7.mjs → dispatch-CNitkOqX.mjs} +346 -1870
- package/dist/{dispatch-CIGyjBDa.mjs → dispatch-Du8ddILK.mjs} +1 -1
- package/dist/main.mjs +1 -1
- package/dist/src-CXmZg1VT.mjs +4115 -0
- package/dist/{src-h6ZExh4A.mjs → src-DIAKpkXO.mjs} +1 -1
- package/dist/{text-input-dialog-D9ByqSsK.mjs → text-input-dialog-6v2667rm.mjs} +6 -2
- package/dist/{text-input-dialog-B-6Pgo6m.mjs → text-input-dialog-DDUarM0_.mjs} +1 -1
- package/package.json +1 -1
- package/dist/src-tDEINaMV.mjs +0 -2249
|
@@ -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
|
|
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-
|
|
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,
|
|
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,
|
|
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
|
|
44498
|
-
const
|
|
44499
|
-
|
|
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: "
|
|
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-
|
|
55562
|
-
const { results, trace } = await multiSearchWithTrace(store, llm, query, {
|
|
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
|
|
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
|
-
|
|
95373
|
-
|
|
95374
|
-
|
|
95375
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95392
|
-
|
|
95393
|
-
|
|
95394
|
-
|
|
95395
|
-
|
|
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.
|
|
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)
|
|
102582
|
-
|
|
102583
|
-
|
|
102584
|
-
|
|
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
|
-
|
|
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" ?
|
|
103236
|
-
this.knowledgeStoreReady = this.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
128934
|
-
return existsSync(modelDir) && existsSync(join(modelDir, "model_optimized.onnx"));
|
|
127375
|
+
return hasEmbeddingModelCache$1(getEmbeddingCacheDir());
|
|
128935
127376
|
}
|
|
128936
127377
|
/**
|
|
128937
|
-
*
|
|
128938
|
-
*
|
|
128939
|
-
*
|
|
128940
|
-
*
|
|
128941
|
-
*
|
|
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 ??= (
|
|
128948
|
-
const store =
|
|
128949
|
-
|
|
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
|
-
})
|
|
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
|
|
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
|
|
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
|
-
|
|
128975
|
-
|
|
128976
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127456
|
+
lastManualFailed = !ok;
|
|
129016
127457
|
return ok ? { ok } : failureResult(error);
|
|
129017
127458
|
} catch (error) {
|
|
129018
|
-
|
|
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
|
|
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
|
|
133470
|
-
const
|
|
133471
|
-
const
|
|
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
|
-
|
|
133476
|
-
|
|
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-
|
|
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-
|
|
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
|
-
*
|
|
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
|
|
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 =
|
|
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, {
|
|
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 $,
|
|
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 };
|