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
package/dist/types.d.ts CHANGED
@@ -360,6 +360,8 @@ interface SearchOptions {
360
360
  * `thorough` explicitly permits optional LLM query rewrite and reranking.
361
361
  */
362
362
  quality?: RetrievalQuality;
363
+ /** Abort provider-backed retrieval when the owning request is cancelled. */
364
+ signal?: AbortSignal;
363
365
  }
364
366
  /** Topic key family heuristics for suggesting stable topic keys */
365
367
  declare const TOPIC_KEY_FAMILIES: Record<string, string[]>;
@@ -2755,6 +2755,8 @@ function getPreferredBatchSize(config) {
2755
2755
  }
2756
2756
  function parseBatchLimit(error) {
2757
2757
  if (!(error instanceof Error)) return null;
2758
+ const status = error instanceof EmbeddingAPIError ? error.status : Number(error.message.match(/Embedding API error \((\d{3})\)/i)?.[1] ?? 0);
2759
+ if (status > 0 && status !== 400) return null;
2758
2760
  const explicit = error.message.match(/should not be larger than\s+(\d+)/i);
2759
2761
  if (explicit) return parseInt(explicit[1], 10);
2760
2762
  if (/batch size/i.test(error.message)) {
@@ -2767,37 +2769,99 @@ async function fetchWithRetry(url, apiKey, body, options = {}, attempt = 0, nati
2767
2769
  const controller = new AbortController();
2768
2770
  const timeoutMs = typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs) ? Math.max(100, Math.min(Math.floor(options.timeoutMs), 6e4)) : 1e4;
2769
2771
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
2772
+ const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
2770
2773
  let response;
2771
2774
  try {
2772
2775
  response = await fetch(url, {
2773
2776
  method: "POST",
2774
2777
  headers: nativeGemini ? { "Content-Type": "application/json", "x-goog-api-key": apiKey } : { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
2775
2778
  body: JSON.stringify(body),
2776
- signal: controller.signal
2779
+ signal
2777
2780
  });
2778
2781
  } catch (err) {
2779
2782
  clearTimeout(timeout);
2780
- if (err instanceof Error && err.name === "AbortError") {
2783
+ if (err instanceof Error && (err.name === "AbortError" || controller.signal.aborted || options.signal?.aborted)) {
2781
2784
  throw new Error(`Embedding API timeout after ${timeoutMs}ms: ${url}`);
2782
2785
  }
2783
2786
  throw err;
2784
2787
  }
2785
- clearTimeout(timeout);
2786
- if (response.ok) {
2787
- return response.json();
2788
+ try {
2789
+ if (response.ok) {
2790
+ return await raceWithAbort(
2791
+ response.json(),
2792
+ signal
2793
+ );
2794
+ }
2795
+ if (options.retry !== false && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES2) {
2796
+ const delay2 = BASE_DELAY_MS * Math.pow(2, attempt);
2797
+ const retryAfter = response.headers.get("retry-after");
2798
+ const parsedRetryAfter = retryAfter ? parseInt(retryAfter, 10) * 1e3 : NaN;
2799
+ const waitMs = Number.isFinite(parsedRetryAfter) ? parsedRetryAfter : delay2;
2800
+ console.error(`[memorix] Embedding API ${response.status}, retry ${attempt + 1}/${MAX_RETRIES2} in ${waitMs}ms`);
2801
+ await waitForRetry(waitMs, options.signal);
2802
+ return fetchWithRetry(url, apiKey, body, options, attempt + 1, nativeGemini);
2803
+ }
2804
+ const errorText2 = await raceWithAbort(
2805
+ response.text().catch(() => "unknown error"),
2806
+ signal
2807
+ );
2808
+ throw new EmbeddingAPIError(response.status, errorText2);
2809
+ } catch (err) {
2810
+ if (err instanceof Error && (err.name === "AbortError" || controller.signal.aborted || options.signal?.aborted)) {
2811
+ throw new Error(`Embedding API timeout after ${timeoutMs}ms: ${url}`);
2812
+ }
2813
+ throw err;
2814
+ } finally {
2815
+ clearTimeout(timeout);
2788
2816
  }
2789
- if (options.retry !== false && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES2) {
2790
- const delay2 = BASE_DELAY_MS * Math.pow(2, attempt);
2791
- const retryAfter = response.headers.get("retry-after");
2792
- const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : delay2;
2793
- console.error(`[memorix] Embedding API ${response.status}, retry ${attempt + 1}/${MAX_RETRIES2} in ${waitMs}ms`);
2817
+ }
2818
+ function abortError() {
2819
+ const error = new Error("Embedding API request aborted");
2820
+ error.name = "AbortError";
2821
+ return error;
2822
+ }
2823
+ function raceWithAbort(promise, signal) {
2824
+ if (signal.aborted) return Promise.reject(abortError());
2825
+ return new Promise((resolve, reject) => {
2826
+ const onAbort = () => {
2827
+ signal.removeEventListener("abort", onAbort);
2828
+ reject(abortError());
2829
+ };
2830
+ signal.addEventListener("abort", onAbort, { once: true });
2831
+ promise.then(
2832
+ (value) => {
2833
+ signal.removeEventListener("abort", onAbort);
2834
+ resolve(value);
2835
+ },
2836
+ (error) => {
2837
+ signal.removeEventListener("abort", onAbort);
2838
+ reject(error);
2839
+ }
2840
+ );
2841
+ });
2842
+ }
2843
+ async function waitForRetry(waitMs, signal) {
2844
+ if (!signal) {
2794
2845
  await new Promise((resolve) => setTimeout(resolve, waitMs));
2795
- return fetchWithRetry(url, apiKey, body, options, attempt + 1, nativeGemini);
2846
+ return;
2796
2847
  }
2797
- const errorText2 = await response.text().catch(() => "unknown error");
2798
- throw new Error(`Embedding API error (${response.status}): ${errorText2}`);
2848
+ const abortSignal = signal;
2849
+ if (abortSignal.aborted) throw new Error("Embedding API retry aborted");
2850
+ await new Promise((resolve, reject) => {
2851
+ const timer = setTimeout(done, waitMs);
2852
+ const onAbort = () => {
2853
+ clearTimeout(timer);
2854
+ abortSignal.removeEventListener("abort", onAbort);
2855
+ reject(new Error("Embedding API retry aborted"));
2856
+ };
2857
+ function done() {
2858
+ abortSignal.removeEventListener("abort", onAbort);
2859
+ resolve();
2860
+ }
2861
+ abortSignal.addEventListener("abort", onAbort, { once: true });
2862
+ });
2799
2863
  }
2800
- var cache3, MAX_CACHE_SIZE3, diskCacheDirty2, diskSaveTimer, diskCacheLoaded, diskCacheLoadPromise, MAX_INPUT_CHARS, MAX_CONCURRENCY, DISK_SAVE_DEBOUNCE_MS, DEFAULT_MAX_BATCH_SIZE, DASHSCOPE_MAX_BATCH_SIZE, MAX_RETRIES2, BASE_DELAY_MS, APIEmbeddingProvider;
2864
+ var cache3, MAX_CACHE_SIZE3, diskCacheDirty2, diskSaveTimer, diskCacheLoaded, diskCacheLoadPromise, MAX_INPUT_CHARS, MAX_CONCURRENCY, DISK_SAVE_DEBOUNCE_MS, DEFAULT_MAX_BATCH_SIZE, DASHSCOPE_MAX_BATCH_SIZE, MAX_RETRIES2, BASE_DELAY_MS, EmbeddingAPIError, APIEmbeddingProvider;
2801
2865
  var init_api_provider = __esm({
2802
2866
  "src/embedding/api-provider.ts"() {
2803
2867
  "use strict";
@@ -2816,6 +2880,14 @@ var init_api_provider = __esm({
2816
2880
  DASHSCOPE_MAX_BATCH_SIZE = 10;
2817
2881
  MAX_RETRIES2 = 3;
2818
2882
  BASE_DELAY_MS = 500;
2883
+ EmbeddingAPIError = class extends Error {
2884
+ status;
2885
+ constructor(status, detail) {
2886
+ super(`Embedding API error (${status}): ${detail}`);
2887
+ this.name = "EmbeddingAPIError";
2888
+ this.status = status;
2889
+ }
2890
+ };
2819
2891
  APIEmbeddingProvider = class _APIEmbeddingProvider {
2820
2892
  name;
2821
2893
  dimensions;
@@ -3027,15 +3099,14 @@ var init_api_provider = __esm({
3027
3099
  }
3028
3100
  } catch (error) {
3029
3101
  const providerLimit = parseBatchLimit(error);
3030
- const fallbackSize = providerLimit ?? Math.ceil(chunkTexts.length / 2);
3031
- if (chunkTexts.length > 1 && fallbackSize < chunkTexts.length) {
3102
+ if (providerLimit !== null && chunkTexts.length > 1 && providerLimit < chunkTexts.length) {
3032
3103
  console.error(
3033
- `[memorix] Embedding batch too large for provider, retrying in chunks of ${fallbackSize}`
3104
+ `[memorix] Embedding batch too large for provider, retrying in chunks of ${providerLimit}`
3034
3105
  );
3035
- for (let start = 0; start < chunkTexts.length; start += fallbackSize) {
3106
+ for (let start = 0; start < chunkTexts.length; start += providerLimit) {
3036
3107
  await processChunk(
3037
- chunkTexts.slice(start, start + fallbackSize),
3038
- chunkIndices.slice(start, start + fallbackSize)
3108
+ chunkTexts.slice(start, start + providerLimit),
3109
+ chunkIndices.slice(start, start + providerLimit)
3039
3110
  );
3040
3111
  }
3041
3112
  return;
@@ -3899,22 +3970,23 @@ function getLLMConfig() {
3899
3970
  function setLLMConfig(config) {
3900
3971
  currentConfig = config;
3901
3972
  }
3902
- async function callLLM(systemPrompt, userMessage) {
3973
+ async function callLLM(systemPrompt, userMessage, signal) {
3903
3974
  if (!currentConfig) {
3904
3975
  throw new Error("LLM not configured. Set MEMORIX_LLM_API_KEY or OPENAI_API_KEY.");
3905
3976
  }
3906
3977
  if (currentConfig.provider === "anthropic") {
3907
- return callAnthropic(systemPrompt, userMessage);
3978
+ return callAnthropic(systemPrompt, userMessage, signal);
3908
3979
  }
3909
- return callOpenAICompatible(systemPrompt, userMessage);
3980
+ return callOpenAICompatible(systemPrompt, userMessage, signal);
3910
3981
  }
3911
- async function callOpenAICompatible(systemPrompt, userMessage) {
3982
+ async function callOpenAICompatible(systemPrompt, userMessage, signal) {
3912
3983
  const config = currentConfig;
3913
3984
  let base = config.baseUrl.replace(/\/+$/, "");
3914
3985
  if (!base.endsWith("/v1")) base += "/v1";
3915
3986
  const url = `${base}/chat/completions`;
3987
+ const fetchSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(LLM_CALL_TIMEOUT_MS)]) : AbortSignal.timeout(LLM_CALL_TIMEOUT_MS);
3916
3988
  const response = await fetch(url, {
3917
- signal: AbortSignal.timeout(LLM_CALL_TIMEOUT_MS),
3989
+ signal: fetchSignal,
3918
3990
  method: "POST",
3919
3991
  headers: {
3920
3992
  "Content-Type": "application/json",
@@ -3931,10 +4003,10 @@ async function callOpenAICompatible(systemPrompt, userMessage) {
3931
4003
  })
3932
4004
  });
3933
4005
  if (!response.ok) {
3934
- const error = await readResponseText(response, void 0, 64 * 1024).catch(() => "unknown error");
4006
+ const error = await readResponseText(response, signal, 64 * 1024).catch(() => "unknown error");
3935
4007
  throw new Error(`LLM API error (${response.status}): ${error}`);
3936
4008
  }
3937
- const data = await readResponseJson(response);
4009
+ const data = await readResponseJson(response, signal);
3938
4010
  return {
3939
4011
  content: data.choices[0]?.message?.content ?? "",
3940
4012
  usage: data.usage ? {
@@ -3943,11 +4015,12 @@ async function callOpenAICompatible(systemPrompt, userMessage) {
3943
4015
  } : void 0
3944
4016
  };
3945
4017
  }
3946
- async function callAnthropic(systemPrompt, userMessage) {
4018
+ async function callAnthropic(systemPrompt, userMessage, signal) {
3947
4019
  const config = currentConfig;
3948
4020
  const url = `${config.baseUrl}/messages`;
4021
+ const fetchSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(LLM_CALL_TIMEOUT_MS)]) : AbortSignal.timeout(LLM_CALL_TIMEOUT_MS);
3949
4022
  const response = await fetch(url, {
3950
- signal: AbortSignal.timeout(LLM_CALL_TIMEOUT_MS),
4023
+ signal: fetchSignal,
3951
4024
  method: "POST",
3952
4025
  headers: {
3953
4026
  "Content-Type": "application/json",
@@ -3965,10 +4038,10 @@ async function callAnthropic(systemPrompt, userMessage) {
3965
4038
  })
3966
4039
  });
3967
4040
  if (!response.ok) {
3968
- const error = await readResponseText(response, void 0, 64 * 1024).catch(() => "unknown error");
4041
+ const error = await readResponseText(response, signal, 64 * 1024).catch(() => "unknown error");
3969
4042
  throw new Error(`Anthropic API error (${response.status}): ${error}`);
3970
4043
  }
3971
- const data = await readResponseJson(response);
4044
+ const data = await readResponseJson(response, signal);
3972
4045
  return {
3973
4046
  content: data.content[0]?.text ?? "",
3974
4047
  usage: data.usage ? {
@@ -4473,6 +4546,57 @@ function withTimeout(promise, ms, label) {
4473
4546
  );
4474
4547
  });
4475
4548
  }
4549
+ function withTimeoutSignal(operation, ms, label, parentSignal) {
4550
+ const controller = new AbortController();
4551
+ const signal = parentSignal ? AbortSignal.any([parentSignal, controller.signal]) : controller.signal;
4552
+ return new Promise((resolve, reject) => {
4553
+ let settled = false;
4554
+ let timedOut = false;
4555
+ let timer;
4556
+ const cleanup = () => {
4557
+ if (timer !== void 0) clearTimeout(timer);
4558
+ parentSignal?.removeEventListener("abort", onParentAbort);
4559
+ };
4560
+ const onParentAbort = () => {
4561
+ if (settled || timedOut) return;
4562
+ settled = true;
4563
+ cleanup();
4564
+ reject(parentSignal?.reason ?? new Error(`${label} aborted`));
4565
+ };
4566
+ parentSignal?.addEventListener("abort", onParentAbort, { once: true });
4567
+ timer = setTimeout(() => {
4568
+ if (settled) return;
4569
+ timedOut = true;
4570
+ controller.abort(new Error(`${label} timed out after ${ms}ms`));
4571
+ settled = true;
4572
+ cleanup();
4573
+ reject(new Error(`${label} timed out after ${ms}ms`));
4574
+ }, ms);
4575
+ let pending;
4576
+ try {
4577
+ pending = operation(signal);
4578
+ } catch (error) {
4579
+ settled = true;
4580
+ cleanup();
4581
+ reject(error);
4582
+ return;
4583
+ }
4584
+ pending.then(
4585
+ (value) => {
4586
+ if (settled) return;
4587
+ settled = true;
4588
+ cleanup();
4589
+ resolve(value);
4590
+ },
4591
+ (error) => {
4592
+ if (settled) return;
4593
+ settled = true;
4594
+ cleanup();
4595
+ reject(timedOut ? new Error(`${label} timed out after ${ms}ms`) : error);
4596
+ }
4597
+ );
4598
+ });
4599
+ }
4476
4600
  var init_timeout = __esm({
4477
4601
  "src/timeout.ts"() {
4478
4602
  "use strict";
@@ -5337,16 +5461,18 @@ async function searchObservations(options) {
5337
5461
  );
5338
5462
  } else {
5339
5463
  const EMBEDDING_TIMEOUT_MS = 15e3;
5340
- queryVector = await withTimeout(
5341
- provider2.embedInput ? provider2.embedInput(
5464
+ queryVector = await withTimeoutSignal(
5465
+ (signal) => provider2.embedInput ? provider2.embedInput(
5342
5466
  { modality: "text", text: expandedEmbeddingQuery },
5343
- { intent: "query", timeoutMs: EMBEDDING_TIMEOUT_MS, retry: false }
5467
+ { intent: "query", timeoutMs: EMBEDDING_TIMEOUT_MS, retry: false, signal }
5344
5468
  ) : provider2.embed(expandedEmbeddingQuery, {
5345
5469
  timeoutMs: EMBEDDING_TIMEOUT_MS,
5346
- retry: false
5470
+ retry: false,
5471
+ signal
5347
5472
  }),
5348
5473
  EMBEDDING_TIMEOUT_MS,
5349
- "Embedding"
5474
+ "Embedding",
5475
+ options.signal
5350
5476
  );
5351
5477
  mark("embedding");
5352
5478
  const cjkRatio = (originalQuery.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g) || []).length / originalQuery.length;
@@ -145244,7 +145370,13 @@ async function maintainLongTermMemories(input) {
145244
145370
  }
145245
145371
  for (const group of groups.values()) {
145246
145372
  if (group.length < 2) continue;
145247
- group.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
145373
+ group.sort((a, b) => {
145374
+ const byCreatedAt = Date.parse(b.createdAt) - Date.parse(a.createdAt);
145375
+ if (byCreatedAt !== 0) return byCreatedAt;
145376
+ const byUpdatedAt = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
145377
+ if (byUpdatedAt !== 0) return byUpdatedAt;
145378
+ return b.id.localeCompare(a.id);
145379
+ });
145248
145380
  const newest = group[0];
145249
145381
  for (const older of group.slice(1)) {
145250
145382
  try {
@@ -147648,6 +147780,7 @@ var init_task_lens = __esm({
147648
147780
  var workflows_exports = {};
147649
147781
  __export(workflows_exports, {
147650
147782
  WORKFLOW_ADAPTER_TARGETS: () => WORKFLOW_ADAPTER_TARGETS,
147783
+ WORKFLOW_AGENT_TARGETS: () => WORKFLOW_AGENT_TARGETS,
147651
147784
  applyWorkflowAdapter: () => applyWorkflowAdapter,
147652
147785
  importWindsurfWorkflows: () => importWindsurfWorkflows,
147653
147786
  parseWorkflowMarkdown: () => parseWorkflowMarkdown,
@@ -147713,7 +147846,7 @@ function optionalStatus(data) {
147713
147846
  return value;
147714
147847
  }
147715
147848
  function normalizeAgents(values) {
147716
- const unknown3 = values.filter((value) => !KNOWN_AGENTS.includes(value));
147849
+ const unknown3 = values.filter((value) => !WORKFLOW_AGENT_TARGETS.includes(value));
147717
147850
  if (unknown3.length > 0) {
147718
147851
  throw new Error("workflow frontmatter has unsupported agent: " + unknown3[0]);
147719
147852
  }
@@ -147989,7 +148122,7 @@ function taskScore(workflow, task) {
147989
148122
  return { score, reasons: [...new Set(reasons)] };
147990
148123
  }
147991
148124
  function selectWorkflows(input) {
147992
- const selected = input.workflows.filter((workflow) => workflow.status === "active").map((workflow) => {
148125
+ const selected = input.workflows.filter((workflow) => workflow.status === "active" && (!input.agent || workflow.compatibleAgents.includes(input.agent))).map((workflow) => {
147993
148126
  const match2 = taskScore(workflow, input.task);
147994
148127
  const cautions = input.store && input.projectId ? input.store.recentFailureCautions(input.projectId, workflow.id) : [];
147995
148128
  return {
@@ -148269,7 +148402,7 @@ async function selectWorkspaceWorkflows(input) {
148269
148402
  errors: synced.errors
148270
148403
  };
148271
148404
  }
148272
- var KNOWN_AGENTS, WORKFLOW_ADAPTER_TARGETS, STATUS_VALUES, LENS_TERMS, GENERIC_WORKFLOW_LENSES, GENERIC_WORKFLOW_TERMS;
148405
+ var WORKFLOW_AGENT_TARGETS, WORKFLOW_ADAPTER_TARGETS, STATUS_VALUES, LENS_TERMS, GENERIC_WORKFLOW_LENSES, GENERIC_WORKFLOW_TERMS;
148273
148406
  var init_workflows2 = __esm({
148274
148407
  "src/knowledge/workflows.ts"() {
148275
148408
  "use strict";
@@ -148281,7 +148414,7 @@ var init_workflows2 = __esm({
148281
148414
  init_workflow_store();
148282
148415
  init_outcome_store();
148283
148416
  init_task_lens();
148284
- KNOWN_AGENTS = [
148417
+ WORKFLOW_AGENT_TARGETS = [
148285
148418
  "windsurf",
148286
148419
  "cursor",
148287
148420
  "claude-code",
@@ -26,7 +26,7 @@ database, a transcript archive, or another default MCP tool.
26
26
  | Start a new, unrelated task | A bounded task Workset. No automatic prior-session text. |
27
27
  | Continue or take over prior work | The same Workset plus a bounded prior-work projection. |
28
28
  | MCP is available | Use the existing `memorix_project_context` tool with the real task. |
29
- | MCP is unavailable | Make one CLI call: `memorix resume "<task>" --json` for continuation or `memorix context "<task>" --json` for a new task. |
29
+ | MCP is unavailable | Make one CLI call: `memorix resume "<task>" --brief-json` for continuation or `memorix context "<task>" --brief-json` for a new task. `--json` remains the detailed diagnostic form. |
30
30
  | Claude Code hook is installed | An explicit continuation prompt receives the same bounded prior-work Workset through the official `UserPromptSubmit` context channel, unless `memory.inject = "silent"`. |
31
31
  | Read-only assessment | The brief may be read, but Memorix does not persist a new observation unless the user explicitly asks to save one. |
32
32
  | Need a deliberate session handoff | Keep using the explicit session/handoff surfaces; this projection does not replace them. |
@@ -115,7 +115,7 @@ MCP:
115
115
  - `memorix_context_pack` builds a task-specific packet with reliable current memories, lower-trust unbound memories, current code facts, freshness warnings, suggested reads, and suggested verification.
116
116
  - `memorix_graph_context` is an advanced compact graph overview. It starts from a small task-matched baseline and can add only directly related, evidence-governed memories. Its `Graph evidence` section includes relation provenance rather than recursively dumping graph neighbors.
117
117
 
118
- `memorix context` defaults to `--refresh auto`, so first use can seed Code State without a separate manual `memorix codegraph refresh`. Its brief puts live package/changelog/Git facts before memory hints and flags an old `ACTIVE_WORK.md`, legacy `progress.txt`, or legacy dev-log note as historical when it predates the latest changelog, so agents should treat current facts as the source of truth when files disagree. Task lenses keep the packet shaped to the work: bugfix briefs prefer failing tests and repros, release briefs prefer metadata/changelog/package checks, and onboarding briefs prefer docs and entry points while hiding unrelated suspect details. Continuation delivery is separate from the task lens: continuation language in `memorix_project_context` enables a bounded prior-work projection, and `memorix resume "..."` makes that choice explicit. It includes only the latest useful session summary, up to three readable durable memories, and at most one recent source-labelled compact checkpoint. Checkpoints are host lifecycle evidence, not durable memory or transcript backups; ordinary new tasks do not receive historical-session context. A completed MCP brief is the default retrieval boundary: search, detail, or Context Pack should expand it only for a named missing fact or an explicit request for deeper history. Use `--refresh never` for read-only inspection and `--refresh always` when you want to force a fresh scan.
118
+ `memorix context` defaults to `--refresh auto`, so first use can seed Code State without a separate manual `memorix codegraph refresh`. Its brief puts live package/changelog/Git facts before memory hints and flags an old `ACTIVE_WORK.md`, legacy `progress.txt`, or legacy dev-log note as historical when it predates the latest changelog, so agents should treat current facts as the source of truth when files disagree. Use `--brief-json` for the same bounded brief plus a receipt of selected and omitted evidence; `--json` remains the detailed legacy diagnostic form. Task lenses keep the packet shaped to the work: bugfix briefs prefer failing tests and repros, release briefs prefer metadata/changelog/package checks, and onboarding briefs prefer docs and entry points while hiding unrelated suspect details. Continuation delivery is separate from the task lens: continuation language in `memorix_project_context` enables a bounded prior-work projection, and `memorix resume "..."` makes that choice explicit. It includes only the latest useful session summary, up to three readable durable memories, and at most one recent source-labelled compact checkpoint. Checkpoints are host lifecycle evidence, not durable memory or transcript backups; ordinary new tasks do not receive historical-session context. A completed MCP brief is the default retrieval boundary: search, detail, or Context Pack should expand it only for a named missing fact or an explicit request for deeper history. Use `--refresh never` for read-only inspection and `--refresh always` when you want to force a fresh scan.
119
119
 
120
120
  Project-specific generated, vendored, or cache paths can be excluded from Code State with `[codegraph].exclude_patterns` in `memorix.toml` or `~/.memorix/config.toml` (`codegraph.excludePatterns` in legacy YAML). User patterns extend the built-in excludes and are applied to indexing, Project Context suggested reads, and Context Pack suggested reads.
121
121
 
@@ -326,9 +326,11 @@ Notes:
326
326
  - `server.json` is the official MCP Registry record. Keep it synchronized with
327
327
  `package.json` by running `npm run sync:mcp-registry`; verify it with
328
328
  `npm run check:mcp-registry` before a release.
329
- - The GitHub publish workflow publishes npm first, then uses GitHub OIDC to
330
- publish the same verified version to the official MCP Registry. It does not
331
- require a Registry token or private key in repository secrets.
329
+ - The GitHub publish workflow can publish npm first, then uses GitHub OIDC to
330
+ publish the same verified version to the official MCP Registry. When npm was
331
+ already published manually, run the workflow with `publish_npm=false` to
332
+ publish only the Registry record. It does not require a Registry token or
333
+ private key in repository secrets.
332
334
  - `@memorix/ai`, `@memorix/agent-core`, `@memorix/tui`, and `@memorix/memcode` are internal workspaces. Their code is bundled into the root `memorix` distribution and they must stay `private` unless the project deliberately establishes an owned npm scope and a separate package-support contract.
333
335
  - npm publish is usually manual, especially when 2FA is enabled
334
336
  - GitHub release automation should not be treated as a substitute for manual runtime validation
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "memorix",
3
3
  "mcpName": "io.github.AVIDS2/memorix",
4
- "version": "1.5.0",
4
+ "version": "1.6.0",
5
5
  "description": "Local-first shared memory layer for AI coding agents across MCP clients, Git history, reasoning context, and project sessions.",
6
6
  "type": "module",
7
7
  "sideEffects": false,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "memorix",
4
- "version": "1.5.0",
4
+ "version": "1.6.0",
5
5
  "description": "Shared workspace memory for Claude Code and other AI coding agents.",
6
6
  "author": {
7
7
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Local-first project memory for CodeBuddy Code and other coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Shared workspace memory for Codex and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Shared local workspace memory for GitHub Copilot CLI and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "mcpServers": {
5
5
  "memorix": {
6
6
  "command": "memorix",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-omp-package",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Memorix shared workspace memory package for Oh-my-Pi.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Shared workspace memory for OpenClaw and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-pi-package",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Memorix shared workspace memory package for Pi coding agent.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
package/server.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "url": "https://github.com/AVIDS2/memorix",
8
8
  "source": "github"
9
9
  },
10
- "version": "1.5.0",
10
+ "version": "1.6.0",
11
11
  "websiteUrl": "https://github.com/AVIDS2/memorix#readme",
12
12
  "icons": [
13
13
  {
@@ -22,7 +22,7 @@
22
22
  {
23
23
  "registryType": "npm",
24
24
  "identifier": "memorix",
25
- "version": "1.5.0",
25
+ "version": "1.6.0",
26
26
  "runtimeHint": "npx",
27
27
  "packageArguments": [
28
28
  {
@@ -5,11 +5,14 @@ import { assembleContextPackForTask, attachTaskWorkset, buildContextPackPrompt }
5
5
  import { backfillMissingObservationCodeRefs } from '../../codegraph/binder.js';
6
6
  import { collectCurrentProjectFacts, formatGitFact } from '../../codegraph/current-facts.js';
7
7
  import { resolveTaskLens } from '../../codegraph/task-lens.js';
8
- import { getExternalCodeGraphContext, inspectExternalCodeGraph } from '../../codegraph/external-provider.js';
8
+ import { getExternalCodeGraphContext, inspectExternalCodeGraph, runExternalCodeGraphLifecycle } from '../../codegraph/external-provider.js';
9
9
  import type { CodeGraphProviderQuality } from '../../codegraph/types.js';
10
10
  import { getResolvedConfig } from '../../config/resolved-config.js';
11
+ import { buildBoundedContextReceipt } from '../../knowledge/context-receipt.js';
12
+ import { WORKFLOW_AGENT_TARGETS } from '../../knowledge/workflows.js';
11
13
  import { getAllObservations } from '../../memory/observations.js';
12
14
  import { filterReadableObservations } from '../../memory/visibility.js';
15
+ import type { AgentTarget } from '../../types.js';
13
16
  import { emitError, emitResult, getCliProjectContext, parsePositiveInt } from './operator-shared.js';
14
17
 
15
18
  function formatSnapshotStatus(status: ReturnType<CodeGraphStore['status']>): string[] {
@@ -48,6 +51,7 @@ function formatStatus(status: ReturnType<CodeGraphStore['status']>, quality?: Co
48
51
  `- Persistent provider: ${status.provider} (heuristic local index)`,
49
52
  `- External semantic CodeGraph: ${quality.external.state}`
50
53
  + (quality.external.reason ? ` (${quality.external.reason})` : ''),
54
+ `- Task-scoped provider: ${quality.selected} (${quality.selectedQuality})`,
51
55
  ]
52
56
  : []),
53
57
  ].join('\n');
@@ -57,6 +61,8 @@ function formatUsageHint(): string {
57
61
  return [
58
62
  'Usage:',
59
63
  ' memorix codegraph refresh',
64
+ ' memorix codegraph init # explicit local CodeGraph initialization',
65
+ ' memorix codegraph sync # explicit local CodeGraph synchronization',
60
66
  ' memorix codegraph status --json',
61
67
  ' memorix codegraph diff [--from <snapshot>] [--to <snapshot>]',
62
68
  ' memorix codegraph impact --path <relative-source-path>',
@@ -116,25 +122,34 @@ function compactFacts(project: { rootPath: string }): { facts: string[]; dirty:
116
122
  return { facts, dirty: current.git.dirty };
117
123
  }
118
124
 
125
+ function coerceAgentTarget(input?: string): AgentTarget | undefined {
126
+ const value = input?.trim().toLowerCase();
127
+ if (!value) return undefined;
128
+ if (WORKFLOW_AGENT_TARGETS.includes(value as AgentTarget)) return value as AgentTarget;
129
+ throw new Error('agent must be one of: ' + WORKFLOW_AGENT_TARGETS.join(', '));
130
+ }
131
+
119
132
  export default defineCommand({
120
133
  meta: {
121
134
  name: 'codegraph',
122
135
  description: 'Inspect and refresh CodeGraph Memory for the current project',
123
136
  },
124
137
  args: {
125
- action: { type: 'string', description: 'Action: status, refresh, diff, impact, or context-pack' },
138
+ action: { type: 'string', description: 'Action: status, refresh, diff, impact, init, sync, or context-pack' },
126
139
  task: { type: 'string', description: 'Task text for context-pack' },
127
140
  path: { type: 'string', description: 'Indexed relative source path for impact' },
128
141
  limit: { type: 'string', description: 'Max active memories to inspect for context-pack' },
129
142
  from: { type: 'string', description: 'Earlier CodeGraph snapshot id for diff' },
130
143
  to: { type: 'string', description: 'Later CodeGraph snapshot id for diff' },
131
144
  impactLimit: { type: 'string', description: 'Maximum graph relations to inspect for diff impact' },
145
+ agent: { type: 'string', description: 'Optional target agent for compatible workflow selection' },
132
146
  json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
147
+ briefJson: { type: 'boolean', description: 'Emit only the bounded agent brief and receipt JSON for context-pack' },
133
148
  },
134
149
  run: async ({ args }) => {
135
150
  const positional = (args._ as string[]) ?? [];
136
151
  const action = (positional[0] || (args.action as string | undefined) || 'status').toLowerCase();
137
- const asJson = !!args.json;
152
+ const asJson = !!args.json || !!args.briefJson;
138
153
 
139
154
  try {
140
155
  const { project, dataDir, reader } = await getCliProjectContext();
@@ -206,6 +221,27 @@ export default defineCommand({
206
221
  return;
207
222
  }
208
223
 
224
+ case 'init':
225
+ case 'sync': {
226
+ const lifecycle = await runExternalCodeGraphLifecycle({
227
+ action,
228
+ projectRoot: project.rootPath,
229
+ mode: codegraphConfig.externalContext,
230
+ command: codegraphConfig.externalCommand,
231
+ timeoutMs: codegraphConfig.externalTimeoutMs,
232
+ });
233
+ emitResult(
234
+ { project, lifecycle, providerQuality: lifecycle.quality },
235
+ [
236
+ lifecycle.message,
237
+ `- External semantic CodeGraph: ${lifecycle.health.state}`,
238
+ `- Task-scoped provider: ${lifecycle.quality.selected} (${lifecycle.quality.selectedQuality})`,
239
+ ].join('\n'),
240
+ asJson,
241
+ );
242
+ return;
243
+ }
244
+
209
245
  case 'diff': {
210
246
  const snapshots = store.listSnapshots(project.id, 2);
211
247
  const fromSnapshotId = (args.from as string | undefined)?.trim() || snapshots[1]?.id;
@@ -299,13 +335,20 @@ export default defineCommand({
299
335
  ? { runtimeCautions: [{ kind: 'external-codegraph-fallback' as const, message: external.caution }] }
300
336
  : {}),
301
337
  reader,
338
+ agent: coerceAgentTarget(args.agent as string | undefined),
302
339
  });
303
- emitResult({ project, pack, providerQuality: external.quality }, buildContextPackPrompt(pack), asJson);
340
+ emitResult(
341
+ args.briefJson && pack.workset
342
+ ? buildBoundedContextReceipt({ workset: pack.workset, providerQuality: external.quality })
343
+ : { project, pack, providerQuality: external.quality },
344
+ buildContextPackPrompt(pack),
345
+ asJson,
346
+ );
304
347
  return;
305
348
  }
306
349
 
307
350
  default:
308
- emitError(`unknown codegraph action "${action}". Use "status", "refresh", "diff", "impact", or "context-pack".`, asJson);
351
+ emitError(`unknown codegraph action "${action}". Use "status", "refresh", "init", "sync", "diff", "impact", or "context-pack".`, asJson);
309
352
  }
310
353
  } catch (error) {
311
354
  emitError(error instanceof Error ? error.message : String(error), asJson);