@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.
@@ -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
- else {
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 providers) {
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 === 'gemini') {
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 || 'gemini',
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',
@@ -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>;
@@ -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
- jieba = module.default ?? module;
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.85,
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 isCore = effectiveCoreCategories.includes(memoryRow.category) ||
2710
- memoryRow.importance >= effectiveCoreImportanceThreshold;
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
- coreCategories: ["identity", "preference", "constraint", "business", "core_rule"],
59
- coreImportanceThreshold: 0.8,
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
- * @deprecated 此欄位已失效。實際 fallback 順序由 concentrator-adapter.ts 內定
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',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memory-river/core",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Framework-neutral long-term memory engine for Node.js agents.",
5
5
  "private": false,
6
6
  "license": "Apache-2.0",