memorix 1.5.0 → 1.6.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 +37 -0
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/dist/cli/index.js +452 -109
- package/dist/index.js +376 -87
- package/dist/maintenance-runner.js +174 -41
- package/dist/media-audio-runner.js +174 -41
- package/dist/media-video-runner.js +174 -41
- package/dist/memcode-runtime/CHANGELOG.md +37 -0
- package/dist/sdk.js +375 -86
- package/dist/types.d.ts +2 -0
- package/dist/vector-backfill-runner.js +174 -41
- package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +1 -1
- package/docs/API_REFERENCE.md +1 -1
- package/docs/DEVELOPMENT.md +5 -3
- package/package.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +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/server.json +2 -2
- package/src/cli/commands/codegraph.ts +48 -5
- package/src/cli/commands/context.ts +20 -4
- package/src/cli/commands/resume.ts +4 -0
- package/src/codegraph/auto-context.ts +4 -1
- package/src/codegraph/context-pack.ts +3 -1
- package/src/codegraph/external-provider.ts +93 -0
- package/src/embedding/api-provider.ts +105 -22
- package/src/embedding/provider.ts +2 -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/knowledge/context-receipt.ts +41 -0
- package/src/knowledge/workflows.ts +6 -3
- package/src/knowledge/workset.ts +56 -2
- package/src/llm/provider.ts +17 -8
- package/src/memory/formation/extract.ts +6 -3
- package/src/memory/formation/index.ts +2 -1
- package/src/memory/formation/resolve.ts +10 -6
- package/src/memory/formation/types.ts +3 -1
- package/src/memory/long-term.ts +7 -1
- package/src/server.ts +29 -13
- package/src/store/orama-store.ts +6 -4
- package/src/timeout.ts +67 -0
- package/src/types.ts +2 -0
package/src/knowledge/workset.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { WorkflowStore } from './workflow-store.js';
|
|
|
8
8
|
import { OutcomeStore } from './outcome-store.js';
|
|
9
9
|
import { qualityFromOutcome, type OutcomeQuality } from './outcome-types.js';
|
|
10
10
|
import { selectWorkflows } from './workflows.js';
|
|
11
|
+
import type { AgentTarget } from '../types.js';
|
|
11
12
|
import { renderLongTermMemorySummary, selectLongTermMemoriesForTask } from '../memory/long-term.js';
|
|
12
13
|
import { governContextCandidates, type GovernancePlan } from './governor.js';
|
|
13
14
|
import type {
|
|
@@ -73,6 +74,18 @@ export interface WorksetWorkflow {
|
|
|
73
74
|
cautions: string[];
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
/**
|
|
78
|
+
* A task-scoped plan assembled from existing workflow and wiki assets. It is
|
|
79
|
+
* descriptive rather than an authorization boundary: host agents retain their
|
|
80
|
+
* own tool and approval policies.
|
|
81
|
+
*/
|
|
82
|
+
export interface WorksetAgentLoadout {
|
|
83
|
+
agent: AgentTarget;
|
|
84
|
+
workflowIds: string[];
|
|
85
|
+
requiredContext: string[];
|
|
86
|
+
allowedTools: string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
export interface WorksetMemorySource {
|
|
77
90
|
id: number;
|
|
78
91
|
title: string;
|
|
@@ -156,6 +169,7 @@ export interface TaskWorkset {
|
|
|
156
169
|
pages: WorksetPage[];
|
|
157
170
|
durableMemory: WorksetLongTermMemory[];
|
|
158
171
|
workflows: WorksetWorkflow[];
|
|
172
|
+
agentLoadout?: WorksetAgentLoadout;
|
|
159
173
|
cautions: WorksetCaution[];
|
|
160
174
|
verification: string[];
|
|
161
175
|
evidenceIds: string[];
|
|
@@ -179,6 +193,8 @@ export interface BuildTaskWorksetInput {
|
|
|
179
193
|
projectId: string;
|
|
180
194
|
dataDir: string;
|
|
181
195
|
task?: string;
|
|
196
|
+
/** Optional target used to select workflows explicitly compatible with this agent. */
|
|
197
|
+
agent?: AgentTarget;
|
|
182
198
|
lens: string;
|
|
183
199
|
currentFacts?: string[];
|
|
184
200
|
continuation?: WorksetContinuation;
|
|
@@ -313,6 +329,18 @@ function workflowOutput(selection: WorkflowSelection): WorksetWorkflow {
|
|
|
313
329
|
};
|
|
314
330
|
}
|
|
315
331
|
|
|
332
|
+
function workflowLoadout(agent: AgentTarget | undefined, selections: WorkflowSelection[]): WorksetAgentLoadout | undefined {
|
|
333
|
+
if (!agent) return undefined;
|
|
334
|
+
return {
|
|
335
|
+
agent,
|
|
336
|
+
workflowIds: selections.map(selection => selection.workflow.id),
|
|
337
|
+
requiredContext: unique(selections.flatMap(selection => selection.workflow.requiredContext))
|
|
338
|
+
.map(item => short(item, 18)).slice(0, 4),
|
|
339
|
+
allowedTools: unique(selections.flatMap(selection => selection.workflow.allowedTools))
|
|
340
|
+
.map(item => short(item, 12)).slice(0, 6),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
316
344
|
function appendLine(
|
|
317
345
|
lines: string[],
|
|
318
346
|
candidate: string,
|
|
@@ -853,6 +881,23 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
|
|
|
853
881
|
}
|
|
854
882
|
}
|
|
855
883
|
|
|
884
|
+
if (input.agentLoadout) {
|
|
885
|
+
appendLine(lines, '', maxTokens, omitted, 'agent-loadout-heading');
|
|
886
|
+
appendLine(lines, 'Agent loadout (' + input.agentLoadout.agent + ')', maxTokens, omitted, 'agent-loadout-heading');
|
|
887
|
+
for (const required of input.agentLoadout.requiredContext.slice(0, 2)) {
|
|
888
|
+
appendLine(lines, '- Need: ' + required, maxTokens, omitted, 'agent-loadout-context');
|
|
889
|
+
}
|
|
890
|
+
if (input.agentLoadout.allowedTools.length > 0) {
|
|
891
|
+
appendLine(
|
|
892
|
+
lines,
|
|
893
|
+
'- Workflow tools: ' + input.agentLoadout.allowedTools.join(', '),
|
|
894
|
+
maxTokens,
|
|
895
|
+
omitted,
|
|
896
|
+
'agent-loadout-tools',
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
856
901
|
if (input.verification.length > 0) {
|
|
857
902
|
appendLine(lines, '', maxTokens, omitted, 'verification-heading');
|
|
858
903
|
appendLine(lines, 'Verify', maxTokens, omitted, 'verification-heading');
|
|
@@ -965,6 +1010,7 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
|
|
|
965
1010
|
let workspace: KnowledgeWorkspace | undefined;
|
|
966
1011
|
let pages: WorksetPage[] = [];
|
|
967
1012
|
let workflows: WorksetWorkflow[] = [];
|
|
1013
|
+
let workflowSelections: WorkflowSelection[] = [];
|
|
968
1014
|
try {
|
|
969
1015
|
workspace = await preferredWorkspace(input.projectId, input.dataDir);
|
|
970
1016
|
if (workspace) {
|
|
@@ -985,14 +1031,15 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
|
|
|
985
1031
|
if (task) {
|
|
986
1032
|
const workflowStore = new WorkflowStore();
|
|
987
1033
|
await workflowStore.init(input.dataDir);
|
|
988
|
-
|
|
1034
|
+
workflowSelections = selectWorkflows({
|
|
989
1035
|
workflows: workflowStore.listWorkflows(workspace.id, 'active'),
|
|
990
1036
|
task,
|
|
991
1037
|
projectId: input.projectId,
|
|
992
1038
|
store: workflowStore,
|
|
1039
|
+
...(input.agent ? { agent: input.agent } : {}),
|
|
993
1040
|
limit: 2,
|
|
994
1041
|
});
|
|
995
|
-
workflows =
|
|
1042
|
+
workflows = workflowSelections.map(workflowOutput);
|
|
996
1043
|
for (const workflow of workflows) {
|
|
997
1044
|
for (const caution of workflow.cautions) {
|
|
998
1045
|
cautions.push({ kind: 'workflow-failed-verification', message: caution });
|
|
@@ -1018,6 +1065,10 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
|
|
|
1018
1065
|
workflows,
|
|
1019
1066
|
});
|
|
1020
1067
|
const governedDurableIds = new Set(governed.durableMemory.map(memory => memory.id));
|
|
1068
|
+
const agentLoadout = workflowLoadout(
|
|
1069
|
+
input.agent,
|
|
1070
|
+
workflowSelections.filter(selection => governed.workflows.some(workflow => workflow.id === selection.workflow.id)),
|
|
1071
|
+
);
|
|
1021
1072
|
const evidenceIds = unique(governed.claims.flatMap(claim => evidenceIdsForClaim(
|
|
1022
1073
|
claim,
|
|
1023
1074
|
evidenceByClaim.get(claim.id) ?? [],
|
|
@@ -1084,6 +1135,9 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
|
|
|
1084
1135
|
pages: pages.filter(page => page.claimIds.some(claimId => governed.claims.some(claim => claim.id === claimId))),
|
|
1085
1136
|
durableMemory: governed.durableMemory,
|
|
1086
1137
|
workflows: governed.workflows,
|
|
1138
|
+
...(agentLoadout
|
|
1139
|
+
? { agentLoadout }
|
|
1140
|
+
: {}),
|
|
1087
1141
|
cautions: normalizedCautions,
|
|
1088
1142
|
verification,
|
|
1089
1143
|
evidenceIds,
|
package/src/llm/provider.ts
CHANGED
|
@@ -237,16 +237,17 @@ export function setLLMConfig(config: LLMConfig | null): void {
|
|
|
237
237
|
export async function callLLM(
|
|
238
238
|
systemPrompt: string,
|
|
239
239
|
userMessage: string,
|
|
240
|
+
signal?: AbortSignal,
|
|
240
241
|
): Promise<LLMResponse> {
|
|
241
242
|
if (!currentConfig) {
|
|
242
243
|
throw new Error('LLM not configured. Set MEMORIX_LLM_API_KEY or OPENAI_API_KEY.');
|
|
243
244
|
}
|
|
244
245
|
|
|
245
246
|
if (currentConfig.provider === 'anthropic') {
|
|
246
|
-
return callAnthropic(systemPrompt, userMessage);
|
|
247
|
+
return callAnthropic(systemPrompt, userMessage, signal);
|
|
247
248
|
}
|
|
248
249
|
|
|
249
|
-
return callOpenAICompatible(systemPrompt, userMessage);
|
|
250
|
+
return callOpenAICompatible(systemPrompt, userMessage, signal);
|
|
250
251
|
}
|
|
251
252
|
|
|
252
253
|
/**
|
|
@@ -255,6 +256,7 @@ export async function callLLM(
|
|
|
255
256
|
async function callOpenAICompatible(
|
|
256
257
|
systemPrompt: string,
|
|
257
258
|
userMessage: string,
|
|
259
|
+
signal?: AbortSignal,
|
|
258
260
|
): Promise<LLMResponse> {
|
|
259
261
|
const config = currentConfig!;
|
|
260
262
|
// Auto-fix: append /v1 if baseUrl doesn't end with it (common user mistake)
|
|
@@ -262,8 +264,11 @@ async function callOpenAICompatible(
|
|
|
262
264
|
if (!base.endsWith('/v1')) base += '/v1';
|
|
263
265
|
const url = `${base}/chat/completions`;
|
|
264
266
|
|
|
267
|
+
const fetchSignal = signal
|
|
268
|
+
? AbortSignal.any([signal, AbortSignal.timeout(LLM_CALL_TIMEOUT_MS)])
|
|
269
|
+
: AbortSignal.timeout(LLM_CALL_TIMEOUT_MS);
|
|
265
270
|
const response = await fetch(url, {
|
|
266
|
-
signal:
|
|
271
|
+
signal: fetchSignal,
|
|
267
272
|
method: 'POST',
|
|
268
273
|
headers: {
|
|
269
274
|
'Content-Type': 'application/json',
|
|
@@ -281,14 +286,14 @@ async function callOpenAICompatible(
|
|
|
281
286
|
});
|
|
282
287
|
|
|
283
288
|
if (!response.ok) {
|
|
284
|
-
const error = await readResponseText(response,
|
|
289
|
+
const error = await readResponseText(response, signal, 64 * 1024).catch(() => 'unknown error');
|
|
285
290
|
throw new Error(`LLM API error (${response.status}): ${error}`);
|
|
286
291
|
}
|
|
287
292
|
|
|
288
293
|
const data = await readResponseJson<{
|
|
289
294
|
choices: Array<{ message: { content: string } }>;
|
|
290
295
|
usage?: { prompt_tokens: number; completion_tokens: number };
|
|
291
|
-
}>(response);
|
|
296
|
+
}>(response, signal);
|
|
292
297
|
|
|
293
298
|
return {
|
|
294
299
|
content: data.choices[0]?.message?.content ?? '',
|
|
@@ -305,12 +310,16 @@ async function callOpenAICompatible(
|
|
|
305
310
|
async function callAnthropic(
|
|
306
311
|
systemPrompt: string,
|
|
307
312
|
userMessage: string,
|
|
313
|
+
signal?: AbortSignal,
|
|
308
314
|
): Promise<LLMResponse> {
|
|
309
315
|
const config = currentConfig!;
|
|
310
316
|
const url = `${config.baseUrl}/messages`;
|
|
311
317
|
|
|
318
|
+
const fetchSignal = signal
|
|
319
|
+
? AbortSignal.any([signal, AbortSignal.timeout(LLM_CALL_TIMEOUT_MS)])
|
|
320
|
+
: AbortSignal.timeout(LLM_CALL_TIMEOUT_MS);
|
|
312
321
|
const response = await fetch(url, {
|
|
313
|
-
signal:
|
|
322
|
+
signal: fetchSignal,
|
|
314
323
|
method: 'POST',
|
|
315
324
|
headers: {
|
|
316
325
|
'Content-Type': 'application/json',
|
|
@@ -329,14 +338,14 @@ async function callAnthropic(
|
|
|
329
338
|
});
|
|
330
339
|
|
|
331
340
|
if (!response.ok) {
|
|
332
|
-
const error = await readResponseText(response,
|
|
341
|
+
const error = await readResponseText(response, signal, 64 * 1024).catch(() => 'unknown error');
|
|
333
342
|
throw new Error(`Anthropic API error (${response.status}): ${error}`);
|
|
334
343
|
}
|
|
335
344
|
|
|
336
345
|
const data = await readResponseJson<{
|
|
337
346
|
content: Array<{ text: string }>;
|
|
338
347
|
usage?: { input_tokens: number; output_tokens: number };
|
|
339
|
-
}>(response);
|
|
348
|
+
}>(response, signal);
|
|
340
349
|
|
|
341
350
|
return {
|
|
342
351
|
content: data.content[0]?.text ?? '',
|
|
@@ -278,11 +278,12 @@ async function extractFactsWithLLM(
|
|
|
278
278
|
narrative: string,
|
|
279
279
|
title: string,
|
|
280
280
|
existingFacts: string[],
|
|
281
|
+
signal?: AbortSignal,
|
|
281
282
|
): Promise<string[]> {
|
|
282
283
|
try {
|
|
283
284
|
const { callLLM } = await import('../../llm/provider.js');
|
|
284
285
|
const input = `Title: ${title}\nContent: ${narrative}${existingFacts.length > 0 ? `\nAlready known facts (don't repeat): ${existingFacts.join('; ')}` : ''}`;
|
|
285
|
-
const response = await callLLM(LLM_EXTRACT_PROMPT, input);
|
|
286
|
+
const response = await callLLM(LLM_EXTRACT_PROMPT, input, signal);
|
|
286
287
|
const text = response.content.trim();
|
|
287
288
|
|
|
288
289
|
// Parse JSON response
|
|
@@ -298,7 +299,8 @@ async function extractFactsWithLLM(
|
|
|
298
299
|
.filter((f: unknown): f is string => typeof f === 'string' && f.length >= 5)
|
|
299
300
|
.filter((f: string) => !existingLower.has(f.toLowerCase().trim()))
|
|
300
301
|
.slice(0, 10);
|
|
301
|
-
} catch {
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (signal?.aborted) throw error;
|
|
302
304
|
return []; // LLM failure → fall back to rules
|
|
303
305
|
}
|
|
304
306
|
}
|
|
@@ -318,6 +320,7 @@ export async function runExtract(
|
|
|
318
320
|
input: FormationInput,
|
|
319
321
|
existingEntities: string[],
|
|
320
322
|
useLLM = false,
|
|
323
|
+
signal?: AbortSignal,
|
|
321
324
|
): Promise<ExtractResult> {
|
|
322
325
|
const callerFacts = input.facts ?? [];
|
|
323
326
|
|
|
@@ -325,7 +328,7 @@ export async function runExtract(
|
|
|
325
328
|
let extractedFacts: string[];
|
|
326
329
|
if (useLLM) {
|
|
327
330
|
// LLM extraction (quality-first, Mem0-style)
|
|
328
|
-
extractedFacts = await extractFactsWithLLM(input.narrative, input.title, callerFacts);
|
|
331
|
+
extractedFacts = await extractFactsWithLLM(input.narrative, input.title, callerFacts, signal);
|
|
329
332
|
// If LLM returned nothing, fall back to rules
|
|
330
333
|
if (extractedFacts.length === 0) {
|
|
331
334
|
extractedFacts = extractFacts(input.narrative, callerFacts);
|
|
@@ -293,7 +293,7 @@ export async function runFormation(
|
|
|
293
293
|
const existingEntities = config.getEntityNames();
|
|
294
294
|
const extractStartTime = Date.now();
|
|
295
295
|
emitStageEvent('extract', 'start');
|
|
296
|
-
const extraction = await runExtract(input, existingEntities, config.useLLM);
|
|
296
|
+
const extraction = await runExtract(input, existingEntities, config.useLLM, config.signal);
|
|
297
297
|
stageDurationsMs.extract = Date.now() - extractStartTime;
|
|
298
298
|
emitStageEvent('extract', 'success', stageDurationsMs.extract);
|
|
299
299
|
stagesCompleted = 1;
|
|
@@ -317,6 +317,7 @@ export async function runFormation(
|
|
|
317
317
|
config.searchMemories,
|
|
318
318
|
config.getObservation,
|
|
319
319
|
config.useLLM,
|
|
320
|
+
config.signal,
|
|
320
321
|
);
|
|
321
322
|
stageDurationsMs.resolve = Date.now() - resolveStartTime;
|
|
322
323
|
emitStageEvent('resolve', 'success', stageDurationsMs.resolve);
|
|
@@ -152,6 +152,7 @@ async function resolveWithLLM(
|
|
|
152
152
|
extracted: ExtractResult,
|
|
153
153
|
hits: SearchHit[],
|
|
154
154
|
getObservation: (id: number) => ExistingMemoryRef | null,
|
|
155
|
+
signal?: AbortSignal,
|
|
155
156
|
): Promise<ResolveResult | null> {
|
|
156
157
|
try {
|
|
157
158
|
const { callLLM } = await import('../../llm/provider.js');
|
|
@@ -173,7 +174,7 @@ Facts: ${extracted.facts.join('; ')}
|
|
|
173
174
|
EXISTING MEMORIES:
|
|
174
175
|
${existingMemories.map(m => `[ID:${m.id}] ${m.title} | ${m.content} | Facts: ${m.facts}`).join('\n')}`;
|
|
175
176
|
|
|
176
|
-
const response = await callLLM(LLM_RESOLVE_PROMPT, input);
|
|
177
|
+
const response = await callLLM(LLM_RESOLVE_PROMPT, input, signal);
|
|
177
178
|
const text = response.content.trim();
|
|
178
179
|
|
|
179
180
|
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
@@ -214,7 +215,8 @@ ${existingMemories.map(m => `[ID:${m.id}] ${m.title} | ${m.content} | Facts: ${m
|
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
return null; // Unrecognized action
|
|
217
|
-
} catch {
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (signal?.aborted) throw error;
|
|
218
220
|
return null; // LLM failure → fall back to rules
|
|
219
221
|
}
|
|
220
222
|
}
|
|
@@ -260,16 +262,18 @@ function scoreCandidate(
|
|
|
260
262
|
export async function runResolve(
|
|
261
263
|
extracted: ExtractResult,
|
|
262
264
|
projectId: string,
|
|
263
|
-
searchMemories: (query: string, limit: number, projectId: string) => Promise<SearchHit[]>,
|
|
265
|
+
searchMemories: (query: string, limit: number, projectId: string, signal?: AbortSignal) => Promise<SearchHit[]>,
|
|
264
266
|
getObservation: (id: number) => ExistingMemoryRef | null,
|
|
265
267
|
useLLM = false,
|
|
268
|
+
signal?: AbortSignal,
|
|
266
269
|
): Promise<ResolveResult> {
|
|
267
270
|
// Search for similar existing memories
|
|
268
271
|
const query = `${extracted.title} ${extracted.narrative.substring(0, 200)}`;
|
|
269
272
|
let hits: SearchHit[];
|
|
270
273
|
try {
|
|
271
|
-
hits = await searchMemories(query, 5, projectId);
|
|
272
|
-
} catch {
|
|
274
|
+
hits = await searchMemories(query, 5, projectId, signal);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (signal?.aborted) throw error;
|
|
273
277
|
// Search failed — default to ADD
|
|
274
278
|
return { action: 'new', reason: 'Search unavailable, defaulting to new' };
|
|
275
279
|
}
|
|
@@ -280,7 +284,7 @@ export async function runResolve(
|
|
|
280
284
|
|
|
281
285
|
// LLM-powered resolution (Mem0-style, quality-first)
|
|
282
286
|
if (useLLM) {
|
|
283
|
-
const llmResult = await resolveWithLLM(extracted, hits, getObservation);
|
|
287
|
+
const llmResult = await resolveWithLLM(extracted, hits, getObservation, signal);
|
|
284
288
|
if (llmResult) return llmResult;
|
|
285
289
|
// LLM failed → fall through to rules-based resolution
|
|
286
290
|
}
|
|
@@ -201,12 +201,14 @@ export interface FormationConfig {
|
|
|
201
201
|
shadow?: boolean;
|
|
202
202
|
/** Enable LLM-powered stages (requires LLM API key) */
|
|
203
203
|
useLLM: boolean;
|
|
204
|
+
/** Abort in-flight LLM work when the formation budget expires. */
|
|
205
|
+
signal?: AbortSignal;
|
|
204
206
|
/** Minimum value score to proceed with storage (default: 0.3) */
|
|
205
207
|
minValueScore: number;
|
|
206
208
|
/** Sampling rate for hooks path (0-1). 0 = always shadow, 1 = always full resolve */
|
|
207
209
|
hooksSamplingRate?: number;
|
|
208
210
|
/** Function to search existing memories (injected dependency) */
|
|
209
|
-
searchMemories: (query: string, limit: number, projectId: string) => Promise<SearchHit[]>;
|
|
211
|
+
searchMemories: (query: string, limit: number, projectId: string, signal?: AbortSignal) => Promise<SearchHit[]>;
|
|
210
212
|
/** Function to get observation by ID (injected dependency) */
|
|
211
213
|
getObservation: (id: number) => ExistingMemoryRef | null;
|
|
212
214
|
/** Function to list existing entity names (injected dependency) */
|
package/src/memory/long-term.ts
CHANGED
|
@@ -589,7 +589,13 @@ export async function maintainLongTermMemories(input: {
|
|
|
589
589
|
}
|
|
590
590
|
for (const group of groups.values()) {
|
|
591
591
|
if (group.length < 2) continue;
|
|
592
|
-
group.sort((a, b) =>
|
|
592
|
+
group.sort((a, b) => {
|
|
593
|
+
const byCreatedAt = Date.parse(b.createdAt) - Date.parse(a.createdAt);
|
|
594
|
+
if (byCreatedAt !== 0) return byCreatedAt;
|
|
595
|
+
const byUpdatedAt = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
|
|
596
|
+
if (byUpdatedAt !== 0) return byUpdatedAt;
|
|
597
|
+
return b.id.localeCompare(a.id);
|
|
598
|
+
});
|
|
593
599
|
const newest = group[0];
|
|
594
600
|
for (const older of group.slice(1)) {
|
|
595
601
|
try {
|
package/src/server.ts
CHANGED
|
@@ -50,7 +50,7 @@ import type { ExistingMemory } from './llm/memory-manager.js';
|
|
|
50
50
|
import { runFormation, getMetricsSummary, getBeforeAfterMetrics } from './memory/formation/index.js';
|
|
51
51
|
import type { FormationConfig, SearchHit, FormedMemory, FormationStage, FormationStageEvent } from './memory/formation/types.js';
|
|
52
52
|
import { parseFormationTimeoutMs } from './server/formation-timeout.js';
|
|
53
|
-
import { withTimeout } from './timeout.js';
|
|
53
|
+
import { withTimeout, withTimeoutSignal } from './timeout.js';
|
|
54
54
|
import { sanitizeCredentials } from './memory/secret-filter.js';
|
|
55
55
|
import { getSurfacedIds, recordSurfacedIds } from './search/surfaced-registry.js';
|
|
56
56
|
import {
|
|
@@ -828,8 +828,8 @@ export async function createMemorixServer(
|
|
|
828
828
|
mode: 'active',
|
|
829
829
|
useLLM: isLLMEnabled(),
|
|
830
830
|
minValueScore: 0.3,
|
|
831
|
-
searchMemories: async (q: string, limit: number, pid: string): Promise<SearchHit[]> => {
|
|
832
|
-
const result = await compactSearch({ query: q, limit, projectId: pid, status: 'active', reader });
|
|
831
|
+
searchMemories: async (q: string, limit: number, pid: string, signal?: AbortSignal): Promise<SearchHit[]> => {
|
|
832
|
+
const result = await compactSearch({ query: q, limit, projectId: pid, status: 'active', reader, signal });
|
|
833
833
|
if (result.entries.length === 0) return [];
|
|
834
834
|
const details = await compactDetail(result.entries.map(e => e.id), { reader });
|
|
835
835
|
return details.documents.map((d, i) => ({
|
|
@@ -860,8 +860,9 @@ export async function createMemorixServer(
|
|
|
860
860
|
onStageEvent: onFormationStageEvent,
|
|
861
861
|
};
|
|
862
862
|
|
|
863
|
-
formationResult = await
|
|
864
|
-
|
|
863
|
+
formationResult = await withTimeoutSignal(async (signal) => {
|
|
864
|
+
formationConfig.signal = signal;
|
|
865
|
+
return runFormation({
|
|
865
866
|
entityName,
|
|
866
867
|
type: type as ObservationType,
|
|
867
868
|
title,
|
|
@@ -869,7 +870,8 @@ export async function createMemorixServer(
|
|
|
869
870
|
facts: safeFacts,
|
|
870
871
|
projectId: project.id,
|
|
871
872
|
source: 'explicit',
|
|
872
|
-
}, formationConfig)
|
|
873
|
+
}, formationConfig);
|
|
874
|
+
},
|
|
873
875
|
FORMATION_TIMEOUT_MS,
|
|
874
876
|
'Formation pipeline',
|
|
875
877
|
);
|
|
@@ -1265,8 +1267,8 @@ export async function createMemorixServer(
|
|
|
1265
1267
|
mode: formationMode,
|
|
1266
1268
|
useLLM: isLLMEnabled(),
|
|
1267
1269
|
minValueScore: 0.3,
|
|
1268
|
-
searchMemories: async (q: string, limit: number, pid: string): Promise<SearchHit[]> => {
|
|
1269
|
-
const result = await compactSearch({ query: q, limit, projectId: pid, status: 'active', reader });
|
|
1270
|
+
searchMemories: async (q: string, limit: number, pid: string, signal?: AbortSignal): Promise<SearchHit[]> => {
|
|
1271
|
+
const result = await compactSearch({ query: q, limit, projectId: pid, status: 'active', reader, signal });
|
|
1270
1272
|
if (result.entries.length === 0) return [];
|
|
1271
1273
|
const details = await compactDetail(result.entries.map(e => e.id), { reader });
|
|
1272
1274
|
return details.documents.map((d, i) => ({
|
|
@@ -1288,8 +1290,10 @@ export async function createMemorixServer(
|
|
|
1288
1290
|
getEntityNames: () => graphManager.getEntityNames(),
|
|
1289
1291
|
};
|
|
1290
1292
|
|
|
1291
|
-
const formed = await
|
|
1292
|
-
|
|
1293
|
+
const formed = await withTimeoutSignal(async (signal) => {
|
|
1294
|
+
formationConfig.signal = signal;
|
|
1295
|
+
return runFormation({ entityName, type: type as ObservationType, title, narrative, facts: safeFacts, projectId: project.id, source: 'explicit', topicKey }, formationConfig);
|
|
1296
|
+
},
|
|
1293
1297
|
FORMATION_TIMEOUT_MS,
|
|
1294
1298
|
'Shadow formation',
|
|
1295
1299
|
);
|
|
@@ -1575,13 +1579,17 @@ export async function createMemorixServer(
|
|
|
1575
1579
|
refresh: z.enum(['auto', 'always', 'never']).optional().default('auto').describe(
|
|
1576
1580
|
'Code Memory refresh policy. auto refreshes only when missing or stale.',
|
|
1577
1581
|
),
|
|
1578
|
-
format: z.enum(['prompt', 'summary', 'json']).optional().default('prompt').describe(
|
|
1579
|
-
'Output format. "prompt" is agent-ready; "summary" is human-readable; "json" is
|
|
1582
|
+
format: z.enum(['prompt', 'summary', 'json', 'receipt']).optional().default('prompt').describe(
|
|
1583
|
+
'Output format. "prompt" is agent-ready; "summary" is human-readable; "receipt" is bounded JSON; "json" is detailed diagnostics.',
|
|
1580
1584
|
),
|
|
1585
|
+
agent: z.enum([
|
|
1586
|
+
'windsurf', 'cursor', 'claude-code', 'codex', 'copilot', 'antigravity', 'gemini-cli',
|
|
1587
|
+
'openclaw', 'hermes', 'omp', 'kiro', 'opencode', 'trae',
|
|
1588
|
+
]).optional().describe('Optional target agent for compatible workflow selection.'),
|
|
1581
1589
|
limit: z.number().optional().describe('Reserved for future source limits; current prompt stays compact by default.'),
|
|
1582
1590
|
},
|
|
1583
1591
|
},
|
|
1584
|
-
async ({ task, refresh, format }) => {
|
|
1592
|
+
async ({ task, refresh, format, agent }) => {
|
|
1585
1593
|
const unresolved = requireResolvedProject('build project context for the current project');
|
|
1586
1594
|
if (unresolved) return unresolved;
|
|
1587
1595
|
|
|
@@ -1595,11 +1603,13 @@ export async function createMemorixServer(
|
|
|
1595
1603
|
{ getObservationStore },
|
|
1596
1604
|
{ MaintenanceJobStore },
|
|
1597
1605
|
{ enqueueCodegraphRefresh },
|
|
1606
|
+
{ buildBoundedContextReceipt },
|
|
1598
1607
|
] = await Promise.all([
|
|
1599
1608
|
import('./codegraph/auto-context.js'),
|
|
1600
1609
|
import('./store/obs-store.js'),
|
|
1601
1610
|
import('./runtime/maintenance-jobs.js'),
|
|
1602
1611
|
import('./runtime/lifecycle.js'),
|
|
1612
|
+
import('./knowledge/context-receipt.js'),
|
|
1603
1613
|
]);
|
|
1604
1614
|
const observations = filterReadableObservations(
|
|
1605
1615
|
await getObservationStore().loadByProject(project.id, { status: 'active' }),
|
|
@@ -1610,6 +1620,7 @@ export async function createMemorixServer(
|
|
|
1610
1620
|
dataDir: projectDir,
|
|
1611
1621
|
observations,
|
|
1612
1622
|
task,
|
|
1623
|
+
agent,
|
|
1613
1624
|
refresh: refresh ?? 'auto',
|
|
1614
1625
|
reader: getObservationReader(),
|
|
1615
1626
|
enqueueRefresh: () => {
|
|
@@ -1624,6 +1635,11 @@ export async function createMemorixServer(
|
|
|
1624
1635
|
});
|
|
1625
1636
|
const text = format === 'json'
|
|
1626
1637
|
? JSON.stringify({ ...context, brief: buildAutoProjectBrief(context) }, null, 2)
|
|
1638
|
+
: format === 'receipt'
|
|
1639
|
+
? JSON.stringify(buildBoundedContextReceipt({
|
|
1640
|
+
workset: context.workset,
|
|
1641
|
+
providerQuality: context.providerQuality,
|
|
1642
|
+
}), null, 2)
|
|
1627
1643
|
: format === 'summary'
|
|
1628
1644
|
? formatAutoProjectContextSummary(context)
|
|
1629
1645
|
: formatAutoProjectContextPrompt(context);
|
package/src/store/orama-store.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
import { calculateProjectAffinity, extractProjectKeywords, type AffinityContext, type MemoryContent } from './project-affinity.js';
|
|
24
24
|
import { detectQueryIntent, applyIntentBoost } from '../search/intent-detector.js';
|
|
25
25
|
import { maybeExpandSearchQuery } from '../search/query-expansion.js';
|
|
26
|
-
import { withTimeout } from '../timeout.js';
|
|
26
|
+
import { withTimeout, withTimeoutSignal } from '../timeout.js';
|
|
27
27
|
|
|
28
28
|
let db: AnyOrama | null = null;
|
|
29
29
|
let dbInitPromise: Promise<AnyOrama> | null = null;
|
|
@@ -642,18 +642,20 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
642
642
|
} else {
|
|
643
643
|
// Embedding timeout: 15 seconds
|
|
644
644
|
const EMBEDDING_TIMEOUT_MS = 15000;
|
|
645
|
-
queryVector = await
|
|
646
|
-
provider.embedInput
|
|
645
|
+
queryVector = await withTimeoutSignal(
|
|
646
|
+
(signal) => provider.embedInput
|
|
647
647
|
? provider.embedInput(
|
|
648
648
|
{ modality: 'text', text: expandedEmbeddingQuery! },
|
|
649
|
-
{ intent: 'query', timeoutMs: EMBEDDING_TIMEOUT_MS, retry: false },
|
|
649
|
+
{ intent: 'query', timeoutMs: EMBEDDING_TIMEOUT_MS, retry: false, signal },
|
|
650
650
|
)
|
|
651
651
|
: provider.embed(expandedEmbeddingQuery!, {
|
|
652
652
|
timeoutMs: EMBEDDING_TIMEOUT_MS,
|
|
653
653
|
retry: false,
|
|
654
|
+
signal,
|
|
654
655
|
}),
|
|
655
656
|
EMBEDDING_TIMEOUT_MS,
|
|
656
657
|
'Embedding',
|
|
658
|
+
options.signal,
|
|
657
659
|
);
|
|
658
660
|
mark('embedding');
|
|
659
661
|
// Detect CJK-heavy queries: BM25 can't tokenize Chinese/Japanese/Korean well
|
package/src/timeout.ts
CHANGED
|
@@ -21,3 +21,70 @@ export function withTimeout<T>(promise: Promise<T>, ms: number, label: string):
|
|
|
21
21
|
);
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run an operation with a deadline and an AbortSignal for cancellable work.
|
|
27
|
+
* Callers should pass the signal to fetch/embedding/LLM APIs they own.
|
|
28
|
+
*/
|
|
29
|
+
export function withTimeoutSignal<T>(
|
|
30
|
+
operation: (signal: AbortSignal) => Promise<T>,
|
|
31
|
+
ms: number,
|
|
32
|
+
label: string,
|
|
33
|
+
parentSignal?: AbortSignal,
|
|
34
|
+
): Promise<T> {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const signal = parentSignal
|
|
37
|
+
? AbortSignal.any([parentSignal, controller.signal])
|
|
38
|
+
: controller.signal;
|
|
39
|
+
|
|
40
|
+
return new Promise<T>((resolve, reject) => {
|
|
41
|
+
let settled = false;
|
|
42
|
+
let timedOut = false;
|
|
43
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
44
|
+
const cleanup = () => {
|
|
45
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
46
|
+
parentSignal?.removeEventListener('abort', onParentAbort);
|
|
47
|
+
};
|
|
48
|
+
const onParentAbort = () => {
|
|
49
|
+
if (settled || timedOut) return;
|
|
50
|
+
settled = true;
|
|
51
|
+
cleanup();
|
|
52
|
+
reject(parentSignal?.reason ?? new Error(`${label} aborted`));
|
|
53
|
+
};
|
|
54
|
+
parentSignal?.addEventListener('abort', onParentAbort, { once: true });
|
|
55
|
+
|
|
56
|
+
timer = setTimeout(() => {
|
|
57
|
+
if (settled) return;
|
|
58
|
+
timedOut = true;
|
|
59
|
+
controller.abort(new Error(`${label} timed out after ${ms}ms`));
|
|
60
|
+
settled = true;
|
|
61
|
+
cleanup();
|
|
62
|
+
reject(new Error(`${label} timed out after ${ms}ms`));
|
|
63
|
+
}, ms);
|
|
64
|
+
|
|
65
|
+
let pending: Promise<T>;
|
|
66
|
+
try {
|
|
67
|
+
pending = operation(signal);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
settled = true;
|
|
70
|
+
cleanup();
|
|
71
|
+
reject(error);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pending.then(
|
|
76
|
+
(value) => {
|
|
77
|
+
if (settled) return;
|
|
78
|
+
settled = true;
|
|
79
|
+
cleanup();
|
|
80
|
+
resolve(value);
|
|
81
|
+
},
|
|
82
|
+
(error: unknown) => {
|
|
83
|
+
if (settled) return;
|
|
84
|
+
settled = true;
|
|
85
|
+
cleanup();
|
|
86
|
+
reject(timedOut ? new Error(`${label} timed out after ${ms}ms`) : error);
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -328,6 +328,8 @@ export interface SearchOptions {
|
|
|
328
328
|
* `thorough` explicitly permits optional LLM query rewrite and reranking.
|
|
329
329
|
*/
|
|
330
330
|
quality?: RetrievalQuality;
|
|
331
|
+
/** Abort provider-backed retrieval when the owning request is cancelled. */
|
|
332
|
+
signal?: AbortSignal;
|
|
331
333
|
}
|
|
332
334
|
|
|
333
335
|
/** Topic key family heuristics for suggesting stable topic keys */
|