@memory-river/core 0.2.0 → 0.2.2
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 +22 -2
- package/dist/distill/concentrator-adapter.js +161 -9
- 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';
|
|
@@ -88,6 +89,21 @@ export declare function logProbeTextMismatchDetail(detail: ProbeTextMismatchDeta
|
|
|
88
89
|
export declare function buildDualTrackPrompt(conversationLog: string, capsuleLanguage?: string): string;
|
|
89
90
|
export declare function buildGeneralConversationPrompt(conversationLog: string, capsuleLanguage?: string): string;
|
|
90
91
|
export declare function buildSimplePrompt(conversationLog: string, capsuleLanguage?: string): string;
|
|
92
|
+
export type CodexReasoningEffort = 'none' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
93
|
+
export interface CodexCliConfig {
|
|
94
|
+
model: string;
|
|
95
|
+
reasoningEffort: CodexReasoningEffort;
|
|
96
|
+
workdir: string;
|
|
97
|
+
timeoutMs: number;
|
|
98
|
+
}
|
|
99
|
+
type ExecFileCallback = (error: NodeJS.ErrnoException | null, stdout: string | Buffer, stderr: string | Buffer) => void;
|
|
100
|
+
type ExecFileInvoker = (file: string, args: string[], options: {
|
|
101
|
+
cwd: string;
|
|
102
|
+
timeout: number;
|
|
103
|
+
}, callback: ExecFileCallback) => Pick<ChildProcess, 'kill' | 'stdin'> | undefined;
|
|
104
|
+
export declare function buildCodexExecArgs(prompt: string, config: CodexCliConfig): string[];
|
|
105
|
+
export declare function parseCodexOutput(stdout: string): unknown;
|
|
106
|
+
export declare function runCodexCli(prompt: string, config: CodexCliConfig, execFileImpl?: ExecFileInvoker): Promise<string>;
|
|
91
107
|
export interface ConcentratorConfig {
|
|
92
108
|
apiKey: string;
|
|
93
109
|
model: string;
|
|
@@ -101,7 +117,11 @@ export interface ConcentratorConfig {
|
|
|
101
117
|
*/
|
|
102
118
|
capsuleLanguage?: string;
|
|
103
119
|
concentrationTarget?: number;
|
|
104
|
-
provider?: 'gemini' | 'deepseek';
|
|
120
|
+
provider?: 'codex' | 'gemini' | 'deepseek';
|
|
121
|
+
codexModel?: string;
|
|
122
|
+
codexReasoningEffort?: CodexReasoningEffort;
|
|
123
|
+
codexWorkdir?: string;
|
|
124
|
+
codexTimeoutMs?: number;
|
|
105
125
|
maxTokens?: number;
|
|
106
126
|
deepseekApiKey?: string;
|
|
107
127
|
deepseekModel?: string;
|
|
@@ -142,7 +162,7 @@ export declare class ConcentratorAdapter implements LlmClient {
|
|
|
142
162
|
};
|
|
143
163
|
/**
|
|
144
164
|
* Provider 輪替 fallback 核心方法
|
|
145
|
-
* 依序嘗試 gemini → deepseek,任一成功即返回
|
|
165
|
+
* 依序嘗試 codex → gemini → deepseek,任一成功即返回
|
|
146
166
|
* Gemini 若連續 3 次 503,冷卻 90 秒內直接跳過
|
|
147
167
|
*/
|
|
148
168
|
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([
|
|
@@ -1076,14 +1078,123 @@ function classifyConcentratorFailure(err) {
|
|
|
1076
1078
|
return 'quota';
|
|
1077
1079
|
return 'other';
|
|
1078
1080
|
}
|
|
1081
|
+
const CODEX_FAILURE_BREAKER_THRESHOLD = 3;
|
|
1082
|
+
const CODEX_FAILURE_COOLDOWN_MS = 90_000;
|
|
1079
1083
|
const GEMINI_503_BREAKER_THRESHOLD = 3;
|
|
1080
1084
|
const GEMINI_503_COOLDOWN_MS = 90_000;
|
|
1085
|
+
let codexConsecutiveFailureCount = 0;
|
|
1086
|
+
let codexCooldownUntil = 0;
|
|
1081
1087
|
let geminiConsecutive503Count = 0;
|
|
1082
1088
|
let geminiCooldownUntil = 0;
|
|
1083
1089
|
function isGemini503Error(err) {
|
|
1084
1090
|
const message = String(err?.message ?? err ?? '').toLowerCase();
|
|
1085
1091
|
return message.includes('gemini api error: 503');
|
|
1086
1092
|
}
|
|
1093
|
+
export function buildCodexExecArgs(prompt, config) {
|
|
1094
|
+
return [
|
|
1095
|
+
'exec',
|
|
1096
|
+
'-C', config.workdir,
|
|
1097
|
+
'-c', `model=${config.model}`,
|
|
1098
|
+
'-c', `model_reasoning_effort=${config.reasoningEffort}`,
|
|
1099
|
+
prompt,
|
|
1100
|
+
];
|
|
1101
|
+
}
|
|
1102
|
+
function findBalancedJsonCandidates(text) {
|
|
1103
|
+
const stack = [];
|
|
1104
|
+
const candidates = [];
|
|
1105
|
+
let inString = false;
|
|
1106
|
+
let escaped = false;
|
|
1107
|
+
for (let i = 0; i < text.length; i++) {
|
|
1108
|
+
const ch = text[i];
|
|
1109
|
+
if (escaped) {
|
|
1110
|
+
escaped = false;
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
if (inString && ch === '\\') {
|
|
1114
|
+
escaped = true;
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if (ch === '"') {
|
|
1118
|
+
inString = !inString;
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
if (inString)
|
|
1122
|
+
continue;
|
|
1123
|
+
if (ch === '[' || ch === '{') {
|
|
1124
|
+
stack.push({ opener: ch, start: i });
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
if (ch !== ']' && ch !== '}')
|
|
1128
|
+
continue;
|
|
1129
|
+
const expected = ch === ']' ? '[' : '{';
|
|
1130
|
+
const top = stack[stack.length - 1];
|
|
1131
|
+
if (!top || top.opener !== expected)
|
|
1132
|
+
continue;
|
|
1133
|
+
stack.pop();
|
|
1134
|
+
candidates.push(text.slice(top.start, i + 1));
|
|
1135
|
+
}
|
|
1136
|
+
return candidates;
|
|
1137
|
+
}
|
|
1138
|
+
export function parseCodexOutput(stdout) {
|
|
1139
|
+
const trimmed = stdout.trim();
|
|
1140
|
+
if (!trimmed)
|
|
1141
|
+
throw new Error('Codex CLI returned empty stdout');
|
|
1142
|
+
try {
|
|
1143
|
+
const parsed = JSON.parse(trimmed);
|
|
1144
|
+
if (parsed && typeof parsed === 'object')
|
|
1145
|
+
return parsed;
|
|
1146
|
+
}
|
|
1147
|
+
catch { }
|
|
1148
|
+
const candidates = findBalancedJsonCandidates(stdout);
|
|
1149
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
1150
|
+
try {
|
|
1151
|
+
const parsed = JSON.parse(candidates[i]);
|
|
1152
|
+
if (parsed && typeof parsed === 'object')
|
|
1153
|
+
return parsed;
|
|
1154
|
+
}
|
|
1155
|
+
catch { }
|
|
1156
|
+
}
|
|
1157
|
+
throw new Error('Codex CLI stdout did not contain parseable JSON');
|
|
1158
|
+
}
|
|
1159
|
+
export function runCodexCli(prompt, config, execFileImpl = execFile) {
|
|
1160
|
+
const args = buildCodexExecArgs(prompt, config);
|
|
1161
|
+
return new Promise((resolve, reject) => {
|
|
1162
|
+
let settled = false;
|
|
1163
|
+
let timedOut = false;
|
|
1164
|
+
let child;
|
|
1165
|
+
const timer = setTimeout(() => {
|
|
1166
|
+
timedOut = true;
|
|
1167
|
+
child?.kill('SIGTERM');
|
|
1168
|
+
reject(new Error(`Codex CLI timed out after ${config.timeoutMs}ms`));
|
|
1169
|
+
}, config.timeoutMs);
|
|
1170
|
+
const finish = (error, stdout, stderr) => {
|
|
1171
|
+
if (settled)
|
|
1172
|
+
return;
|
|
1173
|
+
settled = true;
|
|
1174
|
+
clearTimeout(timer);
|
|
1175
|
+
if (timedOut)
|
|
1176
|
+
return;
|
|
1177
|
+
if (error) {
|
|
1178
|
+
const detail = String(stderr || error.message || error);
|
|
1179
|
+
reject(new Error(`Codex CLI failed: ${detail}`));
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
try {
|
|
1183
|
+
resolve(JSON.stringify(parseCodexOutput(String(stdout))));
|
|
1184
|
+
}
|
|
1185
|
+
catch (parseError) {
|
|
1186
|
+
reject(parseError);
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
child = execFileImpl('codex', args, {
|
|
1190
|
+
cwd: config.workdir,
|
|
1191
|
+
timeout: config.timeoutMs,
|
|
1192
|
+
}, finish);
|
|
1193
|
+
// execFile 不吃 stdio 選項,child 的 stdin 會是一個開著的 pipe,
|
|
1194
|
+
// codex CLI 因此停在 "Reading additional input from stdin" 直到 timeout。
|
|
1195
|
+
child?.stdin?.end();
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1087
1198
|
function extractBalancedObjectForKey(text, key) {
|
|
1088
1199
|
const keyIndex = text.indexOf(`"${key}"`);
|
|
1089
1200
|
if (keyIndex === -1)
|
|
@@ -1298,6 +1409,10 @@ export class ConcentratorAdapter {
|
|
|
1298
1409
|
capsuleLanguage: config.capsuleLanguage ?? '繁體中文',
|
|
1299
1410
|
concentrationTarget: config.concentrationTarget ?? 0,
|
|
1300
1411
|
provider: config.provider ?? 'gemini',
|
|
1412
|
+
codexModel: config.codexModel ?? 'gpt-5.6-luna',
|
|
1413
|
+
codexReasoningEffort: config.codexReasoningEffort ?? 'low',
|
|
1414
|
+
codexWorkdir: config.codexWorkdir || os.homedir(),
|
|
1415
|
+
codexTimeoutMs: config.codexTimeoutMs ?? 120000,
|
|
1301
1416
|
maxTokens: config.maxTokens ?? 8192,
|
|
1302
1417
|
deepseekApiKey: config.deepseekApiKey || '',
|
|
1303
1418
|
deepseekModel: config.deepseekModel ?? 'deepseek-v4-flash',
|
|
@@ -1388,7 +1503,7 @@ export class ConcentratorAdapter {
|
|
|
1388
1503
|
if (!needsCut) {
|
|
1389
1504
|
return { messages, wasConcentrated: false, processedThroughIndex: 0 };
|
|
1390
1505
|
}
|
|
1391
|
-
if (!this.llm && !this.config.apiKey && !this.config.deepseekApiKey) {
|
|
1506
|
+
if (!this.llm && this.config.provider !== 'codex' && !this.config.apiKey && !this.config.deepseekApiKey) {
|
|
1392
1507
|
console.warn('[ConcentratorAdapter] Concentration skipped: no LLM API key configured; raw transcripts and recall remain available.');
|
|
1393
1508
|
return { messages, wasConcentrated: false, processedThroughIndex: 0 };
|
|
1394
1509
|
}
|
|
@@ -1683,7 +1798,7 @@ export class ConcentratorAdapter {
|
|
|
1683
1798
|
}
|
|
1684
1799
|
/**
|
|
1685
1800
|
* Provider 輪替 fallback 核心方法
|
|
1686
|
-
* 依序嘗試 gemini → deepseek,任一成功即返回
|
|
1801
|
+
* 依序嘗試 codex → gemini → deepseek,任一成功即返回
|
|
1687
1802
|
* Gemini 若連續 3 次 503,冷卻 90 秒內直接跳過
|
|
1688
1803
|
*/
|
|
1689
1804
|
async callWithFallback(prompt, fnName = 'generate', fallbackPrompt, // concentrate 失敗時由呼叫端接 deterministic capsule;此處不處理
|
|
@@ -1691,22 +1806,44 @@ export class ConcentratorAdapter {
|
|
|
1691
1806
|
if (this.llm) {
|
|
1692
1807
|
return this.llm.generate(prompt, { purpose: fnName, maxTokens });
|
|
1693
1808
|
}
|
|
1694
|
-
const providers =
|
|
1809
|
+
const providers = this.config.provider === 'codex'
|
|
1810
|
+
? ['codex', 'gemini', 'deepseek']
|
|
1811
|
+
: ['gemini', 'deepseek'];
|
|
1695
1812
|
const now = Date.now();
|
|
1813
|
+
if (providers.includes('codex')) {
|
|
1814
|
+
if (now < codexCooldownUntil) {
|
|
1815
|
+
console.warn(`[${fnName}] codex skipped: circuit breaker cooling down for ${Math.ceil((codexCooldownUntil - now) / 1000)}s`);
|
|
1816
|
+
}
|
|
1817
|
+
else {
|
|
1818
|
+
// codex has no API key; authentication belongs to the local CLI.
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
const eligibleProviders = providers.filter((provider) => provider !== 'codex' || now >= codexCooldownUntil);
|
|
1696
1822
|
if (now < geminiCooldownUntil) {
|
|
1697
1823
|
console.warn(`[${fnName}] gemini skipped: circuit breaker cooling down for ${Math.ceil((geminiCooldownUntil - now) / 1000)}s`);
|
|
1698
1824
|
}
|
|
1699
|
-
|
|
1700
|
-
providers.push('gemini');
|
|
1701
|
-
}
|
|
1702
|
-
providers.push('deepseek');
|
|
1825
|
+
const orderedProviders = eligibleProviders.filter((provider) => provider !== 'gemini' || now >= geminiCooldownUntil);
|
|
1703
1826
|
const attemptedProviders = [];
|
|
1704
1827
|
const startedAt = Date.now();
|
|
1705
1828
|
const shouldRecordMetric = fnName === 'concentrate';
|
|
1706
1829
|
let lastError = null;
|
|
1707
|
-
for (const provider of
|
|
1830
|
+
for (const provider of orderedProviders) {
|
|
1708
1831
|
attemptedProviders.push(provider);
|
|
1709
1832
|
try {
|
|
1833
|
+
if (provider === 'codex') {
|
|
1834
|
+
const result = await this.callProvider(provider, prompt, maxTokens);
|
|
1835
|
+
codexConsecutiveFailureCount = 0;
|
|
1836
|
+
await this.recordConcentratorAttemptMetric({
|
|
1837
|
+
metricContext,
|
|
1838
|
+
provider,
|
|
1839
|
+
outcome: 'success',
|
|
1840
|
+
attemptedProviders,
|
|
1841
|
+
inputTokens: metricContext?.inputTokens ?? estimatePromptTokens(prompt),
|
|
1842
|
+
outputTokens: estimatePromptTokens(result),
|
|
1843
|
+
durationMs: Date.now() - startedAt,
|
|
1844
|
+
}, shouldRecordMetric);
|
|
1845
|
+
return result;
|
|
1846
|
+
}
|
|
1710
1847
|
if (provider === 'gemini') {
|
|
1711
1848
|
if (this.config.apiKey) {
|
|
1712
1849
|
const result = await this.callProvider(provider, prompt, maxTokens);
|
|
@@ -1745,7 +1882,14 @@ export class ConcentratorAdapter {
|
|
|
1745
1882
|
}
|
|
1746
1883
|
catch (err) {
|
|
1747
1884
|
console.warn(`[${fnName}] ${provider} failed; trying next provider:`, err);
|
|
1748
|
-
if (provider === '
|
|
1885
|
+
if (provider === 'codex') {
|
|
1886
|
+
codexConsecutiveFailureCount += 1;
|
|
1887
|
+
if (codexConsecutiveFailureCount >= CODEX_FAILURE_BREAKER_THRESHOLD) {
|
|
1888
|
+
codexCooldownUntil = Date.now() + CODEX_FAILURE_COOLDOWN_MS;
|
|
1889
|
+
console.warn(`[${fnName}] codex circuit breaker opened for ${CODEX_FAILURE_COOLDOWN_MS / 1000}s after ${codexConsecutiveFailureCount} consecutive failures`);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
else if (provider === 'gemini') {
|
|
1749
1893
|
if (isGemini503Error(err)) {
|
|
1750
1894
|
geminiConsecutive503Count += 1;
|
|
1751
1895
|
if (geminiConsecutive503Count >= GEMINI_503_BREAKER_THRESHOLD) {
|
|
@@ -1776,6 +1920,14 @@ export class ConcentratorAdapter {
|
|
|
1776
1920
|
if (provider === 'gemini') {
|
|
1777
1921
|
return callGeminiAPI(this.config.apiKey, this.config.model, prompt, maxTokens);
|
|
1778
1922
|
}
|
|
1923
|
+
if (provider === 'codex') {
|
|
1924
|
+
return runCodexCli(prompt, {
|
|
1925
|
+
model: this.config.codexModel,
|
|
1926
|
+
reasoningEffort: this.config.codexReasoningEffort,
|
|
1927
|
+
workdir: this.config.codexWorkdir,
|
|
1928
|
+
timeoutMs: this.config.codexTimeoutMs,
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1779
1931
|
if (provider === 'deepseek') {
|
|
1780
1932
|
return callDeepSeekAPI(this.config.deepseekApiKey, this.config.deepseekModel, prompt, maxTokens);
|
|
1781
1933
|
}
|
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',
|