memorix 1.7.2 → 1.8.0
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/CHANGELOG.md +59 -1
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/assets/star-history-dark.svg +4 -4
- package/assets/star-history-light.svg +4 -4
- package/assets/star-history.json +47 -3
- package/dist/cli/index.js +1801 -254
- package/dist/dashboard/static/app.js +88 -4
- package/dist/dashboard/static/index.html +10 -0
- package/dist/dashboard/static/style.css +86 -0
- package/dist/index.js +1241 -224
- package/dist/maintenance-runner.js +972 -126
- package/dist/media-audio-runner.js +967 -121
- package/dist/media-video-runner.js +972 -126
- package/dist/memcode-runtime/CHANGELOG.md +59 -1
- package/dist/sdk.js +1240 -223
- package/dist/types.d.ts +8 -0
- package/dist/vector-backfill-runner.js +972 -126
- package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +1 -1
- package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +75 -0
- package/docs/API_REFERENCE.md +37 -1
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CONFIGURATION.md +57 -1
- package/docs/DASHBOARD.md +10 -0
- package/docs/DEVELOPMENT.md +3 -2
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/PERFORMANCE.md +3 -1
- package/docs/README.md +11 -1
- package/docs/SETUP.md +2 -1
- package/package.json +1 -1
- package/plugins/antigravity/memorix/rules/memorix.md +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/memorix/SKILL.md +1 -1
- package/plugins/claude/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/copilot/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/cursor/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/gemini/memorix/GEMINI.md +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/plugins/pi/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/server.json +2 -2
- package/src/cli/capability-map.ts +2 -0
- package/src/cli/commands/config-migrate.ts +7 -0
- package/src/cli/commands/context.ts +11 -0
- package/src/cli/commands/doctor.ts +5 -1
- package/src/cli/commands/evidence.ts +56 -0
- package/src/cli/commands/feedback.ts +58 -0
- package/src/cli/commands/init.ts +17 -0
- package/src/cli/commands/resume.ts +2 -0
- package/src/cli/commands/serve-http.ts +201 -12
- package/src/cli/commands/status.ts +2 -0
- package/src/cli/fallback-budget.ts +59 -0
- package/src/cli/index.ts +5 -1
- package/src/compact/engine.ts +26 -1
- package/src/config/dotenv-loader.ts +17 -0
- package/src/config/resolved-config.ts +110 -12
- package/src/config/toml-loader.ts +8 -0
- package/src/config/yaml-loader.ts +9 -0
- package/src/dashboard/config-provenance.ts +17 -0
- package/src/dashboard/evidence.ts +63 -0
- package/src/dashboard/server.ts +93 -68
- package/src/dashboard/static/index.html +10 -0
- package/src/dashboard/static/style.css +86 -0
- package/src/hooks/installers/index.ts +1 -1
- package/src/hooks/official-skills.ts +3 -3
- package/src/hooks/rules/memorix-agent-rules.md +1 -1
- package/src/llm/provider.ts +33 -7
- package/src/memory/feedback-model.ts +4 -0
- package/src/memory/feedback.ts +220 -0
- package/src/memory/observations.ts +19 -0
- package/src/memory/retention.ts +38 -5
- package/src/multimodal/image-loader.ts +6 -3
- package/src/rerank/http-provider.ts +125 -0
- package/src/rerank/index.ts +81 -0
- package/src/rerank/policy.ts +88 -0
- package/src/server/mcp-binding-store.ts +79 -0
- package/src/server/mcp-diagnostics.ts +40 -0
- package/src/server/tool-profile.ts +4 -0
- package/src/server.ts +121 -1
- package/src/store/evidence-store.ts +261 -0
- package/src/store/orama-store.ts +79 -53
- package/src/store/sqlite-db.ts +104 -0
- package/src/types.ts +8 -0
|
@@ -92,6 +92,15 @@
|
|
|
92
92
|
</svg>
|
|
93
93
|
<span class="nav-label">Observations</span>
|
|
94
94
|
</button>
|
|
95
|
+
<button class="nav-btn" data-page="evidence" title="Evidence" aria-label="Evidence">
|
|
96
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
97
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
|
98
|
+
<polyline points="14 2 14 8 20 8"/>
|
|
99
|
+
<line x1="8" y1="13" x2="16" y2="13"/>
|
|
100
|
+
<line x1="8" y1="17" x2="14" y2="17"/>
|
|
101
|
+
</svg>
|
|
102
|
+
<span class="nav-label">Evidence</span>
|
|
103
|
+
</button>
|
|
95
104
|
</div>
|
|
96
105
|
<div class="sidebar-section-label" data-section="health">HEALTH</div>
|
|
97
106
|
<div class="sidebar-nav">
|
|
@@ -175,6 +184,7 @@
|
|
|
175
184
|
<div id="page-knowledge" class="page"></div>
|
|
176
185
|
<div id="page-graph" class="page"></div>
|
|
177
186
|
<div id="page-observations" class="page"></div>
|
|
187
|
+
<div id="page-evidence" class="page"></div>
|
|
178
188
|
<div id="page-retention" class="page"></div>
|
|
179
189
|
<div id="page-config" class="page"></div>
|
|
180
190
|
<div id="page-identity" class="page"></div>
|
|
@@ -64,6 +64,92 @@
|
|
|
64
64
|
--transition-emphasized: 500ms cubic-bezier(0.2, 0, 0, 1);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/* Evidence Console: provenance is readable at a glance and stays bounded on
|
|
68
|
+
narrow screens; long paths and generated titles never widen the viewport. */
|
|
69
|
+
.evidence-console {
|
|
70
|
+
display: grid;
|
|
71
|
+
gap: 12px;
|
|
72
|
+
min-width: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.evidence-card {
|
|
76
|
+
border: 1px solid var(--border-color, rgba(148, 163, 184, .18));
|
|
77
|
+
border-radius: 8px;
|
|
78
|
+
background: var(--surface, rgba(15, 23, 42, .45));
|
|
79
|
+
padding: 16px;
|
|
80
|
+
min-width: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.evidence-card-header {
|
|
84
|
+
display: flex;
|
|
85
|
+
gap: 12px;
|
|
86
|
+
align-items: flex-start;
|
|
87
|
+
justify-content: space-between;
|
|
88
|
+
min-width: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.evidence-card-heading {
|
|
92
|
+
min-width: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.evidence-card-heading h2 {
|
|
96
|
+
margin: 4px 0 0;
|
|
97
|
+
font-size: 16px;
|
|
98
|
+
line-height: 1.35;
|
|
99
|
+
overflow-wrap: anywhere;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.evidence-card-kind {
|
|
103
|
+
color: var(--text-muted);
|
|
104
|
+
font-size: 11px;
|
|
105
|
+
text-transform: uppercase;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.evidence-card-state {
|
|
109
|
+
display: flex;
|
|
110
|
+
gap: 6px;
|
|
111
|
+
flex: 0 0 auto;
|
|
112
|
+
flex-wrap: wrap;
|
|
113
|
+
justify-content: flex-end;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.evidence-state {
|
|
117
|
+
border: 1px solid currentColor;
|
|
118
|
+
border-radius: 999px;
|
|
119
|
+
padding: 3px 7px;
|
|
120
|
+
font-size: 11px;
|
|
121
|
+
white-space: nowrap;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.evidence-current, .evidence-verified { color: #4ade80; }
|
|
125
|
+
.evidence-stale, .evidence-conflicted { color: #fb923c; }
|
|
126
|
+
.evidence-unverified { color: var(--text-muted); }
|
|
127
|
+
|
|
128
|
+
.evidence-card-summary {
|
|
129
|
+
margin: 12px 0;
|
|
130
|
+
color: var(--text-secondary, var(--text-muted));
|
|
131
|
+
line-height: 1.55;
|
|
132
|
+
overflow-wrap: anywhere;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.evidence-card-meta {
|
|
136
|
+
display: grid;
|
|
137
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
138
|
+
gap: 10px;
|
|
139
|
+
margin: 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.evidence-card-meta div { min-width: 0; }
|
|
143
|
+
.evidence-card-meta dt { color: var(--text-muted); font-size: 11px; margin-bottom: 3px; }
|
|
144
|
+
.evidence-card-meta dd { margin: 0; font-size: 12px; overflow-wrap: anywhere; }
|
|
145
|
+
.evidence-card-warning { margin-top: 10px; color: #fb923c; font-size: 12px; overflow-wrap: anywhere; }
|
|
146
|
+
|
|
147
|
+
@media (max-width: 600px) {
|
|
148
|
+
.evidence-card-header { display: block; }
|
|
149
|
+
.evidence-card-state { justify-content: flex-start; margin-top: 10px; }
|
|
150
|
+
.evidence-card-meta { grid-template-columns: 1fr; }
|
|
151
|
+
}
|
|
152
|
+
|
|
67
153
|
/* Light Theme - MD3 */
|
|
68
154
|
[data-theme="light"] {
|
|
69
155
|
--bg-base: #FFFBFE;
|
|
@@ -1259,7 +1259,7 @@ function getAgentRulesContent(agent?: AgentName, scope: 'project' | 'global' = '
|
|
|
1259
1259
|
'',
|
|
1260
1260
|
`Default first step for non-trivial coding work: call \x60memorix_project_context\x60 with the user's actual task before progress files, dev-log reads, ad-hoc file reads, or git archaeology. Memorix will choose a task-lensed brief (bugfix, feature, release, onboarding, refactor, docs, test, or general). When the task is continuing prior work, the same brief also includes a bounded prior-work projection. Treat its "Start here" files as the first ${contextNoun} files to inspect.`,
|
|
1261
1261
|
'',
|
|
1262
|
-
'If the MCP tool is not visible yet but the client supports tool discovery or dynamic loading, search/select \x60memorix_project_context\x60 first. Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run exactly one CLI brief with the user\'s real task before inspecting files, Git history, progress notes, or guessing: \x60memorix resume "<task>" --brief-json\x60. For a new task, use \x60memorix context "<task>" --brief-json\x60 instead. The absence of \x60.memorix\x60 or visible memory files never proves project memory is empty. Use \x60--json\x60 only when a diagnostic needs the detailed legacy payload. If that one command fails, report it and proceed normally. Do not probe help, enumerate commands, chain broad searches, wait indefinitely on MCP startup, or hand-write tool-call syntax.',
|
|
1262
|
+
'If the MCP tool is not visible yet but the client supports tool discovery or dynamic loading, search/select \x60memorix_project_context\x60 first. Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run exactly one CLI brief with the user\'s real task before inspecting files, Git history, progress notes, or guessing: \x60memorix resume "<task>" --fallback --brief-json\x60. For a new task, use \x60memorix context "<task>" --fallback --brief-json\x60 instead. The absence of \x60.memorix\x60 or visible memory files never proves project memory is empty. Use \x60--json\x60 only when a diagnostic needs the detailed legacy payload. If that one command fails, report it and proceed normally. Do not probe help, enumerate commands, chain broad searches, wait indefinitely on MCP startup, or hand-write tool-call syntax.',
|
|
1263
1263
|
'',
|
|
1264
1264
|
'After a successful \x60memorix_project_context\x60 result, the brief is the default retrieval boundary. Do not call more Memorix retrieval tools after a complete brief. Use \x60memorix_context_pack\x60, \x60memorix_search\x60, or \x60memorix_detail\x60 only when the brief lacks a specific reference, freshness field, or fact needed for the task, or when the user explicitly asks for deeper history. In MCP, name that missing fact in \x60purpose\x60 when intentionally expanding beyond the brief. Do not retrieve the same decision twice just to confirm an already-complete brief.',
|
|
1265
1265
|
'If the user asks for read-only work or says not to modify files, do not call \x60memorix_store\x60 just to record an assessment. Store only when the user explicitly asks to preserve it.',
|
|
@@ -31,8 +31,8 @@ export const OFFICIAL_MEMORIX_SKILLS: OfficialMemorixSkill[] = [
|
|
|
31
31
|
'',
|
|
32
32
|
'| Situation | Prefer | CLI fallback |',
|
|
33
33
|
'|---|---|---|',
|
|
34
|
-
'| Starting a coding task and needing the best task-lensed project map | `memorix_project_context` with the user\'s task | `memorix context "<task>" --brief-json` |',
|
|
35
|
-
'| Continuing prior project work | `memorix_project_context` with the user\'s task | `memorix resume "<task>" --brief-json` |',
|
|
34
|
+
'| Starting a coding task and needing the best task-lensed project map | `memorix_project_context` with the user\'s task | `memorix context "<task>" --fallback --brief-json` |',
|
|
35
|
+
'| Continuing prior project work | `memorix_project_context` with the user\'s task | `memorix resume "<task>" --fallback --brief-json` |',
|
|
36
36
|
'| Need structured refs/freshness for code-bound memories | `memorix_context_pack` | `memorix codegraph context-pack --task "<topic>"` |',
|
|
37
37
|
'| Explicit memory graph question | `memorix_graph_context` | `memorix memory graph-context --query "<topic>"` |',
|
|
38
38
|
'| Specific past decision, bug, file, or change | `memorix_search` | `memorix memory search --query "<topic>"` |',
|
|
@@ -45,7 +45,7 @@ export const OFFICIAL_MEMORIX_SKILLS: OfficialMemorixSkill[] = [
|
|
|
45
45
|
'',
|
|
46
46
|
'- Default first step for non-trivial coding work: call `memorix_project_context` before progress files, dev-log reads, ad-hoc file reads, or git archaeology. Pass the user\'s actual task so Memorix can choose a bugfix, feature, release, onboarding, refactor, docs, test, or general brief. Start by reading its suggested files.',
|
|
47
47
|
'- In Claude Code headless/print-mode, an initial MCP `pending` status is not a failure by itself. Prefer MCP first: use the client\'s tool discovery/dynamic loading to find `memorix_project_context` when it is not in the first visible tool list.',
|
|
48
|
-
'- Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run `memorix resume "<task>" --brief-json` exactly once before inspecting files, Git history, progress notes, or guessing. The absence of `.memorix` or visible memory files never proves project memory is empty. For a new task, use `memorix context "<task>" --brief-json` instead. Use `--json` only when a diagnostic needs the detailed legacy payload. If that one command fails, report it and proceed normally; do not probe help, enumerate commands, or chain broad searches.',
|
|
48
|
+
'- Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run `memorix resume "<task>" --fallback --brief-json` exactly once before inspecting files, Git history, progress notes, or guessing. The absence of `.memorix` or visible memory files never proves project memory is empty. For a new task, use `memorix context "<task>" --fallback --brief-json` instead. Use `--json` only when a diagnostic needs the detailed legacy payload. If that one command fails, report it and proceed normally; do not probe help, enumerate commands, or chain broad searches.',
|
|
49
49
|
'- After a successful `memorix_project_context`, the brief is the default retrieval boundary. Do not call more Memorix retrieval tools after a complete brief. Use Context Pack, search, or detail only when the brief lacks a specific reference, freshness field, or fact needed for the task, or when the user explicitly asks for deeper history. In MCP, name that missing fact in `purpose` when intentionally expanding beyond the brief. Do not retrieve the same decision twice just to confirm an already-complete brief.',
|
|
50
50
|
'- If the user asks for read-only work or says not to modify files, do not call `memorix_store` just to record an assessment. Store only when the user explicitly asks to preserve it.',
|
|
51
51
|
'- Use `memorix_search` for a specific decision, bug, file, or prior change that the project context did not answer. Fetch detail only for a result you still need after the brief.',
|
|
@@ -7,7 +7,7 @@ You have access to Memorix memory tools. Follow these rules to maintain persiste
|
|
|
7
7
|
At the beginning of non-trivial coding work, use Memorix Memory Autopilot before local progress notes or broad file exploration. A session bind is not required for every conversation.
|
|
8
8
|
|
|
9
9
|
1. Default first step for non-trivial coding work: call `memorix_project_context` with the user's actual task before progress files, dev-log reads, ad-hoc file reads, or git archaeology. Memorix will choose a task-lensed brief (bugfix, feature, release, onboarding, refactor, docs, test, or general). Treat its "Start here" files as the first code or docs to inspect.
|
|
10
|
-
2. If the MCP tool is not visible yet but the client supports tool discovery or dynamic loading, search/select `memorix_project_context` first. Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run exactly one CLI brief with the user's real task before inspecting files, Git history, progress notes, or guessing: use `memorix resume "<task>" --brief-json`. For a new task, use `memorix context "<task>" --brief-json` instead. The absence of `.memorix` or visible memory files never proves project memory is empty. If that one command fails, report it and proceed normally. Do not probe help, enumerate commands, chain broad searches, wait indefinitely on MCP startup, or hand-write tool-call syntax. Use `--json` only when a diagnostic needs the detailed legacy payload.
|
|
10
|
+
2. If the MCP tool is not visible yet but the client supports tool discovery or dynamic loading, search/select `memorix_project_context` first. Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run exactly one CLI brief with the user's real task before inspecting files, Git history, progress notes, or guessing: use `memorix resume "<task>" --fallback --brief-json`. For a new task, use `memorix context "<task>" --fallback --brief-json` instead. The absence of `.memorix` or visible memory files never proves project memory is empty. If that one command fails, report it and proceed normally. Do not probe help, enumerate commands, chain broad searches, wait indefinitely on MCP startup, or hand-write tool-call syntax. Use `--json` only when a diagnostic needs the detailed legacy payload.
|
|
11
11
|
3. After a successful `memorix_project_context` result, the brief is the default retrieval boundary. Do not call more Memorix retrieval tools after a complete brief. Use `memorix_context_pack`, `memorix_search`, or `memorix_detail` only when the brief lacks a specific reference, freshness field, or fact needed for the task, or when the user explicitly asks for deeper history. In MCP, name that missing fact in `purpose` when intentionally expanding beyond the brief. Do not retrieve the same decision twice just to confirm an already-complete brief.
|
|
12
12
|
If the user asks for read-only work or says not to modify files, do not call `memorix_store` just to record an assessment. Store only when the user explicitly asks to preserve it.
|
|
13
13
|
4. Use `memorix_context_pack` when you need structured refs and freshness for code-bound memories.
|
package/src/llm/provider.ts
CHANGED
|
@@ -31,6 +31,36 @@ const LLM_TIMEOUT_MIN_MS = 1_000;
|
|
|
31
31
|
const LLM_TIMEOUT_MAX_MS = 300_000;
|
|
32
32
|
const MAX_LLM_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
33
33
|
|
|
34
|
+
const OPENAI_API_VERSION_SEGMENT = /^v\d+(?:[a-z][a-z0-9-]*)?$/i;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Normalize an OpenAI-compatible API root without rewriting an explicit
|
|
38
|
+
* provider version such as `/api/v3` or `/v1beta`.
|
|
39
|
+
*
|
|
40
|
+
* Bare hosts keep the historical convenience of receiving `/v1`. Once a
|
|
41
|
+
* caller supplies a versioned path, that path is authoritative.
|
|
42
|
+
*/
|
|
43
|
+
export function normalizeOpenAICompatibleBaseUrl(baseUrl: string): string {
|
|
44
|
+
const base = baseUrl.trim().replace(/\/+$/, '');
|
|
45
|
+
if (!base) return base;
|
|
46
|
+
|
|
47
|
+
let pathname = '';
|
|
48
|
+
try {
|
|
49
|
+
pathname = new URL(base).pathname.replace(/\/+$/, '');
|
|
50
|
+
} catch {
|
|
51
|
+
pathname = base.split(/[?#]/, 1)[0].replace(/\/+$/, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lastSegment = pathname.split('/').filter(Boolean).at(-1);
|
|
55
|
+
if (lastSegment && OPENAI_API_VERSION_SEGMENT.test(lastSegment)) {
|
|
56
|
+
return base;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const queryOrHash = base.match(/[?#].*$/)?.[0] ?? '';
|
|
60
|
+
const pathWithoutQuery = base.slice(0, base.length - queryOrHash.length).replace(/\/+$/, '');
|
|
61
|
+
return `${pathWithoutQuery}/v1${queryOrHash}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
34
64
|
/**
|
|
35
65
|
* Parse and validate MEMORIX_LLM_TIMEOUT_MS environment variable.
|
|
36
66
|
* - Must be a valid integer in the range 1000–300000ms.
|
|
@@ -259,9 +289,7 @@ async function callOpenAICompatible(
|
|
|
259
289
|
signal?: AbortSignal,
|
|
260
290
|
): Promise<LLMResponse> {
|
|
261
291
|
const config = currentConfig!;
|
|
262
|
-
|
|
263
|
-
let base = config.baseUrl!.replace(/\/+$/, '');
|
|
264
|
-
if (!base.endsWith('/v1')) base += '/v1';
|
|
292
|
+
const base = normalizeOpenAICompatibleBaseUrl(config.baseUrl!);
|
|
265
293
|
const url = `${base}/chat/completions`;
|
|
266
294
|
|
|
267
295
|
const fetchSignal = signal
|
|
@@ -387,8 +415,7 @@ async function callOpenAIWithTools(
|
|
|
387
415
|
signal?: AbortSignal,
|
|
388
416
|
): Promise<LLMToolResponse> {
|
|
389
417
|
const config = currentConfig!;
|
|
390
|
-
|
|
391
|
-
if (!base.endsWith('/v1')) base += '/v1';
|
|
418
|
+
const base = normalizeOpenAICompatibleBaseUrl(config.baseUrl!);
|
|
392
419
|
const url = `${base}/chat/completions`;
|
|
393
420
|
|
|
394
421
|
// Convert ChatMessage[] to OpenAI format
|
|
@@ -603,8 +630,7 @@ async function* callOpenAIWithToolsStream(
|
|
|
603
630
|
tools: ToolDefinition[],
|
|
604
631
|
): AsyncGenerator<LLMStreamEvent, void, undefined> {
|
|
605
632
|
const config = currentConfig!;
|
|
606
|
-
|
|
607
|
-
if (!base.endsWith('/v1')) base += '/v1';
|
|
633
|
+
const base = normalizeOpenAICompatibleBaseUrl(config.baseUrl!);
|
|
608
634
|
const url = `${base}/chat/completions`;
|
|
609
635
|
|
|
610
636
|
const openaiMessages: Array<Record<string, unknown>> = messages.map((msg) => {
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { ObservationStatus } from '../types.js';
|
|
3
|
+
import { getDatabase } from '../store/sqlite-db.js';
|
|
4
|
+
export { feedbackWeightMultiplier } from './feedback-model.js';
|
|
5
|
+
|
|
6
|
+
export type MemoryFeedbackSignal =
|
|
7
|
+
| 'user-correction'
|
|
8
|
+
| 'verification-success'
|
|
9
|
+
| 'verification-failure'
|
|
10
|
+
| 'code-change'
|
|
11
|
+
| 'used'
|
|
12
|
+
| 'not-used'
|
|
13
|
+
| 'code-conflict'
|
|
14
|
+
| 'strengthen'
|
|
15
|
+
| 'weaken'
|
|
16
|
+
| 'revoke';
|
|
17
|
+
|
|
18
|
+
export interface MemoryFeedbackEvent {
|
|
19
|
+
signal: MemoryFeedbackSignal;
|
|
20
|
+
at: string;
|
|
21
|
+
actor?: string;
|
|
22
|
+
note?: string;
|
|
23
|
+
sourceRef?: string;
|
|
24
|
+
targetEventId?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FeedbackState {
|
|
28
|
+
weight: number;
|
|
29
|
+
status: ObservationStatus;
|
|
30
|
+
audit: MemoryFeedbackEvent[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type FeedbackCandidateKind = 'observation' | 'claim' | 'durable-memory' | 'workflow';
|
|
34
|
+
|
|
35
|
+
export interface PersistedFeedbackEvent extends MemoryFeedbackEvent {
|
|
36
|
+
id: string;
|
|
37
|
+
projectId: string;
|
|
38
|
+
candidateKind: FeedbackCandidateKind;
|
|
39
|
+
candidateId: string;
|
|
40
|
+
delta: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const FEEDBACK_DELTAS: Record<Exclude<MemoryFeedbackSignal, 'revoke'>, number> = {
|
|
44
|
+
'user-correction': -0.35,
|
|
45
|
+
'verification-success': 0.25,
|
|
46
|
+
'verification-failure': -0.25,
|
|
47
|
+
'code-change': -0.15,
|
|
48
|
+
used: 0.1,
|
|
49
|
+
'not-used': -0.05,
|
|
50
|
+
'code-conflict': -0.4,
|
|
51
|
+
strengthen: 0.2,
|
|
52
|
+
weaken: -0.2,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Deterministic feedback reducer used by MCP/CLI adapters and tests. */
|
|
56
|
+
export function applyMemoryFeedback(
|
|
57
|
+
current: FeedbackState,
|
|
58
|
+
event: MemoryFeedbackEvent,
|
|
59
|
+
): FeedbackState {
|
|
60
|
+
const delta = event.signal === 'revoke' ? 0 : FEEDBACK_DELTAS[event.signal];
|
|
61
|
+
const weight = Math.max(0, Math.min(2, current.weight + delta));
|
|
62
|
+
const status: ObservationStatus = (event.signal === 'code-conflict' || event.signal === 'user-correction' || event.signal === 'weaken') && weight < 0.5
|
|
63
|
+
? 'archived'
|
|
64
|
+
: current.status;
|
|
65
|
+
return { weight, status, audit: [...current.audit, { ...event }] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function rowToEvent(row: any): PersistedFeedbackEvent {
|
|
69
|
+
return {
|
|
70
|
+
id: String(row.id),
|
|
71
|
+
projectId: String(row.project_id),
|
|
72
|
+
candidateKind: row.candidate_kind,
|
|
73
|
+
candidateId: String(row.candidate_id),
|
|
74
|
+
signal: row.signal,
|
|
75
|
+
...(row.observed_at ? { at: String(row.observed_at) } : { at: new Date().toISOString() }),
|
|
76
|
+
...(row.actor ? { actor: String(row.actor) } : {}),
|
|
77
|
+
...(row.note ? { note: String(row.note) } : {}),
|
|
78
|
+
...(row.source_ref ? { sourceRef: String(row.source_ref) } : {}),
|
|
79
|
+
...(row.target_event_id ? { targetEventId: String(row.target_event_id) } : {}),
|
|
80
|
+
delta: Number(row.delta ?? 0),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function candidateKey(projectId: string, candidateKind: FeedbackCandidateKind, candidateId: string): string {
|
|
85
|
+
return `${projectId}\u0000${candidateKind}\u0000${candidateId}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Durable feedback loop. Events are append-only; the state row is a materialized
|
|
90
|
+
* projection rebuilt from non-revoked events, so undo never destroys audit truth.
|
|
91
|
+
*/
|
|
92
|
+
export class MemoryFeedbackStore {
|
|
93
|
+
private db: any = null;
|
|
94
|
+
|
|
95
|
+
async init(dataDir: string): Promise<void> {
|
|
96
|
+
this.db = getDatabase(dataDir);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private requireDb(): any {
|
|
100
|
+
if (!this.db) throw new Error('MemoryFeedbackStore is not initialized.');
|
|
101
|
+
return this.db;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private loadEvents(projectId: string, candidateKind: FeedbackCandidateKind, candidateId: string): PersistedFeedbackEvent[] {
|
|
105
|
+
return this.requireDb().prepare(`
|
|
106
|
+
SELECT * FROM memory_feedback_events
|
|
107
|
+
WHERE project_id = ? AND candidate_kind = ? AND candidate_id = ?
|
|
108
|
+
ORDER BY observed_at ASC, rowid ASC
|
|
109
|
+
`).all(projectId, candidateKind, candidateId).map(rowToEvent);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private rebuild(projectId: string, candidateKind: FeedbackCandidateKind, candidateId: string): FeedbackState {
|
|
113
|
+
const events = this.loadEvents(projectId, candidateKind, candidateId);
|
|
114
|
+
const revoked = new Set(events.filter((event) => event.signal === 'revoke' && event.targetEventId).map((event) => event.targetEventId!));
|
|
115
|
+
let state: FeedbackState = { weight: 1, status: 'active', audit: [] };
|
|
116
|
+
for (const event of events) {
|
|
117
|
+
if (event.signal === 'revoke' || revoked.has(event.id)) {
|
|
118
|
+
state.audit.push(event);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
state = applyMemoryFeedback(state, event);
|
|
122
|
+
}
|
|
123
|
+
this.requireDb().prepare(`
|
|
124
|
+
INSERT INTO memory_feedback_states (project_id, candidate_kind, candidate_id, weight, lifecycle_status, updated_at)
|
|
125
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
126
|
+
ON CONFLICT(project_id, candidate_kind, candidate_id) DO UPDATE SET
|
|
127
|
+
weight = excluded.weight, lifecycle_status = excluded.lifecycle_status, updated_at = excluded.updated_at
|
|
128
|
+
`).run(projectId, candidateKind, candidateId, state.weight, state.status, new Date().toISOString());
|
|
129
|
+
return state;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getState(projectId: string, candidateKind: FeedbackCandidateKind, candidateId: string): FeedbackState {
|
|
133
|
+
const row = this.requireDb().prepare(`
|
|
134
|
+
SELECT weight, lifecycle_status FROM memory_feedback_states
|
|
135
|
+
WHERE project_id = ? AND candidate_kind = ? AND candidate_id = ?
|
|
136
|
+
`).get(projectId, candidateKind, candidateId);
|
|
137
|
+
if (!row) return { weight: 1, status: 'active', audit: [] };
|
|
138
|
+
return {
|
|
139
|
+
weight: Number(row.weight ?? 1),
|
|
140
|
+
status: row.lifecycle_status ?? 'active',
|
|
141
|
+
audit: this.loadEvents(projectId, candidateKind, candidateId),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
listStates(projectId: string, candidateKind: FeedbackCandidateKind = 'observation'): Array<{ candidateId: string; weight: number; status: ObservationStatus; updatedAt: string }> {
|
|
146
|
+
return this.requireDb().prepare(`
|
|
147
|
+
SELECT candidate_id, weight, lifecycle_status, updated_at
|
|
148
|
+
FROM memory_feedback_states WHERE project_id = ? AND candidate_kind = ?
|
|
149
|
+
ORDER BY updated_at DESC
|
|
150
|
+
`).all(projectId, candidateKind).map((row: any) => ({
|
|
151
|
+
candidateId: String(row.candidate_id),
|
|
152
|
+
weight: Number(row.weight),
|
|
153
|
+
status: row.lifecycle_status,
|
|
154
|
+
updatedAt: String(row.updated_at),
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
weights(projectId: string, candidateKind: FeedbackCandidateKind = 'observation'): Map<string, FeedbackState> {
|
|
159
|
+
const states = new Map<string, FeedbackState>();
|
|
160
|
+
for (const row of this.listStates(projectId, candidateKind)) {
|
|
161
|
+
states.set(row.candidateId, this.getState(projectId, candidateKind, row.candidateId));
|
|
162
|
+
}
|
|
163
|
+
return states;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
record(input: {
|
|
167
|
+
projectId: string;
|
|
168
|
+
candidateKind: FeedbackCandidateKind;
|
|
169
|
+
candidateId: string;
|
|
170
|
+
signal: MemoryFeedbackSignal;
|
|
171
|
+
sourceRef: string;
|
|
172
|
+
actor?: string;
|
|
173
|
+
note?: string;
|
|
174
|
+
targetEventId?: string;
|
|
175
|
+
at?: string;
|
|
176
|
+
}): { event: PersistedFeedbackEvent; state: FeedbackState } {
|
|
177
|
+
if (input.signal === 'revoke' && !input.targetEventId) {
|
|
178
|
+
throw new Error('A revoke feedback event requires targetEventId.');
|
|
179
|
+
}
|
|
180
|
+
if (input.signal === 'revoke') {
|
|
181
|
+
const target = this.requireDb().prepare(`
|
|
182
|
+
SELECT 1 FROM memory_feedback_events
|
|
183
|
+
WHERE id = ? AND project_id = ? AND candidate_kind = ? AND candidate_id = ?
|
|
184
|
+
`).get(input.targetEventId, input.projectId, input.candidateKind, input.candidateId);
|
|
185
|
+
if (!target) throw new Error('The feedback event to revoke was not found in the same candidate scope.');
|
|
186
|
+
}
|
|
187
|
+
const event: PersistedFeedbackEvent = {
|
|
188
|
+
id: randomUUID(),
|
|
189
|
+
projectId: input.projectId,
|
|
190
|
+
candidateKind: input.candidateKind,
|
|
191
|
+
candidateId: input.candidateId,
|
|
192
|
+
signal: input.signal,
|
|
193
|
+
sourceRef: input.sourceRef,
|
|
194
|
+
...(input.actor ? { actor: input.actor } : {}),
|
|
195
|
+
...(input.note ? { note: input.note.slice(0, 2_000) } : {}),
|
|
196
|
+
...(input.targetEventId ? { targetEventId: input.targetEventId } : {}),
|
|
197
|
+
at: input.at ?? new Date().toISOString(),
|
|
198
|
+
delta: input.signal === 'revoke' ? 0 : FEEDBACK_DELTAS[input.signal],
|
|
199
|
+
};
|
|
200
|
+
this.requireDb().prepare(`
|
|
201
|
+
INSERT INTO memory_feedback_events (
|
|
202
|
+
id, project_id, candidate_kind, candidate_id, signal, actor, note,
|
|
203
|
+
source_ref, target_event_id, delta, observed_at
|
|
204
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
205
|
+
`).run(
|
|
206
|
+
event.id, event.projectId, event.candidateKind, event.candidateId, event.signal,
|
|
207
|
+
event.actor ?? null, event.note ?? null, event.sourceRef, event.targetEventId ?? null,
|
|
208
|
+
event.delta, event.at,
|
|
209
|
+
);
|
|
210
|
+
return { event, state: this.rebuild(input.projectId, input.candidateKind, input.candidateId) };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
audit(projectId: string, candidateKind: FeedbackCandidateKind, candidateId: string, limit = 100): PersistedFeedbackEvent[] {
|
|
214
|
+
return this.requireDb().prepare(`
|
|
215
|
+
SELECT * FROM memory_feedback_events
|
|
216
|
+
WHERE project_id = ? AND candidate_kind = ? AND candidate_id = ?
|
|
217
|
+
ORDER BY observed_at DESC, rowid DESC LIMIT ?
|
|
218
|
+
`).all(projectId, candidateKind, candidateId, Math.min(200, Math.max(1, Math.floor(limit)))).map(rowToEvent);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -44,6 +44,7 @@ import { getEmbeddingProvider, isEmbeddingExplicitlyDisabled, validateEmbeddingI
|
|
|
44
44
|
import { sanitizeCredentials } from './secret-filter.js';
|
|
45
45
|
import { enqueueClaimDerivation, enqueueObservationQualification } from '../runtime/lifecycle.js';
|
|
46
46
|
import { canManageObservation, resolveObservationVisibility } from './visibility.js';
|
|
47
|
+
import { EvidenceCardStore } from '../store/evidence-store.js';
|
|
47
48
|
|
|
48
49
|
/** In-memory observation list (loaded from persistence on init) */
|
|
49
50
|
let observations: Observation[] = [];
|
|
@@ -186,6 +187,20 @@ async function bindObservationCodeRefsBestEffort(observation: Observation): Prom
|
|
|
186
187
|
}
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
async function persistEvidenceCard(observation: Observation): Promise<void> {
|
|
191
|
+
if (!projectDir) return;
|
|
192
|
+
try {
|
|
193
|
+
const evidence = new EvidenceCardStore();
|
|
194
|
+
await evidence.init(projectDir);
|
|
195
|
+
evidence.upsertObservation(observation);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
// Evidence is a derived control-plane projection. A failed projection must
|
|
198
|
+
// never roll back the canonical observation, but it must be visible for a
|
|
199
|
+
// later repair/sync pass.
|
|
200
|
+
console.error(`[memorix] Evidence card projection failed for obs-${observation.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
189
204
|
function queueClaimDerivation(observation: Observation): void {
|
|
190
205
|
const dataDir = projectDir;
|
|
191
206
|
const isExplicit = observation.sourceDetail === 'explicit';
|
|
@@ -692,6 +707,7 @@ export async function storeObservation(input: {
|
|
|
692
707
|
if (!persistedCallbackInvoked) await input.onPersisted?.(observation);
|
|
693
708
|
|
|
694
709
|
await bindObservationCodeRefsBestEffort(observation);
|
|
710
|
+
await persistEvidenceCard(observation);
|
|
695
711
|
queueObservationQualification(observation);
|
|
696
712
|
if (resolveObservationVisibility(observation) === 'project') {
|
|
697
713
|
queueClaimDerivation(observation);
|
|
@@ -828,6 +844,7 @@ async function upsertObservation(
|
|
|
828
844
|
}
|
|
829
845
|
|
|
830
846
|
await bindObservationCodeRefsBestEffort(existing);
|
|
847
|
+
await persistEvidenceCard(existing);
|
|
831
848
|
queueObservationQualification(existing);
|
|
832
849
|
if (resolveObservationVisibility(existing) === 'project') {
|
|
833
850
|
queueClaimDerivation(existing);
|
|
@@ -1002,6 +1019,8 @@ export async function resolveObservations(
|
|
|
1002
1019
|
});
|
|
1003
1020
|
}
|
|
1004
1021
|
|
|
1022
|
+
for (const observation of changed) await persistEvidenceCard(observation);
|
|
1023
|
+
|
|
1005
1024
|
return { resolved, notFound };
|
|
1006
1025
|
}
|
|
1007
1026
|
|
package/src/memory/retention.ts
CHANGED
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
* retention period but are no longer permanently immune — they decay normally.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import type { MemorixDocument, Observation, ObservationReader } from '../types.js';
|
|
18
|
+
import type { MemorixDocument, Observation, ObservationReader, ObservationStatus } from '../types.js';
|
|
19
19
|
import { getObservationStore } from '../store/obs-store.js';
|
|
20
20
|
import { canManageObservation } from './visibility.js';
|
|
21
|
+
import { feedbackWeightMultiplier } from './feedback-model.js';
|
|
21
22
|
|
|
22
23
|
// ── Importance → Retention Period mapping ────────────────────────────
|
|
23
24
|
|
|
@@ -113,6 +114,7 @@ export function getEffectiveRetentionDays(doc: MemorixDocument): number {
|
|
|
113
114
|
export function isImmune(doc: MemorixDocument): boolean {
|
|
114
115
|
// Probe observations are operational heartbeats -- never immune, regardless of valueCategory or access.
|
|
115
116
|
if (doc.type === 'probe') return false;
|
|
117
|
+
if (doc.feedbackStatus === 'archived') return false;
|
|
116
118
|
|
|
117
119
|
// A candidate has not earned durable status. Never let an automatic core
|
|
118
120
|
// classification turn unqualified capture into permanently retained memory.
|
|
@@ -164,6 +166,7 @@ export interface RelevanceScore {
|
|
|
164
166
|
accessBoost: number;
|
|
165
167
|
ageDays: number;
|
|
166
168
|
isImmune: boolean;
|
|
169
|
+
feedbackMultiplier: number;
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
/**
|
|
@@ -202,7 +205,12 @@ export function calculateRelevance(
|
|
|
202
205
|
const accessCount = doc.accessCount ?? 0;
|
|
203
206
|
const accessBoost = Math.min(2.0, 1 + 0.1 * accessCount);
|
|
204
207
|
|
|
205
|
-
|
|
208
|
+
// Legacy memories have no feedback row. Preserve their historical score
|
|
209
|
+
// exactly; only an explicit feedback projection changes relevance.
|
|
210
|
+
const feedbackMultiplier = doc.feedbackWeight === undefined
|
|
211
|
+
? 1
|
|
212
|
+
: feedbackWeightMultiplier(doc.feedbackWeight);
|
|
213
|
+
let totalScore = base * decayFactor * accessBoost * feedbackMultiplier;
|
|
206
214
|
|
|
207
215
|
// Immune observations get minimum 0.5 relevance
|
|
208
216
|
const immune = isImmune(doc);
|
|
@@ -218,6 +226,7 @@ export function calculateRelevance(
|
|
|
218
226
|
accessBoost,
|
|
219
227
|
ageDays,
|
|
220
228
|
isImmune: immune,
|
|
229
|
+
feedbackMultiplier,
|
|
221
230
|
};
|
|
222
231
|
}
|
|
223
232
|
|
|
@@ -248,6 +257,7 @@ export type RetentionZone = 'active' | 'stale' | 'archive-candidate';
|
|
|
248
257
|
*/
|
|
249
258
|
export function getRetentionZone(doc: MemorixDocument, referenceTime?: Date): RetentionZone {
|
|
250
259
|
const now = referenceTime ?? new Date();
|
|
260
|
+
if (doc.feedbackStatus === 'archived') return 'archive-candidate';
|
|
251
261
|
const importance = getImportanceLevel(doc);
|
|
252
262
|
const retention = getEffectiveRetentionDays(doc);
|
|
253
263
|
|
|
@@ -344,6 +354,7 @@ export interface RetentionProjection extends RetentionExplanation {
|
|
|
344
354
|
export interface ObservationRetentionProjectionOptions {
|
|
345
355
|
referenceTime?: Date;
|
|
346
356
|
accessMap?: ObservationAccessMap;
|
|
357
|
+
feedback?: { weight: number; status: ObservationStatus };
|
|
347
358
|
}
|
|
348
359
|
|
|
349
360
|
export interface RetentionProjectionSummary {
|
|
@@ -415,6 +426,7 @@ export function explainRetention(
|
|
|
415
426
|
export function toRetentionDocument(
|
|
416
427
|
obs: Observation,
|
|
417
428
|
accessMap?: ObservationAccessMap,
|
|
429
|
+
feedback?: { weight: number; status: ObservationStatus },
|
|
418
430
|
): MemorixDocument {
|
|
419
431
|
const access = accessMap?.get(obs.id);
|
|
420
432
|
return {
|
|
@@ -438,6 +450,7 @@ export function toRetentionDocument(
|
|
|
438
450
|
valueCategory: obs.valueCategory ?? '',
|
|
439
451
|
admissionState: obs.admissionState ?? '',
|
|
440
452
|
admissionReason: obs.admissionReason ?? '',
|
|
453
|
+
...(feedback ? { feedbackWeight: feedback.weight, feedbackStatus: feedback.status } : {}),
|
|
441
454
|
};
|
|
442
455
|
}
|
|
443
456
|
|
|
@@ -446,7 +459,7 @@ export function projectObservationRetention(
|
|
|
446
459
|
observation: Observation,
|
|
447
460
|
options: ObservationRetentionProjectionOptions = {},
|
|
448
461
|
): RetentionProjection {
|
|
449
|
-
const document = toRetentionDocument(observation, options.accessMap);
|
|
462
|
+
const document = toRetentionDocument(observation, options.accessMap, options.feedback);
|
|
450
463
|
const relevance = calculateRelevance(document, options.referenceTime);
|
|
451
464
|
const explanation = explainRetention(document, options.referenceTime);
|
|
452
465
|
|
|
@@ -497,12 +510,25 @@ export interface ArchiveExpiredBatchResult {
|
|
|
497
510
|
nextCursor?: number;
|
|
498
511
|
}
|
|
499
512
|
|
|
513
|
+
async function loadFeedbackStore(dataDir: string): Promise<import('./feedback.js').MemoryFeedbackStore | undefined> {
|
|
514
|
+
try {
|
|
515
|
+
const { MemoryFeedbackStore } = await import('./feedback.js');
|
|
516
|
+
const store = new MemoryFeedbackStore();
|
|
517
|
+
await store.init(dataDir);
|
|
518
|
+
return store;
|
|
519
|
+
} catch {
|
|
520
|
+
// Pre-1.8 databases or degraded SQLite installations keep the legacy
|
|
521
|
+
// retention behavior until the feedback projection is available.
|
|
522
|
+
return undefined;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
500
526
|
/**
|
|
501
527
|
* Archive one bounded page of a project's active memories. The caller owns the
|
|
502
528
|
* cursor, which makes the operation safe to run from a durable maintenance job.
|
|
503
529
|
*/
|
|
504
530
|
export async function archiveExpiredBatch(
|
|
505
|
-
|
|
531
|
+
projectDir: string,
|
|
506
532
|
options: ArchiveExpiredBatchOptions,
|
|
507
533
|
): Promise<ArchiveExpiredBatchResult> {
|
|
508
534
|
const store = getObservationStore();
|
|
@@ -513,6 +539,7 @@ export async function archiveExpiredBatch(
|
|
|
513
539
|
afterId,
|
|
514
540
|
limit: limit + 1,
|
|
515
541
|
});
|
|
542
|
+
const feedback = await loadFeedbackStore(projectDir);
|
|
516
543
|
const hasMore = page.length > limit;
|
|
517
544
|
const scanned = hasMore ? page.slice(0, limit) : page;
|
|
518
545
|
const candidateIds = scanned
|
|
@@ -520,6 +547,7 @@ export async function archiveExpiredBatch(
|
|
|
520
547
|
.filter((observation) => projectObservationRetention(observation, {
|
|
521
548
|
accessMap: options.accessMap,
|
|
522
549
|
referenceTime: options.referenceTime,
|
|
550
|
+
...(feedback ? { feedback: feedback.getState(options.projectId, 'observation', String(observation.id)) } : {}),
|
|
523
551
|
}).zone === 'archive-candidate')
|
|
524
552
|
.map((observation) => observation.id);
|
|
525
553
|
|
|
@@ -580,6 +608,7 @@ export async function archiveExpired(
|
|
|
580
608
|
return { archived, remaining };
|
|
581
609
|
}
|
|
582
610
|
|
|
611
|
+
const feedback = await loadFeedbackStore(projectDir);
|
|
583
612
|
return await store.atomic(async (tx) => {
|
|
584
613
|
const allObs = await tx.loadAll();
|
|
585
614
|
|
|
@@ -591,7 +620,11 @@ export async function archiveExpired(
|
|
|
591
620
|
const archivedIds: number[] = [];
|
|
592
621
|
|
|
593
622
|
for (const obs of activeObs) {
|
|
594
|
-
const retention = projectObservationRetention(obs, {
|
|
623
|
+
const retention = projectObservationRetention(obs, {
|
|
624
|
+
accessMap,
|
|
625
|
+
referenceTime,
|
|
626
|
+
...(feedback ? { feedback: feedback.getState(obs.projectId, 'observation', String(obs.id)) } : {}),
|
|
627
|
+
});
|
|
595
628
|
if (retention.zone === 'archive-candidate') {
|
|
596
629
|
archivedIds.push(obs.id);
|
|
597
630
|
}
|