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.
Files changed (49) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +3 -3
  4. package/dist/cli/index.js +452 -109
  5. package/dist/index.js +376 -87
  6. package/dist/maintenance-runner.js +174 -41
  7. package/dist/media-audio-runner.js +174 -41
  8. package/dist/media-video-runner.js +174 -41
  9. package/dist/memcode-runtime/CHANGELOG.md +37 -0
  10. package/dist/sdk.js +375 -86
  11. package/dist/types.d.ts +2 -0
  12. package/dist/vector-backfill-runner.js +174 -41
  13. package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +1 -1
  14. package/docs/API_REFERENCE.md +1 -1
  15. package/docs/DEVELOPMENT.md +5 -3
  16. package/package.json +1 -1
  17. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  18. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  19. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  20. package/plugins/copilot/memorix/plugin.json +1 -1
  21. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  22. package/plugins/omp/memorix/package.json +1 -1
  23. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  24. package/plugins/pi/memorix/package.json +1 -1
  25. package/server.json +2 -2
  26. package/src/cli/commands/codegraph.ts +48 -5
  27. package/src/cli/commands/context.ts +20 -4
  28. package/src/cli/commands/resume.ts +4 -0
  29. package/src/codegraph/auto-context.ts +4 -1
  30. package/src/codegraph/context-pack.ts +3 -1
  31. package/src/codegraph/external-provider.ts +93 -0
  32. package/src/embedding/api-provider.ts +105 -22
  33. package/src/embedding/provider.ts +2 -0
  34. package/src/hooks/installers/index.ts +1 -1
  35. package/src/hooks/official-skills.ts +3 -3
  36. package/src/hooks/rules/memorix-agent-rules.md +1 -1
  37. package/src/knowledge/context-receipt.ts +41 -0
  38. package/src/knowledge/workflows.ts +6 -3
  39. package/src/knowledge/workset.ts +56 -2
  40. package/src/llm/provider.ts +17 -8
  41. package/src/memory/formation/extract.ts +6 -3
  42. package/src/memory/formation/index.ts +2 -1
  43. package/src/memory/formation/resolve.ts +10 -6
  44. package/src/memory/formation/types.ts +3 -1
  45. package/src/memory/long-term.ts +7 -1
  46. package/src/server.ts +29 -13
  47. package/src/store/orama-store.ts +6 -4
  48. package/src/timeout.ts +67 -0
  49. package/src/types.ts +2 -0
@@ -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
- const selected = selectWorkflows({
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 = selected.map(workflowOutput);
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,
@@ -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: AbortSignal.timeout(LLM_CALL_TIMEOUT_MS),
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, undefined, 64 * 1024).catch(() => 'unknown error');
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: AbortSignal.timeout(LLM_CALL_TIMEOUT_MS),
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, undefined, 64 * 1024).catch(() => 'unknown error');
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) */
@@ -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) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
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 withTimeout(
864
- runFormation({
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 withTimeout(
1292
- runFormation({ entityName, type: type as ObservationType, title, narrative, facts: safeFacts, projectId: project.id, source: 'explicit', topicKey }, formationConfig),
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 structured.',
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);
@@ -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 withTimeout(
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 */