@memory-river/core 0.2.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/distill/concentrator-adapter.d.ts +26 -2
- package/dist/distill/concentrator-adapter.js +273 -17
- package/dist/engine.js +5 -1
- package/dist/store/store-v4.d.ts +1 -0
- package/dist/store/store-v4.js +27 -6
- package/dist/types.d.ts +8 -7
- package/dist/types.js +16 -8
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* 3. 雙軌並行提煉:短期前情提要 (大膠囊, health: 30) + 長期精確記憶 (小紙條)
|
|
8
8
|
* 4. 完美回注與幽靈清理:注入 Context 頂端,並自動拔除斷片的 Tool Result
|
|
9
9
|
*/
|
|
10
|
+
import { type ChildProcess } from 'node:child_process';
|
|
10
11
|
import { ContextMessage } from '../types.js';
|
|
11
12
|
import type { RawTranscriptEntry, TranscriptArchive } from '../transcript/transcript-archive.js';
|
|
12
13
|
import type { SessionIdentity } from '../util/session-identity.js';
|
|
@@ -87,7 +88,23 @@ export declare function findProbeTextMismatchDetail(summarizePairs: ComparableTr
|
|
|
87
88
|
export declare function logProbeTextMismatchDetail(detail: ProbeTextMismatchDetail): void;
|
|
88
89
|
export declare function buildDualTrackPrompt(conversationLog: string, capsuleLanguage?: string): string;
|
|
89
90
|
export declare function buildGeneralConversationPrompt(conversationLog: string, capsuleLanguage?: string): string;
|
|
91
|
+
export declare function buildImportanceScoringPrompt(noteTexts: string[]): string;
|
|
90
92
|
export declare function buildSimplePrompt(conversationLog: string, capsuleLanguage?: string): string;
|
|
93
|
+
export type CodexReasoningEffort = 'none' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
94
|
+
export interface CodexCliConfig {
|
|
95
|
+
model: string;
|
|
96
|
+
reasoningEffort: CodexReasoningEffort;
|
|
97
|
+
workdir: string;
|
|
98
|
+
timeoutMs: number;
|
|
99
|
+
}
|
|
100
|
+
type ExecFileCallback = (error: NodeJS.ErrnoException | null, stdout: string | Buffer, stderr: string | Buffer) => void;
|
|
101
|
+
type ExecFileInvoker = (file: string, args: string[], options: {
|
|
102
|
+
cwd: string;
|
|
103
|
+
timeout: number;
|
|
104
|
+
}, callback: ExecFileCallback) => Pick<ChildProcess, 'kill' | 'stdin'> | undefined;
|
|
105
|
+
export declare function buildCodexExecArgs(prompt: string, config: CodexCliConfig): string[];
|
|
106
|
+
export declare function parseCodexOutput(stdout: string): unknown;
|
|
107
|
+
export declare function runCodexCli(prompt: string, config: CodexCliConfig, execFileImpl?: ExecFileInvoker): Promise<string>;
|
|
91
108
|
export interface ConcentratorConfig {
|
|
92
109
|
apiKey: string;
|
|
93
110
|
model: string;
|
|
@@ -101,7 +118,11 @@ export interface ConcentratorConfig {
|
|
|
101
118
|
*/
|
|
102
119
|
capsuleLanguage?: string;
|
|
103
120
|
concentrationTarget?: number;
|
|
104
|
-
provider?: 'gemini' | 'deepseek';
|
|
121
|
+
provider?: 'codex' | 'gemini' | 'deepseek';
|
|
122
|
+
codexModel?: string;
|
|
123
|
+
codexReasoningEffort?: CodexReasoningEffort;
|
|
124
|
+
codexWorkdir?: string;
|
|
125
|
+
codexTimeoutMs?: number;
|
|
105
126
|
maxTokens?: number;
|
|
106
127
|
deepseekApiKey?: string;
|
|
107
128
|
deepseekModel?: string;
|
|
@@ -109,6 +130,7 @@ export interface ConcentratorConfig {
|
|
|
109
130
|
transcriptArchive: TranscriptArchive;
|
|
110
131
|
sessionSummaryDir: string;
|
|
111
132
|
llm?: LlmClient;
|
|
133
|
+
importanceScorer?: (prompt: string) => Promise<string>;
|
|
112
134
|
}
|
|
113
135
|
export interface CapsuleOutput {
|
|
114
136
|
messages: ContextMessage[];
|
|
@@ -122,6 +144,7 @@ export declare class ConcentratorAdapter implements LlmClient {
|
|
|
122
144
|
private statsStore?;
|
|
123
145
|
private transcriptArchive;
|
|
124
146
|
private llm?;
|
|
147
|
+
private importanceScorer?;
|
|
125
148
|
private readonly MAX_CONTEXT_WINDOW;
|
|
126
149
|
private static readonly WATERLINE_CODE;
|
|
127
150
|
private static readonly WATERLINE_DEFAULT;
|
|
@@ -133,6 +156,7 @@ export declare class ConcentratorAdapter implements LlmClient {
|
|
|
133
156
|
*/
|
|
134
157
|
private detectConversationMode;
|
|
135
158
|
buildFallbackCapsule(messages: ContextMessage[]): string;
|
|
159
|
+
private scoreNoteImportance;
|
|
136
160
|
concentrate(rawMessages: ContextMessage[], dryRun?: boolean, force?: boolean, context?: ConcentrateContext): Promise<CapsuleOutput>;
|
|
137
161
|
estimateTokens(messages: ContextMessage[]): number;
|
|
138
162
|
estimateTokenBreakdown(messages: ContextMessage[]): {
|
|
@@ -142,7 +166,7 @@ export declare class ConcentratorAdapter implements LlmClient {
|
|
|
142
166
|
};
|
|
143
167
|
/**
|
|
144
168
|
* Provider 輪替 fallback 核心方法
|
|
145
|
-
* 依序嘗試 gemini → deepseek,任一成功即返回
|
|
169
|
+
* 依序嘗試 codex → gemini → deepseek,任一成功即返回
|
|
146
170
|
* Gemini 若連續 3 次 503,冷卻 90 秒內直接跳過
|
|
147
171
|
*/
|
|
148
172
|
private callWithFallback;
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import * as path from 'node:path';
|
|
11
11
|
import * as fs from 'node:fs';
|
|
12
|
+
import * as os from 'node:os';
|
|
13
|
+
import { execFile } from 'node:child_process';
|
|
12
14
|
import { CapsuleBridge } from '../pipeline/capsule-bridge.js';
|
|
13
15
|
import { sharedLLMRateLimiter } from '../util/rate-limiter.js';
|
|
14
16
|
const CONCRETE_FACT_NOTE_CATEGORIES = new Set([
|
|
@@ -794,7 +796,7 @@ ${conversationLog}
|
|
|
794
796
|
【時間正規化】對話每個 turn 前綴有 [at=<ISO>] 絕對時間戳。當事實含「昨天/今天/明天/上週/下個月/剛才/之後」等相對時間,必須以該事實所在 turn 的 at 為錨,在 text 內寫出絕對日期(例:「在 2023-05-07(原文稱昨天)參加了…」),並填 when 欄位。若 at=unknown 或語意不足以唯一解析,保留原文相對詞、不可猜,when.precision 與 when.source 設為 "unknown"。明確日期、月份、年份、區間、期限、事件先後順序都要保留,不要只寫「最近」「之前」。
|
|
795
797
|
|
|
796
798
|
每筆格式:
|
|
797
|
-
{ "text": "...", "category": "fact|decision|entity|preference|constraint|identity|knowledge|history|business|other", "
|
|
799
|
+
{ "text": "...", "category": "fact|decision|entity|preference|constraint|identity|knowledge|history|business|other", "tags": ["..."], "subject": "主體(選配)", "predicate": "關係/屬性/動作(選配)", "value": "值,字串/數字/布林(選配)", "unit": "數值單位(選配)", "when": { "start": "YYYY-MM-DD 或 ISO(選配)", "end": "(選配)", "precision": "datetime|date|month|year|range|unknown", "sourceText": "原文時間詞", "source": "explicit|relative_anchored|contextual|unknown", "anchor": "錨定用的 turn ISO 時間" } }
|
|
798
800
|
(subject/predicate/value/unit/entities/when 全為選配:有對應資訊才填,沒有就整個省略,不可填空字串或編造。text 仍是主要可讀、可檢索內容;日期、數值、人名、地名、檔案名、產品名、事件名也必須同時寫進 text,能正規化的日期同時寫入 text 與 when。)
|
|
799
801
|
|
|
800
802
|
收錄:
|
|
@@ -814,7 +816,6 @@ ${conversationLog}
|
|
|
814
816
|
- 一筆只表達一個可獨立更新的主張,必須自足:寫明主體、內容、作用域,以及必要的時間、狀態、條件或理由;不得使用「這個」「上述方案」「已處理」等脫離原文便無法理解的指涉。
|
|
815
817
|
- 保留足以語意檢索與回查原始 transcript 的專案、元件、人物或事件名稱,但不要複製整段對話、長篇推理、日誌或操作過程(原文可由 rehydrate 取回)。
|
|
816
818
|
- category 選最能代表該主張長期用途者;tags 用少量具辨識力的實體/專案/領域/狀態詞。
|
|
817
|
-
- importance 依「跨時間耐久性 × 未來決策效用」評分,不依篇幅、情緒強度或主題是否熱門;較低分代表耐久性或決策效用較低,但不得因此省略可獨立檢索的具體事實。
|
|
818
819
|
|
|
819
820
|
CRITICAL INSTRUCTION: Output ONLY valid, raw JSON. Do NOT wrap in markdown code blocks. Start with '{' and end with '}'.
|
|
820
821
|
|
|
@@ -841,7 +842,6 @@ confidence 評分標準(0.0–1.0):
|
|
|
841
842
|
{
|
|
842
843
|
"text": "精確的顆粒化記憶(含足夠上下文,獨立可理解;日期/數值/人名要寫進 text)",
|
|
843
844
|
"category": "fact|decision|entity|preference|constraint|identity|knowledge|history|business|other",
|
|
844
|
-
"importance": 0.0-1.0,
|
|
845
845
|
"tags": [],
|
|
846
846
|
"subject": "主體(選配,省略則整個不要出現)",
|
|
847
847
|
"predicate": "關係/屬性/動作(選配)",
|
|
@@ -896,7 +896,7 @@ ${capsuleLanguageInstruction}寫一段「前情提要」(600–900 字),
|
|
|
896
896
|
【時間正規化】對話每個 turn 前綴有 [at=<ISO>] 絕對時間戳。當事實含「昨天/今天/明天/上週/下個月/剛才/之後」等相對時間,必須以該事實所在 turn 的 at 為錨,在 text 內寫出絕對日期(例:「在 2023-05-07(原文稱昨天)參加了…」),並填 when 欄位。若 at=unknown 或語意不足以唯一解析,保留原文相對詞、不可猜,when.precision 與 when.source 設為 "unknown"。明確日期、月份、年份、區間、期限、事件先後順序都要保留,不要只寫「最近」「之前」。
|
|
897
897
|
|
|
898
898
|
每筆格式:
|
|
899
|
-
{ "text": "...", "category": "fact|decision|entity|preference|constraint|identity|knowledge|history|business|other", "
|
|
899
|
+
{ "text": "...", "category": "fact|decision|entity|preference|constraint|identity|knowledge|history|business|other", "tags": ["..."], "subject": "主體(選配)", "predicate": "關係/屬性/動作(選配)", "value": "值,字串/數字/布林(選配)", "unit": "數值單位(選配)", "when": { "start": "YYYY-MM-DD 或 ISO(選配)", "end": "(選配)", "precision": "datetime|date|month|year|range|unknown", "sourceText": "原文時間詞", "source": "explicit|relative_anchored|contextual|unknown", "anchor": "錨定用的 turn ISO 時間" } }
|
|
900
900
|
(subject/predicate/value/unit/entities/when 全為選配:有對應資訊才填,沒有就整個省略,不可填空字串或編造。text 仍是主要可讀、可檢索內容;日期、數值、人名、地名、檔案名、產品名、事件名也必須同時寫進 text,能正規化的日期同時寫入 text 與 when。)
|
|
901
901
|
|
|
902
902
|
收錄:
|
|
@@ -916,7 +916,6 @@ ${capsuleLanguageInstruction}寫一段「前情提要」(600–900 字),
|
|
|
916
916
|
- 一筆只表達一個可獨立更新的主張,必須自足:寫明主體、內容、作用域,以及必要的時間、狀態、條件或理由;不得使用「這個」「上述方案」「已處理」等脫離原文便無法理解的指涉。
|
|
917
917
|
- 保留足以語意檢索與回查原始 transcript 的專案、元件、人物或事件名稱,但不要複製整段對話、長篇推理、日誌或操作過程(原文可由 rehydrate 取回)。
|
|
918
918
|
- category 選最能代表該主張長期用途者;tags 用少量具辨識力的實體/專案/領域/狀態詞。
|
|
919
|
-
- importance 依「跨時間耐久性 × 未來決策效用」評分,不依篇幅、情緒強度或主題是否熱門;較低分代表耐久性或決策效用較低,但不得因此省略可獨立檢索的具體事實。
|
|
920
919
|
|
|
921
920
|
CRITICAL INSTRUCTION: Output ONLY valid, raw JSON. Do NOT wrap in markdown code blocks. Start with '{' and end with '}'.
|
|
922
921
|
|
|
@@ -930,7 +929,7 @@ confidence 評分標準(0.0–1.0):
|
|
|
930
929
|
"confidence": 0.85,
|
|
931
930
|
"capsule": "任務B的自然語言前情提要...",
|
|
932
931
|
"notes": [
|
|
933
|
-
{ "text": "精確的顆粒化記憶(含足夠上下文,獨立可理解;日期/數值/人名要寫進 text)", "category": "fact|decision|entity|preference|constraint|identity|knowledge|history|business|other", "
|
|
932
|
+
{ "text": "精確的顆粒化記憶(含足夠上下文,獨立可理解;日期/數值/人名要寫進 text)", "category": "fact|decision|entity|preference|constraint|identity|knowledge|history|business|other", "tags": [], "subject": "(選配,省略則不要出現)", "predicate": "(選配)", "value": "(選配)", "unit": "(選配)", "when": { "start": "YYYY-MM-DD(選配)", "precision": "date", "sourceText": "原文時間詞", "source": "relative_anchored", "anchor": "turn ISO 時間" } }
|
|
934
933
|
]
|
|
935
934
|
}
|
|
936
935
|
|
|
@@ -940,6 +939,70 @@ confidence 評分標準(0.0–1.0):
|
|
|
940
939
|
- 必須回傳可完整 JSON.parse 的有效 JSON,不可輸出半截 JSON`;
|
|
941
940
|
return isSourceLanguage ? `${sourceLanguageRequirementTop}\n\n${prompt}\n\n${sourceLanguageRequirementBottom}` : prompt;
|
|
942
941
|
}
|
|
942
|
+
export function buildImportanceScoringPrompt(noteTexts) {
|
|
943
|
+
const numberedNotes = noteTexts
|
|
944
|
+
.map((text, index) => `${index + 1}. ${text}`)
|
|
945
|
+
.join('\n');
|
|
946
|
+
return `非互動模式,立即回答,不要提問、不要使用任何工具、不要讀檔。
|
|
947
|
+
|
|
948
|
+
下面是即將存進 AI 長期記憶庫的記憶。請為每一則評 importance(0.0-1.0)。
|
|
949
|
+
|
|
950
|
+
評分標準(照這個定義,不要自行更改):
|
|
951
|
+
importance 依「跨時間耐久性 × 未來決策效用」評分,不依篇幅、情緒強度或主題是否熱門;
|
|
952
|
+
較低分代表耐久性或決策效用較低。
|
|
953
|
+
|
|
954
|
+
只輸出 JSON,不要任何其他文字、不要 markdown 圍欄。必須包含全部 <${noteTexts.length}> 筆:
|
|
955
|
+
{"r":[{"n":<編號>,"i":<0.0-1.0>}]}
|
|
956
|
+
|
|
957
|
+
${numberedNotes}`;
|
|
958
|
+
}
|
|
959
|
+
class ImportanceScoreParseError extends Error {
|
|
960
|
+
scores;
|
|
961
|
+
constructor(message, scores) {
|
|
962
|
+
super(message);
|
|
963
|
+
this.scores = scores;
|
|
964
|
+
this.name = 'ImportanceScoreParseError';
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
function parseImportanceScores(raw, expectedCount) {
|
|
968
|
+
const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
969
|
+
let response = null;
|
|
970
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
971
|
+
try {
|
|
972
|
+
const candidate = JSON.parse(lines[index]);
|
|
973
|
+
if (candidate && typeof candidate === 'object' && Array.isArray(candidate.r)) {
|
|
974
|
+
response = candidate;
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
catch {
|
|
979
|
+
// Codex progress lines and other non-JSON output are expected here.
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (!response) {
|
|
983
|
+
throw new ImportanceScoreParseError('importance score response parse failed', new Map());
|
|
984
|
+
}
|
|
985
|
+
const scores = new Map();
|
|
986
|
+
for (const entry of response.r) {
|
|
987
|
+
if (!entry || typeof entry !== 'object')
|
|
988
|
+
continue;
|
|
989
|
+
const item = entry;
|
|
990
|
+
if (typeof item.n === 'number'
|
|
991
|
+
&& Number.isInteger(item.n)
|
|
992
|
+
&& item.n >= 1
|
|
993
|
+
&& item.n <= expectedCount
|
|
994
|
+
&& typeof item.i === 'number'
|
|
995
|
+
&& Number.isFinite(item.i)) {
|
|
996
|
+
scores.set(item.n, Math.max(0, Math.min(1, item.i)));
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const missing = Array.from({ length: expectedCount }, (_, index) => index + 1)
|
|
1000
|
+
.filter((number) => !scores.has(number));
|
|
1001
|
+
if (missing.length > 0) {
|
|
1002
|
+
throw new ImportanceScoreParseError(`importance score response missing notes: ${missing.join(',')}`, scores);
|
|
1003
|
+
}
|
|
1004
|
+
return scores;
|
|
1005
|
+
}
|
|
943
1006
|
// ─── 降級版 Prompt (專供本地小模型使用) ───
|
|
944
1007
|
export function buildSimplePrompt(conversationLog, capsuleLanguage = '繁體中文') {
|
|
945
1008
|
if (capsuleLanguage === 'source') {
|
|
@@ -1076,14 +1139,123 @@ function classifyConcentratorFailure(err) {
|
|
|
1076
1139
|
return 'quota';
|
|
1077
1140
|
return 'other';
|
|
1078
1141
|
}
|
|
1142
|
+
const CODEX_FAILURE_BREAKER_THRESHOLD = 3;
|
|
1143
|
+
const CODEX_FAILURE_COOLDOWN_MS = 90_000;
|
|
1079
1144
|
const GEMINI_503_BREAKER_THRESHOLD = 3;
|
|
1080
1145
|
const GEMINI_503_COOLDOWN_MS = 90_000;
|
|
1146
|
+
let codexConsecutiveFailureCount = 0;
|
|
1147
|
+
let codexCooldownUntil = 0;
|
|
1081
1148
|
let geminiConsecutive503Count = 0;
|
|
1082
1149
|
let geminiCooldownUntil = 0;
|
|
1083
1150
|
function isGemini503Error(err) {
|
|
1084
1151
|
const message = String(err?.message ?? err ?? '').toLowerCase();
|
|
1085
1152
|
return message.includes('gemini api error: 503');
|
|
1086
1153
|
}
|
|
1154
|
+
export function buildCodexExecArgs(prompt, config) {
|
|
1155
|
+
return [
|
|
1156
|
+
'exec',
|
|
1157
|
+
'-C', config.workdir,
|
|
1158
|
+
'-c', `model=${config.model}`,
|
|
1159
|
+
'-c', `model_reasoning_effort=${config.reasoningEffort}`,
|
|
1160
|
+
prompt,
|
|
1161
|
+
];
|
|
1162
|
+
}
|
|
1163
|
+
function findBalancedJsonCandidates(text) {
|
|
1164
|
+
const stack = [];
|
|
1165
|
+
const candidates = [];
|
|
1166
|
+
let inString = false;
|
|
1167
|
+
let escaped = false;
|
|
1168
|
+
for (let i = 0; i < text.length; i++) {
|
|
1169
|
+
const ch = text[i];
|
|
1170
|
+
if (escaped) {
|
|
1171
|
+
escaped = false;
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
if (inString && ch === '\\') {
|
|
1175
|
+
escaped = true;
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
if (ch === '"') {
|
|
1179
|
+
inString = !inString;
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
if (inString)
|
|
1183
|
+
continue;
|
|
1184
|
+
if (ch === '[' || ch === '{') {
|
|
1185
|
+
stack.push({ opener: ch, start: i });
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
if (ch !== ']' && ch !== '}')
|
|
1189
|
+
continue;
|
|
1190
|
+
const expected = ch === ']' ? '[' : '{';
|
|
1191
|
+
const top = stack[stack.length - 1];
|
|
1192
|
+
if (!top || top.opener !== expected)
|
|
1193
|
+
continue;
|
|
1194
|
+
stack.pop();
|
|
1195
|
+
candidates.push(text.slice(top.start, i + 1));
|
|
1196
|
+
}
|
|
1197
|
+
return candidates;
|
|
1198
|
+
}
|
|
1199
|
+
export function parseCodexOutput(stdout) {
|
|
1200
|
+
const trimmed = stdout.trim();
|
|
1201
|
+
if (!trimmed)
|
|
1202
|
+
throw new Error('Codex CLI returned empty stdout');
|
|
1203
|
+
try {
|
|
1204
|
+
const parsed = JSON.parse(trimmed);
|
|
1205
|
+
if (parsed && typeof parsed === 'object')
|
|
1206
|
+
return parsed;
|
|
1207
|
+
}
|
|
1208
|
+
catch { }
|
|
1209
|
+
const candidates = findBalancedJsonCandidates(stdout);
|
|
1210
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
1211
|
+
try {
|
|
1212
|
+
const parsed = JSON.parse(candidates[i]);
|
|
1213
|
+
if (parsed && typeof parsed === 'object')
|
|
1214
|
+
return parsed;
|
|
1215
|
+
}
|
|
1216
|
+
catch { }
|
|
1217
|
+
}
|
|
1218
|
+
throw new Error('Codex CLI stdout did not contain parseable JSON');
|
|
1219
|
+
}
|
|
1220
|
+
export function runCodexCli(prompt, config, execFileImpl = execFile) {
|
|
1221
|
+
const args = buildCodexExecArgs(prompt, config);
|
|
1222
|
+
return new Promise((resolve, reject) => {
|
|
1223
|
+
let settled = false;
|
|
1224
|
+
let timedOut = false;
|
|
1225
|
+
let child;
|
|
1226
|
+
const timer = setTimeout(() => {
|
|
1227
|
+
timedOut = true;
|
|
1228
|
+
child?.kill('SIGTERM');
|
|
1229
|
+
reject(new Error(`Codex CLI timed out after ${config.timeoutMs}ms`));
|
|
1230
|
+
}, config.timeoutMs);
|
|
1231
|
+
const finish = (error, stdout, stderr) => {
|
|
1232
|
+
if (settled)
|
|
1233
|
+
return;
|
|
1234
|
+
settled = true;
|
|
1235
|
+
clearTimeout(timer);
|
|
1236
|
+
if (timedOut)
|
|
1237
|
+
return;
|
|
1238
|
+
if (error) {
|
|
1239
|
+
const detail = String(stderr || error.message || error);
|
|
1240
|
+
reject(new Error(`Codex CLI failed: ${detail}`));
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
try {
|
|
1244
|
+
resolve(JSON.stringify(parseCodexOutput(String(stdout))));
|
|
1245
|
+
}
|
|
1246
|
+
catch (parseError) {
|
|
1247
|
+
reject(parseError);
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
child = execFileImpl('codex', args, {
|
|
1251
|
+
cwd: config.workdir,
|
|
1252
|
+
timeout: config.timeoutMs,
|
|
1253
|
+
}, finish);
|
|
1254
|
+
// execFile 不吃 stdio 選項,child 的 stdin 會是一個開著的 pipe,
|
|
1255
|
+
// codex CLI 因此停在 "Reading additional input from stdin" 直到 timeout。
|
|
1256
|
+
child?.stdin?.end();
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1087
1259
|
function extractBalancedObjectForKey(text, key) {
|
|
1088
1260
|
const keyIndex = text.indexOf(`"${key}"`);
|
|
1089
1261
|
if (keyIndex === -1)
|
|
@@ -1285,6 +1457,7 @@ export class ConcentratorAdapter {
|
|
|
1285
1457
|
statsStore;
|
|
1286
1458
|
transcriptArchive;
|
|
1287
1459
|
llm;
|
|
1460
|
+
importanceScorer;
|
|
1288
1461
|
MAX_CONTEXT_WINDOW = 200000;
|
|
1289
1462
|
// ── 動態水位線常數 ──────────────────────────────────────
|
|
1290
1463
|
static WATERLINE_CODE = 0.40; // 代碼/技術密集:40%
|
|
@@ -1298,6 +1471,10 @@ export class ConcentratorAdapter {
|
|
|
1298
1471
|
capsuleLanguage: config.capsuleLanguage ?? '繁體中文',
|
|
1299
1472
|
concentrationTarget: config.concentrationTarget ?? 0,
|
|
1300
1473
|
provider: config.provider ?? 'gemini',
|
|
1474
|
+
codexModel: config.codexModel ?? 'gpt-5.6-luna',
|
|
1475
|
+
codexReasoningEffort: config.codexReasoningEffort ?? 'low',
|
|
1476
|
+
codexWorkdir: config.codexWorkdir || os.homedir(),
|
|
1477
|
+
codexTimeoutMs: config.codexTimeoutMs ?? 120000,
|
|
1301
1478
|
maxTokens: config.maxTokens ?? 8192,
|
|
1302
1479
|
deepseekApiKey: config.deepseekApiKey || '',
|
|
1303
1480
|
deepseekModel: config.deepseekModel ?? 'deepseek-v4-flash',
|
|
@@ -1307,6 +1484,7 @@ export class ConcentratorAdapter {
|
|
|
1307
1484
|
this.statsStore = config.statsStore;
|
|
1308
1485
|
this.transcriptArchive = config.transcriptArchive;
|
|
1309
1486
|
this.llm = config.llm;
|
|
1487
|
+
this.importanceScorer = config.importanceScorer;
|
|
1310
1488
|
this.capsuleBridge = new CapsuleBridge(this.config.inboxPath);
|
|
1311
1489
|
}
|
|
1312
1490
|
/**
|
|
@@ -1376,6 +1554,41 @@ export class ConcentratorAdapter {
|
|
|
1376
1554
|
`## 9. Optional Next Step\n${latestAssistant === '無' ? '無' : latestAssistant}`,
|
|
1377
1555
|
].join('\n\n');
|
|
1378
1556
|
}
|
|
1557
|
+
async scoreNoteImportance(notes, metricContext) {
|
|
1558
|
+
if (notes.length === 0)
|
|
1559
|
+
return;
|
|
1560
|
+
const prompt = buildImportanceScoringPrompt(notes.map((note) => typeof note.text === 'string' ? note.text : ''));
|
|
1561
|
+
let scores = new Map();
|
|
1562
|
+
try {
|
|
1563
|
+
const raw = this.importanceScorer
|
|
1564
|
+
? await this.importanceScorer(prompt)
|
|
1565
|
+
: await this.callWithFallback(prompt, 'scoreImportance', undefined, { sessionIdentity: metricContext?.sessionIdentity, inputTokens: estimatePromptTokens(prompt) }, 2048);
|
|
1566
|
+
scores = parseImportanceScores(raw, notes.length);
|
|
1567
|
+
}
|
|
1568
|
+
catch (error) {
|
|
1569
|
+
if (error instanceof ImportanceScoreParseError) {
|
|
1570
|
+
scores = error.scores;
|
|
1571
|
+
}
|
|
1572
|
+
const message = String(error?.message ?? error).toLowerCase();
|
|
1573
|
+
const reason = error instanceof ImportanceScoreParseError
|
|
1574
|
+
? (message.includes('missing') ? 'partial missing' : 'parse failed')
|
|
1575
|
+
: (message.includes('timeout') || message.includes('timed out') || message.includes('abort')
|
|
1576
|
+
? 'timeout'
|
|
1577
|
+
: 'call failed');
|
|
1578
|
+
console.warn(`[ConcentratorAdapter] Importance scoring fallback: ${reason}; missing scores use 0.5`);
|
|
1579
|
+
}
|
|
1580
|
+
notes.forEach((note, index) => {
|
|
1581
|
+
const importance = scores.get(index + 1);
|
|
1582
|
+
if (importance === undefined) {
|
|
1583
|
+
note.importance = 0.5;
|
|
1584
|
+
note.importanceFallback = true;
|
|
1585
|
+
}
|
|
1586
|
+
else {
|
|
1587
|
+
note.importance = importance;
|
|
1588
|
+
delete note.importanceFallback;
|
|
1589
|
+
}
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1379
1592
|
async concentrate(rawMessages, dryRun = false, force = false, context = {}) {
|
|
1380
1593
|
const messages = rawMessages.map(msg => truncateGiantStrings(msg));
|
|
1381
1594
|
const currentTokens = estimateTotalTokens(messages);
|
|
@@ -1388,7 +1601,7 @@ export class ConcentratorAdapter {
|
|
|
1388
1601
|
if (!needsCut) {
|
|
1389
1602
|
return { messages, wasConcentrated: false, processedThroughIndex: 0 };
|
|
1390
1603
|
}
|
|
1391
|
-
if (!this.llm && !this.config.apiKey && !this.config.deepseekApiKey) {
|
|
1604
|
+
if (!this.llm && this.config.provider !== 'codex' && !this.config.apiKey && !this.config.deepseekApiKey) {
|
|
1392
1605
|
console.warn('[ConcentratorAdapter] Concentration skipped: no LLM API key configured; raw transcripts and recall remain available.');
|
|
1393
1606
|
return { messages, wasConcentrated: false, processedThroughIndex: 0 };
|
|
1394
1607
|
}
|
|
@@ -1577,7 +1790,9 @@ export class ConcentratorAdapter {
|
|
|
1577
1790
|
const originalTokens = estimatePromptTokens(conversationLog);
|
|
1578
1791
|
const summaryTokens = estimatePromptTokens(capsuleText);
|
|
1579
1792
|
const compressionRatio = originalTokens / Math.max(1, summaryTokens);
|
|
1580
|
-
const notes = parsedData.notes
|
|
1793
|
+
const notes = Array.isArray(parsedData.notes)
|
|
1794
|
+
? parsedData.notes.map((item) => item && typeof item === 'object' ? item : {})
|
|
1795
|
+
: [];
|
|
1581
1796
|
const sourceEntryIds = sourceEntryIdsProbe?.matched ? sourceEntryIdsProbe.sourceEntryIds : [];
|
|
1582
1797
|
const sourceEntryRange = sourceEntryIds.length > 0
|
|
1583
1798
|
? {
|
|
@@ -1605,6 +1820,9 @@ export class ConcentratorAdapter {
|
|
|
1605
1820
|
console.log(`[ConcentratorAdapter] sourceEntryIds metadata: length=${sourceEntryIds.length} firstEntryId=${sourceEntryRange?.firstEntryId ?? 'none'} lastEntryId=${sourceEntryRange?.lastEntryId ?? 'none'}`);
|
|
1606
1821
|
console.log(`[ConcentratorAdapter] Short-term capsule written (health: 30, confidence: ${confidence.toFixed(2)})`);
|
|
1607
1822
|
}
|
|
1823
|
+
await this.scoreNoteImportance(notes, {
|
|
1824
|
+
sessionIdentity: context.sessionIdentity,
|
|
1825
|
+
});
|
|
1608
1826
|
const acceptedNoteTexts = [];
|
|
1609
1827
|
for (const item of notes) {
|
|
1610
1828
|
const text = typeof item.text === 'string' ? item.text.trim() : '';
|
|
@@ -1629,6 +1847,7 @@ export class ConcentratorAdapter {
|
|
|
1629
1847
|
confidence,
|
|
1630
1848
|
firstTimestamp,
|
|
1631
1849
|
lastTimestamp,
|
|
1850
|
+
...(item.importanceFallback === true ? { importanceFallback: true } : {}),
|
|
1632
1851
|
...(sourceEntryIds.length > 0 ? { sourceEntryIds } : {}),
|
|
1633
1852
|
...(sourceEntryRange ? { sourceEntryRange } : {}),
|
|
1634
1853
|
// Optional structured enrichment — additive, stored as-is (no schema change),
|
|
@@ -1655,7 +1874,7 @@ export class ConcentratorAdapter {
|
|
|
1655
1874
|
|| rawMessages[0]?.sessionId
|
|
1656
1875
|
|| 'unknown';
|
|
1657
1876
|
const concentratedAt = Date.now();
|
|
1658
|
-
await this.writeSessionSummary({ sessionId, concentratedAt, capsule: capsuleText, notes:
|
|
1877
|
+
await this.writeSessionSummary({ sessionId, concentratedAt, capsule: capsuleText, notes: notes, primaryRequest: summary?.primaryRequest || '', pendingTasks: summary?.pendingTasks || '', nextStep: summary?.nextStep || '' });
|
|
1659
1878
|
}
|
|
1660
1879
|
catch (err) {
|
|
1661
1880
|
console.error("[ConcentratorAdapter] Compaction failed:", err);
|
|
@@ -1683,7 +1902,7 @@ export class ConcentratorAdapter {
|
|
|
1683
1902
|
}
|
|
1684
1903
|
/**
|
|
1685
1904
|
* Provider 輪替 fallback 核心方法
|
|
1686
|
-
* 依序嘗試 gemini → deepseek,任一成功即返回
|
|
1905
|
+
* 依序嘗試 codex → gemini → deepseek,任一成功即返回
|
|
1687
1906
|
* Gemini 若連續 3 次 503,冷卻 90 秒內直接跳過
|
|
1688
1907
|
*/
|
|
1689
1908
|
async callWithFallback(prompt, fnName = 'generate', fallbackPrompt, // concentrate 失敗時由呼叫端接 deterministic capsule;此處不處理
|
|
@@ -1691,22 +1910,44 @@ export class ConcentratorAdapter {
|
|
|
1691
1910
|
if (this.llm) {
|
|
1692
1911
|
return this.llm.generate(prompt, { purpose: fnName, maxTokens });
|
|
1693
1912
|
}
|
|
1694
|
-
const providers =
|
|
1913
|
+
const providers = this.config.provider === 'codex'
|
|
1914
|
+
? ['codex', 'gemini', 'deepseek']
|
|
1915
|
+
: ['gemini', 'deepseek'];
|
|
1695
1916
|
const now = Date.now();
|
|
1917
|
+
if (providers.includes('codex')) {
|
|
1918
|
+
if (now < codexCooldownUntil) {
|
|
1919
|
+
console.warn(`[${fnName}] codex skipped: circuit breaker cooling down for ${Math.ceil((codexCooldownUntil - now) / 1000)}s`);
|
|
1920
|
+
}
|
|
1921
|
+
else {
|
|
1922
|
+
// codex has no API key; authentication belongs to the local CLI.
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
const eligibleProviders = providers.filter((provider) => provider !== 'codex' || now >= codexCooldownUntil);
|
|
1696
1926
|
if (now < geminiCooldownUntil) {
|
|
1697
1927
|
console.warn(`[${fnName}] gemini skipped: circuit breaker cooling down for ${Math.ceil((geminiCooldownUntil - now) / 1000)}s`);
|
|
1698
1928
|
}
|
|
1699
|
-
|
|
1700
|
-
providers.push('gemini');
|
|
1701
|
-
}
|
|
1702
|
-
providers.push('deepseek');
|
|
1929
|
+
const orderedProviders = eligibleProviders.filter((provider) => provider !== 'gemini' || now >= geminiCooldownUntil);
|
|
1703
1930
|
const attemptedProviders = [];
|
|
1704
1931
|
const startedAt = Date.now();
|
|
1705
1932
|
const shouldRecordMetric = fnName === 'concentrate';
|
|
1706
1933
|
let lastError = null;
|
|
1707
|
-
for (const provider of
|
|
1934
|
+
for (const provider of orderedProviders) {
|
|
1708
1935
|
attemptedProviders.push(provider);
|
|
1709
1936
|
try {
|
|
1937
|
+
if (provider === 'codex') {
|
|
1938
|
+
const result = await this.callProvider(provider, prompt, maxTokens);
|
|
1939
|
+
codexConsecutiveFailureCount = 0;
|
|
1940
|
+
await this.recordConcentratorAttemptMetric({
|
|
1941
|
+
metricContext,
|
|
1942
|
+
provider,
|
|
1943
|
+
outcome: 'success',
|
|
1944
|
+
attemptedProviders,
|
|
1945
|
+
inputTokens: metricContext?.inputTokens ?? estimatePromptTokens(prompt),
|
|
1946
|
+
outputTokens: estimatePromptTokens(result),
|
|
1947
|
+
durationMs: Date.now() - startedAt,
|
|
1948
|
+
}, shouldRecordMetric);
|
|
1949
|
+
return result;
|
|
1950
|
+
}
|
|
1710
1951
|
if (provider === 'gemini') {
|
|
1711
1952
|
if (this.config.apiKey) {
|
|
1712
1953
|
const result = await this.callProvider(provider, prompt, maxTokens);
|
|
@@ -1745,7 +1986,14 @@ export class ConcentratorAdapter {
|
|
|
1745
1986
|
}
|
|
1746
1987
|
catch (err) {
|
|
1747
1988
|
console.warn(`[${fnName}] ${provider} failed; trying next provider:`, err);
|
|
1748
|
-
if (provider === '
|
|
1989
|
+
if (provider === 'codex') {
|
|
1990
|
+
codexConsecutiveFailureCount += 1;
|
|
1991
|
+
if (codexConsecutiveFailureCount >= CODEX_FAILURE_BREAKER_THRESHOLD) {
|
|
1992
|
+
codexCooldownUntil = Date.now() + CODEX_FAILURE_COOLDOWN_MS;
|
|
1993
|
+
console.warn(`[${fnName}] codex circuit breaker opened for ${CODEX_FAILURE_COOLDOWN_MS / 1000}s after ${codexConsecutiveFailureCount} consecutive failures`);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
else if (provider === 'gemini') {
|
|
1749
1997
|
if (isGemini503Error(err)) {
|
|
1750
1998
|
geminiConsecutive503Count += 1;
|
|
1751
1999
|
if (geminiConsecutive503Count >= GEMINI_503_BREAKER_THRESHOLD) {
|
|
@@ -1776,6 +2024,14 @@ export class ConcentratorAdapter {
|
|
|
1776
2024
|
if (provider === 'gemini') {
|
|
1777
2025
|
return callGeminiAPI(this.config.apiKey, this.config.model, prompt, maxTokens);
|
|
1778
2026
|
}
|
|
2027
|
+
if (provider === 'codex') {
|
|
2028
|
+
return runCodexCli(prompt, {
|
|
2029
|
+
model: this.config.codexModel,
|
|
2030
|
+
reasoningEffort: this.config.codexReasoningEffort,
|
|
2031
|
+
workdir: this.config.codexWorkdir,
|
|
2032
|
+
timeoutMs: this.config.codexTimeoutMs,
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
1779
2035
|
if (provider === 'deepseek') {
|
|
1780
2036
|
return callDeepSeekAPI(this.config.deepseekApiKey, this.config.deepseekModel, prompt, maxTokens);
|
|
1781
2037
|
}
|
package/dist/engine.js
CHANGED
|
@@ -87,7 +87,11 @@ export class MemoryRiverEngine {
|
|
|
87
87
|
apiKey: config.concentration?.geminiApiKey || config.embedding.apiKey || deps.geminiApiKey,
|
|
88
88
|
model: config.concentration?.model || 'gemini-2.5-flash-lite',
|
|
89
89
|
inboxPath: config.inboxPath,
|
|
90
|
-
provider: config.concentration?.provider || '
|
|
90
|
+
provider: config.concentration?.provider || 'codex',
|
|
91
|
+
codexModel: config.concentration?.codexModel,
|
|
92
|
+
codexReasoningEffort: config.concentration?.codexReasoningEffort,
|
|
93
|
+
codexWorkdir: config.concentration?.codexWorkdir,
|
|
94
|
+
codexTimeoutMs: config.concentration?.codexTimeoutMs,
|
|
91
95
|
maxTokens: config.concentration?.maxTokens ?? 8192,
|
|
92
96
|
deepseekApiKey: config.concentration?.deepseekApiKey || deps.deepseekApiKey,
|
|
93
97
|
deepseekModel: config.concentration?.deepseekModel || 'deepseek-v4-flash',
|
package/dist/store/store-v4.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export declare function normalizeLanceUpdateValues(values: Record<string, unknow
|
|
|
27
27
|
type DecayOptions = {
|
|
28
28
|
coreCategories?: string[];
|
|
29
29
|
coreImportanceThreshold?: number;
|
|
30
|
+
capsuleBypassCore?: boolean;
|
|
30
31
|
skillCapsuleProtection?: boolean;
|
|
31
32
|
dryRun?: boolean;
|
|
32
33
|
deleteWith?: (id: string) => Promise<boolean>;
|
package/dist/store/store-v4.js
CHANGED
|
@@ -18,11 +18,26 @@ import { Schema, Field, Int64, Utf8, Bool, Float64 } from "apache-arrow";
|
|
|
18
18
|
import { optimizeAuxTablesInConnection, recordAuxTableWrite, } from "./aux-table-maintenance.js";
|
|
19
19
|
// 動態載入 jieba
|
|
20
20
|
let jieba = null;
|
|
21
|
+
let jiebaUnavailable = false;
|
|
21
22
|
const loadJieba = async () => {
|
|
22
23
|
if (jieba)
|
|
23
24
|
return jieba;
|
|
25
|
+
if (jiebaUnavailable)
|
|
26
|
+
return null;
|
|
24
27
|
const module = await import("nodejieba");
|
|
25
|
-
|
|
28
|
+
const candidate = module.default ?? module;
|
|
29
|
+
// nodejieba 的 index.js 在原生 binding 缺失時仍能 import 成功,只有第一次真正
|
|
30
|
+
// 呼叫才丟。這裡先探一次,讓失敗落在 tokenizeChinese 的 catch 裡走降級,
|
|
31
|
+
// 而不是逃出去把每一次 store/update/ftsSearch 都打掛。
|
|
32
|
+
try {
|
|
33
|
+
candidate.cut("測");
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
jiebaUnavailable = true;
|
|
37
|
+
console.warn("[MemoryStore] nodejieba unavailable, falling back to per-character Chinese tokenization:", error?.message ?? error);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
jieba = candidate;
|
|
26
41
|
return jieba;
|
|
27
42
|
};
|
|
28
43
|
let lancedbImportPromise = null;
|
|
@@ -56,10 +71,13 @@ export class SchemaViolationError extends Error {
|
|
|
56
71
|
}
|
|
57
72
|
}
|
|
58
73
|
// 預設健康度配置
|
|
74
|
+
// coreImportanceThreshold 必須與 types.ts 的 DEFAULT_CLEANUP_CONFIG 一致。
|
|
75
|
+
// 兩處曾經分別是 0.85 / 0.75,同一個「記憶要多重要才不被淘汰」的問題會因為
|
|
76
|
+
// 呼叫入口不同而得到差一倍的答案(0.85→41% 受保護、0.75→76%)。改一邊要改兩邊。
|
|
59
77
|
const DEFAULT_HEALTH_CONFIG = {
|
|
60
78
|
initialScore: 100,
|
|
61
|
-
coreCategories: ["identity", "constraint", "business", "core_rule"],
|
|
62
|
-
coreImportanceThreshold: 0.
|
|
79
|
+
coreCategories: ["identity", "preference", "constraint", "business", "decision", "core_rule"],
|
|
80
|
+
coreImportanceThreshold: 0.75,
|
|
63
81
|
skillDecayFactor: 0.25,
|
|
64
82
|
};
|
|
65
83
|
const STRING_UPDATE_FIELDS = new Set([
|
|
@@ -2678,6 +2696,7 @@ export class MemoryStore {
|
|
|
2678
2696
|
let deferredDecay = 0, deferredDelete = 0;
|
|
2679
2697
|
const effectiveCoreCategories = options.coreCategories ?? this.healthConfig.coreCategories;
|
|
2680
2698
|
const effectiveCoreImportanceThreshold = options.coreImportanceThreshold ?? this.healthConfig.coreImportanceThreshold;
|
|
2699
|
+
const capsuleBypassCore = options.capsuleBypassCore ?? true;
|
|
2681
2700
|
const protectSkillCapsules = options.skillCapsuleProtection ?? true;
|
|
2682
2701
|
const isDryRun = options.dryRun ?? false;
|
|
2683
2702
|
const maxDelete = options.maxDelete === undefined ? Infinity : Math.max(0, Math.floor(options.maxDelete));
|
|
@@ -2706,14 +2725,16 @@ export class MemoryStore {
|
|
|
2706
2725
|
const id = memoryRow.id;
|
|
2707
2726
|
if (id.startsWith("init_"))
|
|
2708
2727
|
continue;
|
|
2709
|
-
const
|
|
2710
|
-
|
|
2728
|
+
const metaObj = this.parseMetadata(memoryRow.metadata);
|
|
2729
|
+
// dynamic capsule 出生就是短命設計(health 30),不該因為 importance 高而被當成核心記憶保護
|
|
2730
|
+
const isDynamicCapsule = metaObj?.type === 'dynamic_capsule';
|
|
2731
|
+
const isCore = (!capsuleBypassCore || !isDynamicCapsule) && (effectiveCoreCategories.includes(memoryRow.category) ||
|
|
2732
|
+
memoryRow.importance >= effectiveCoreImportanceThreshold);
|
|
2711
2733
|
if (isCore) {
|
|
2712
2734
|
coreProtected++;
|
|
2713
2735
|
continue;
|
|
2714
2736
|
}
|
|
2715
2737
|
// 技能膠囊不參與灰塵清理(只響應用戶明確刪除)
|
|
2716
|
-
const metaObj = this.parseMetadata(memoryRow.metadata);
|
|
2717
2738
|
if (protectSkillCapsules && metaObj?.capsuleType === 'skill_capsule') {
|
|
2718
2739
|
continue;
|
|
2719
2740
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified Types - memory-river
|
|
3
|
-
* Merged from v4/types.ts + context-river/concentrator.ts
|
|
4
|
-
*/
|
|
5
1
|
export type MemoryCategory = "preference" | "fact" | "decision" | "entity" | "constraint" | "identity" | "business" | "knowledge" | "skill" | "other";
|
|
6
2
|
export declare const MEMORY_CATEGORIES: readonly ["preference", "fact", "decision", "entity", "constraint", "identity", "business", "knowledge", "skill", "other"];
|
|
7
3
|
export declare const PROTECTED_CATEGORIES: MemoryCategory[];
|
|
@@ -171,7 +167,7 @@ export interface TranscriptWatermarkRow {
|
|
|
171
167
|
lineCount: number;
|
|
172
168
|
updatedAt: number;
|
|
173
169
|
}
|
|
174
|
-
export type ConcentratorProvider = "gemini" | "deepseek" | "all_failed";
|
|
170
|
+
export type ConcentratorProvider = "codex" | "gemini" | "deepseek" | "all_failed";
|
|
175
171
|
export type ConcentratorStatOutcome = "success" | "partial" | "failure";
|
|
176
172
|
export type ConcentratorFailureReason = "broken_json" | "timeout" | "quota" | "other";
|
|
177
173
|
export interface ConcentratorStat {
|
|
@@ -284,8 +280,9 @@ export interface InboxWriteOptions {
|
|
|
284
280
|
* 實際 embedder-v5.ts 使用 Ollama,不受此欄位影響。
|
|
285
281
|
*
|
|
286
282
|
* Concentration(濃縮)Provider 順序:
|
|
287
|
-
* - 預設:Gemini → DeepSeek
|
|
283
|
+
* - 預設:Codex CLI → Gemini → DeepSeek
|
|
288
284
|
* - Gemini 若連續 3 次 503:冷卻 90 秒,期間直接跳過 Gemini
|
|
285
|
+
* - Codex CLI 若連續 3 次失敗:冷卻 90 秒,期間直接跳過 Codex CLI
|
|
289
286
|
* - concentrate() 全失敗時仍由 ConcentratorAdapter 走 deterministic fallback capsule
|
|
290
287
|
*
|
|
291
288
|
* =============================================================================
|
|
@@ -351,8 +348,12 @@ export interface PluginConfig {
|
|
|
351
348
|
};
|
|
352
349
|
concentration?: {
|
|
353
350
|
model?: string;
|
|
354
|
-
provider?: 'gemini' | 'deepseek';
|
|
351
|
+
provider?: 'codex' | 'gemini' | 'deepseek';
|
|
355
352
|
geminiApiKey?: string;
|
|
353
|
+
codexModel?: string;
|
|
354
|
+
codexReasoningEffort?: 'none' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
355
|
+
codexWorkdir?: string;
|
|
356
|
+
codexTimeoutMs?: number;
|
|
356
357
|
maxTokens?: number;
|
|
357
358
|
deepseekApiKey?: string;
|
|
358
359
|
deepseekModel?: string;
|
package/dist/types.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Types - memory-river
|
|
3
|
+
* Merged from v4/types.ts + context-river/concentrator.ts
|
|
4
|
+
*/
|
|
5
|
+
import * as os from 'node:os';
|
|
1
6
|
export const MEMORY_CATEGORIES = [
|
|
2
7
|
"preference",
|
|
3
8
|
"fact",
|
|
@@ -55,8 +60,11 @@ export const DEFAULT_CONFIG = {
|
|
|
55
60
|
decayPerRun: 5,
|
|
56
61
|
decayIntervalMs: 24 * 60 * 60 * 1000,
|
|
57
62
|
deleteThreshold: 0,
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
// 與上面 cleanupEngine 及 store-v4 的 DEFAULT_HEALTH_CONFIG 保持一致。
|
|
64
|
+
// 這三處曾經各寫各的(0.75 / 0.8 / 0.85,category 也是 6 / 5 / 4 類),
|
|
65
|
+
// 「記憶要多重要才不被淘汰」會因為呼叫入口不同而得到不同答案。改一處要改三處。
|
|
66
|
+
coreCategories: ["identity", "preference", "constraint", "business", "decision", "core_rule"],
|
|
67
|
+
coreImportanceThreshold: 0.75,
|
|
60
68
|
skillDecayFactor: 0.25,
|
|
61
69
|
},
|
|
62
70
|
hooks: {
|
|
@@ -73,13 +81,13 @@ export const DEFAULT_CONFIG = {
|
|
|
73
81
|
},
|
|
74
82
|
concentration: {
|
|
75
83
|
model: 'gemini-2.5-flash-lite',
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
* (Gemini → DeepSeek),不受 config 影響。
|
|
79
|
-
* 保留欄位以避免破壞既有宿主設定,請勿依賴其值。
|
|
80
|
-
*/
|
|
81
|
-
provider: 'gemini',
|
|
84
|
+
/** Provider chain starts with Codex when this is 'codex'; old values retain their legacy chain. */
|
|
85
|
+
provider: 'codex',
|
|
82
86
|
geminiApiKey: "",
|
|
87
|
+
codexModel: 'gpt-5.6-luna',
|
|
88
|
+
codexReasoningEffort: 'low',
|
|
89
|
+
codexWorkdir: os.homedir(),
|
|
90
|
+
codexTimeoutMs: 120000,
|
|
83
91
|
maxTokens: 8192,
|
|
84
92
|
deepseekApiKey: "",
|
|
85
93
|
deepseekModel: 'deepseek-v4-flash',
|