memorix 1.6.1 → 1.6.3

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 (122) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/TEAM.md +86 -86
  3. package/dist/cli/index.js +2582 -1845
  4. package/dist/index.js +1355 -683
  5. package/dist/maintenance-runner.js +1621 -1096
  6. package/dist/media-audio-runner.js +1612 -1088
  7. package/dist/media-video-runner.js +1612 -1088
  8. package/dist/memcode-runtime/CHANGELOG.md +35 -0
  9. package/dist/sdk.js +2007 -1396
  10. package/dist/vector-backfill-runner.js +1610 -1086
  11. package/docs/DESIGN_DECISIONS.md +357 -357
  12. package/docs/PERFORMANCE.md +15 -0
  13. package/package.json +2 -1
  14. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  15. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  16. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  17. package/plugins/copilot/memorix/plugin.json +1 -1
  18. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  19. package/plugins/omp/memorix/package.json +1 -1
  20. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  21. package/plugins/pi/memorix/package.json +1 -1
  22. package/server.json +2 -2
  23. package/src/audit/index.ts +156 -156
  24. package/src/cli/commands/background.ts +51 -5
  25. package/src/cli/commands/formation.ts +48 -48
  26. package/src/cli/commands/git-hook-install.ts +111 -111
  27. package/src/cli/commands/handoff.ts +54 -54
  28. package/src/cli/commands/hooks-status.ts +63 -63
  29. package/src/cli/commands/ingest.ts +44 -44
  30. package/src/cli/commands/integrate-shared.ts +15 -15
  31. package/src/cli/commands/lock.ts +82 -82
  32. package/src/cli/commands/message.ts +104 -104
  33. package/src/cli/commands/operator-shared.ts +18 -11
  34. package/src/cli/commands/poll.ts +58 -58
  35. package/src/cli/commands/reasoning.ts +118 -118
  36. package/src/cli/commands/serve-http.ts +29 -7
  37. package/src/cli/commands/serve-shared.ts +49 -6
  38. package/src/cli/commands/serve.ts +4 -0
  39. package/src/cli/commands/skills.ts +114 -114
  40. package/src/cli/commands/task.ts +167 -167
  41. package/src/cli/commands/transfer.ts +47 -47
  42. package/src/cli/index.ts +406 -406
  43. package/src/cli/tui/ChatView.tsx +234 -234
  44. package/src/cli/tui/CommandBar.tsx +312 -312
  45. package/src/cli/tui/ContextRail.tsx +118 -118
  46. package/src/cli/tui/HeaderBar.tsx +72 -72
  47. package/src/cli/tui/LogoBanner.tsx +51 -51
  48. package/src/cli/tui/index.ts +41 -41
  49. package/src/cli/version.ts +7 -7
  50. package/src/cli/workbench.ts +1 -1
  51. package/src/dashboard/project-classification.ts +64 -64
  52. package/src/embedding/api-provider.ts +503 -27
  53. package/src/embedding/provider.ts +20 -3
  54. package/src/git/hooks-path.ts +85 -85
  55. package/src/hooks/lifecycle.ts +1 -0
  56. package/src/hooks/pattern-detector.ts +173 -173
  57. package/src/hooks/significance-filter.ts +250 -250
  58. package/src/llm/memory-manager.ts +328 -328
  59. package/src/llm/quality.ts +248 -248
  60. package/src/memory/attribution-guard.ts +249 -249
  61. package/src/memory/disclosure-policy.ts +135 -135
  62. package/src/memory/entity-extractor.ts +197 -197
  63. package/src/memory/formation/evaluate.ts +217 -217
  64. package/src/memory/freshness.ts +122 -122
  65. package/src/memory/graph.ts +197 -197
  66. package/src/memory/observations.ts +68 -18
  67. package/src/memory/refs.ts +94 -94
  68. package/src/memory/secret-filter.ts +79 -79
  69. package/src/memory/session.ts +13 -3
  70. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  71. package/src/orchestrate/adapters/claude.ts +111 -111
  72. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  73. package/src/orchestrate/adapters/codex.ts +41 -41
  74. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  75. package/src/orchestrate/adapters/gemini.ts +42 -42
  76. package/src/orchestrate/adapters/index.ts +73 -73
  77. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  78. package/src/orchestrate/adapters/opencode.ts +47 -47
  79. package/src/orchestrate/adapters/types.ts +77 -77
  80. package/src/orchestrate/capability-router.ts +284 -284
  81. package/src/orchestrate/cost-tracker.ts +219 -219
  82. package/src/orchestrate/error-recovery.ts +191 -191
  83. package/src/orchestrate/evidence.ts +140 -140
  84. package/src/orchestrate/ledger.ts +110 -110
  85. package/src/orchestrate/memorix-bridge.ts +343 -343
  86. package/src/orchestrate/output-budget.ts +80 -80
  87. package/src/orchestrate/permission.ts +152 -152
  88. package/src/orchestrate/pipeline-trace.ts +131 -131
  89. package/src/orchestrate/prompt-builder.ts +155 -155
  90. package/src/orchestrate/ring-buffer.ts +37 -37
  91. package/src/orchestrate/task-graph.ts +389 -389
  92. package/src/project/aliases.ts +374 -374
  93. package/src/project/launch-root.ts +115 -0
  94. package/src/rules/adapters/trae.ts +56 -56
  95. package/src/rules/syncer.ts +235 -235
  96. package/src/runtime/isolated-maintenance.ts +12 -1
  97. package/src/sdk.ts +12 -8
  98. package/src/search/query-expansion.ts +52 -52
  99. package/src/server/formation-timeout.ts +27 -27
  100. package/src/server.ts +38 -5
  101. package/src/skills/mini-skills.ts +386 -386
  102. package/src/store/chat-store.ts +119 -119
  103. package/src/store/graph-store.ts +249 -249
  104. package/src/store/mini-skill-store.ts +349 -349
  105. package/src/store/obs-store.ts +2 -2
  106. package/src/store/orama-store.ts +21 -3
  107. package/src/store/persistence-json.ts +212 -212
  108. package/src/store/persistence.ts +291 -291
  109. package/src/store/project-affinity.ts +195 -195
  110. package/src/store/sqlite-db.ts +2 -0
  111. package/src/store/sqlite-store.ts +19 -4
  112. package/src/team/event-bus.ts +76 -76
  113. package/src/team/handoff.ts +161 -161
  114. package/src/team/poll.ts +132 -132
  115. package/src/workspace/mcp-adapters/codex.ts +191 -191
  116. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  117. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  118. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  119. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  120. package/src/workspace/mcp-adapters/trae.ts +134 -134
  121. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  122. package/src/workspace/sanitizer.ts +60 -60
@@ -37,7 +37,7 @@ import {
37
37
  searchObservations,
38
38
  updateObservationMetadata,
39
39
  } from '../store/orama-store.js';
40
- import { getObservationStore, initObservationStore } from '../store/obs-store.js';
40
+ import { getObservationStore, initObservationStore, type ObservationStore } from '../store/obs-store.js';
41
41
  import { countTextTokens } from '../compact/token-budget.js';
42
42
  import { extractEntities, enrichConcepts } from './entity-extractor.js';
43
43
  import { getEmbeddingProvider, isEmbeddingExplicitlyDisabled, validateEmbeddingInput } from '../embedding/provider.js';
@@ -57,10 +57,32 @@ export interface ObservationRuntimeOptions {
57
57
  embeddingWriteMode?: ObservationEmbeddingWriteMode;
58
58
  /** Git root used by the detached vector worker. */
59
59
  projectRoot?: string;
60
+ /**
61
+ * Open SQLite and the ID counter without materializing the full corpus.
62
+ * Hook writes use disk atomic() + findByTopicKey; they must not loadAll()
63
+ * tens of thousands of rows on every PostToolUse.
64
+ */
65
+ skipCorpusLoad?: boolean;
60
66
  }
61
67
 
62
68
  let embeddingWriteMode: ObservationEmbeddingWriteMode = 'background';
63
69
  let embeddingWorkerProjectRoot: string | undefined;
70
+ let corpusLoaded = false;
71
+ let loadedObservationStore: ObservationStore | null = null;
72
+
73
+ /** @internal Reset in-process observation state between tests. */
74
+ export function resetObservationRuntime(): void {
75
+ observations = [];
76
+ nextId = 1;
77
+ projectDir = null;
78
+ searchIndexPrepared = false;
79
+ corpusLoaded = false;
80
+ loadedObservationStore = null;
81
+ embeddingWriteMode = 'background';
82
+ embeddingWorkerProjectRoot = undefined;
83
+ vectorMissingIds.clear();
84
+ vectorSchemaUpgradePromise = null;
85
+ }
64
86
 
65
87
  // ── Vector-missing tracking ──────────────────────────────────────
66
88
  // Tracks observation IDs whose async embedding write failed or was skipped.
@@ -233,14 +255,26 @@ export async function initObservations(
233
255
  ): Promise<void> {
234
256
  embeddingWriteMode = options.embeddingWriteMode ?? 'background';
235
257
  embeddingWorkerProjectRoot = options.projectRoot;
236
- if (projectDir === dir) return;
237
258
  await initObservationStore(dir);
238
259
  const store = getObservationStore();
239
- observations = await store.loadAll();
260
+
261
+ if (projectDir === dir && loadedObservationStore === store) {
262
+ if (options.skipCorpusLoad || corpusLoaded) return;
263
+ }
264
+
240
265
  nextId = await store.loadIdCounter();
241
266
  projectDir = dir;
242
267
  searchIndexPrepared = false;
243
268
  vectorSchemaUpgradePromise = null;
269
+ if (options.skipCorpusLoad) {
270
+ observations = [];
271
+ corpusLoaded = false;
272
+ loadedObservationStore = store;
273
+ return;
274
+ }
275
+ observations = await store.loadAll();
276
+ corpusLoaded = true;
277
+ loadedObservationStore = store;
244
278
  }
245
279
 
246
280
  function scheduleObservationEmbedding(input: {
@@ -252,14 +286,18 @@ function scheduleObservationEmbedding(input: {
252
286
  const { observationId, projectId, searchableText, doc } = input;
253
287
  vectorMissingIds.add(observationId);
254
288
 
289
+ // Ordinary store must not pay embedding-provider or cache I/O when vectors
290
+ // are disabled. Check before generateEmbedding() so MEMORIX_EMBEDDING=off
291
+ // stays on the lexical write path.
292
+ if (isEmbeddingExplicitlyDisabled()) {
293
+ vectorMissingIds.delete(observationId);
294
+ return;
295
+ }
296
+
255
297
  // A network fetch keeps Node's event loop alive even when its Promise is not
256
298
  // awaited. One-shot CLI commands therefore persist first and let a detached
257
299
  // worker consume the same durable queue.
258
300
  if (embeddingWriteMode === 'deferred') {
259
- if (isEmbeddingExplicitlyDisabled()) {
260
- vectorMissingIds.delete(observationId);
261
- return;
262
- }
263
301
  queueVectorBackfill(projectId, {
264
302
  detachedWorker: true,
265
303
  projectRoot: embeddingWorkerProjectRoot,
@@ -322,7 +360,7 @@ function scheduleObservationEmbedding(input: {
322
360
  * For DegradedBackend this is a no-op (always returns false).
323
361
  */
324
362
  export async function ensureFreshObservations(): Promise<boolean> {
325
- if (!projectDir) return false;
363
+ if (!projectDir || !corpusLoaded) return false;
326
364
  try {
327
365
  const store = getObservationStore();
328
366
  const wasStale = await store.ensureFresh();
@@ -424,12 +462,13 @@ export async function storeObservation(input: {
424
462
 
425
463
  // Sync the local cache before using it as the topicKey fast path. This costs a
426
464
  // generation read in the normal case and only reloads when another process wrote.
465
+ // Write-only / hook processes skip the corpus cache; disk findByTopicKey is authoritative.
427
466
  await ensureFreshObservations();
428
467
 
429
468
  // Topic key upsert: fast-path check in-memory (optimistic, may be stale).
430
469
  // A second authoritative check happens inside the file lock to prevent TOCTOU races
431
470
  // where two concurrent calls with the same topicKey both miss this check.
432
- if (input.topicKey) {
471
+ if (input.topicKey && corpusLoaded) {
433
472
  const existing = observations.find(
434
473
  o => o.topicKey === input.topicKey && o.projectId === input.projectId,
435
474
  );
@@ -546,14 +585,18 @@ export async function storeObservation(input: {
546
585
  observation.writeGeneration = store.getGeneration();
547
586
 
548
587
  if (upsertedInsideLock || reloadCacheAfterCommit) {
549
- observations = await store.loadAll();
550
- nextId = await store.loadIdCounter();
551
- if (upsertedInsideLock) {
552
- observation = observations.find((candidate) => candidate.id === observation.id)
553
- ?? observation;
588
+ if (corpusLoaded) {
589
+ observations = await store.loadAll();
590
+ nextId = await store.loadIdCounter();
591
+ if (upsertedInsideLock) {
592
+ observation = observations.find((candidate) => candidate.id === observation.id)
593
+ ?? observation;
594
+ }
595
+ } else {
596
+ nextId = await store.loadIdCounter();
554
597
  }
555
598
  } else {
556
- observations.push(observation);
599
+ if (corpusLoaded) observations.push(observation);
557
600
  nextId = observation.id + 1;
558
601
  }
559
602
 
@@ -627,7 +670,12 @@ export async function storeObservation(input: {
627
670
  sharedWithAgentIds: JSON.stringify(input.sharedWithAgentIds ?? []),
628
671
  };
629
672
 
630
- await insertObservation(doc);
673
+ // Hook / write-only processes persist SQLite only. The control plane
674
+ // picks the row up via ensureFresh. Creating Orama here would load the
675
+ // embedding provider and the on-disk vector cache in a 10s hook budget.
676
+ if (corpusLoaded) {
677
+ await insertObservation(doc);
678
+ }
631
679
  };
632
680
 
633
681
  await assignAndPersist();
@@ -1163,10 +1211,12 @@ export async function reindexObservations(): Promise<number> {
1163
1211
  * coupled to embedding provider throughput. Missing vectors are queued for the
1164
1212
  * existing background backfill cycle.
1165
1213
  */
1166
- export async function prepareSearchIndex(): Promise<number> {
1214
+ export async function prepareSearchIndex(options: { skipCachedVectors?: boolean } = {}): Promise<number> {
1167
1215
  if (searchIndexPrepared) return 0;
1168
1216
 
1169
- const count = await hydrateIndexForStartup(observations as unknown as any[]);
1217
+ const count = await hydrateIndexForStartup(observations as unknown as any[], {
1218
+ skipCachedVectors: options.skipCachedVectors,
1219
+ });
1170
1220
  if (count === 0) {
1171
1221
  searchIndexPrepared = true;
1172
1222
  return 0;
@@ -1,94 +1,94 @@
1
- /**
2
- * Typed Memory Reference Protocol (Phase 3a)
3
- *
4
- * Provides a formal, unambiguous way to reference memory objects
5
- * (observations and mini-skills) across internal code and the MCP API.
6
- *
7
- * String format:
8
- * obs:42 — observation #42
9
- * skill:3 — mini-skill #3
10
- * obs:42@org/proj — observation #42 in project org/proj
11
- *
12
- * Legacy support:
13
- * 42 (bare number) → obs:42
14
- * "42" (bare string) → obs:42
15
- *
16
- * Display short forms (presentation only):
17
- * #42 — observation
18
- * S3 — mini-skill
19
- */
20
-
21
- import type { MemoryRef } from '../types.js';
22
-
23
- // ── Parsing ──────────────────────────────────────────────────────
24
-
25
- const TYPED_REF_RE = /^(obs|skill):(\d+)(?:@(.+))?$/;
26
-
27
- /**
28
- * Parse a typed memory reference from a string or number.
29
- *
30
- * Accepts:
31
- * - "obs:42", "skill:3", "obs:42@org/proj"
32
- * - 42 (bare number → obs:42)
33
- * - "42" (bare numeric string → obs:42)
34
- *
35
- * Throws on invalid input.
36
- */
37
- export function parseMemoryRef(input: string | number): MemoryRef {
38
- // Bare number → legacy observation ref
39
- if (typeof input === 'number') {
40
- if (!Number.isInteger(input) || input < 0) {
41
- throw new Error(`Invalid memory ref: ${input} (must be a non-negative integer)`);
42
- }
43
- return { kind: 'obs', id: input };
44
- }
45
-
46
- const trimmed = input.trim();
47
-
48
- // Bare numeric string → legacy observation ref
49
- if (/^\d+$/.test(trimmed)) {
50
- return { kind: 'obs', id: parseInt(trimmed, 10) };
51
- }
52
-
53
- // Typed ref: obs:42 or skill:3 or obs:42@org/proj
54
- const match = trimmed.match(TYPED_REF_RE);
55
- if (!match) {
56
- throw new Error(
57
- `Invalid memory ref: "${input}". Expected format: obs:<id>, skill:<id>, obs:<id>@<projectId>, or a bare number.`,
58
- );
59
- }
60
-
61
- const kind = match[1] as 'obs' | 'skill';
62
- const id = parseInt(match[2], 10);
63
- const projectId = match[3] || undefined;
64
-
65
- return { kind, id, projectId };
66
- }
67
-
68
- // ── Serialization ────────────────────────────────────────────────
69
-
70
- /**
71
- * Serialize a MemoryRef to its canonical string form.
72
- *
73
- * Examples:
74
- * { kind: 'obs', id: 42 } → "obs:42"
75
- * { kind: 'skill', id: 3 } → "skill:3"
76
- * { kind: 'obs', id: 42, projectId: 'o/p' } → "obs:42@o/p"
77
- */
78
- export function serializeMemoryRef(ref: MemoryRef): string {
79
- const base = `${ref.kind}:${ref.id}`;
80
- return ref.projectId ? `${base}@${ref.projectId}` : base;
81
- }
82
-
83
- // ── Display ──────────────────────────────────────────────────────
84
-
85
- /**
86
- * Format a MemoryRef for human-readable display.
87
- *
88
- * Short forms:
89
- * obs:42 → "#42"
90
- * skill:3 → "S3"
91
- */
92
- export function displayRef(ref: MemoryRef): string {
93
- return ref.kind === 'obs' ? `#${ref.id}` : `S${ref.id}`;
94
- }
1
+ /**
2
+ * Typed Memory Reference Protocol (Phase 3a)
3
+ *
4
+ * Provides a formal, unambiguous way to reference memory objects
5
+ * (observations and mini-skills) across internal code and the MCP API.
6
+ *
7
+ * String format:
8
+ * obs:42 — observation #42
9
+ * skill:3 — mini-skill #3
10
+ * obs:42@org/proj — observation #42 in project org/proj
11
+ *
12
+ * Legacy support:
13
+ * 42 (bare number) → obs:42
14
+ * "42" (bare string) → obs:42
15
+ *
16
+ * Display short forms (presentation only):
17
+ * #42 — observation
18
+ * S3 — mini-skill
19
+ */
20
+
21
+ import type { MemoryRef } from '../types.js';
22
+
23
+ // ── Parsing ──────────────────────────────────────────────────────
24
+
25
+ const TYPED_REF_RE = /^(obs|skill):(\d+)(?:@(.+))?$/;
26
+
27
+ /**
28
+ * Parse a typed memory reference from a string or number.
29
+ *
30
+ * Accepts:
31
+ * - "obs:42", "skill:3", "obs:42@org/proj"
32
+ * - 42 (bare number → obs:42)
33
+ * - "42" (bare numeric string → obs:42)
34
+ *
35
+ * Throws on invalid input.
36
+ */
37
+ export function parseMemoryRef(input: string | number): MemoryRef {
38
+ // Bare number → legacy observation ref
39
+ if (typeof input === 'number') {
40
+ if (!Number.isInteger(input) || input < 0) {
41
+ throw new Error(`Invalid memory ref: ${input} (must be a non-negative integer)`);
42
+ }
43
+ return { kind: 'obs', id: input };
44
+ }
45
+
46
+ const trimmed = input.trim();
47
+
48
+ // Bare numeric string → legacy observation ref
49
+ if (/^\d+$/.test(trimmed)) {
50
+ return { kind: 'obs', id: parseInt(trimmed, 10) };
51
+ }
52
+
53
+ // Typed ref: obs:42 or skill:3 or obs:42@org/proj
54
+ const match = trimmed.match(TYPED_REF_RE);
55
+ if (!match) {
56
+ throw new Error(
57
+ `Invalid memory ref: "${input}". Expected format: obs:<id>, skill:<id>, obs:<id>@<projectId>, or a bare number.`,
58
+ );
59
+ }
60
+
61
+ const kind = match[1] as 'obs' | 'skill';
62
+ const id = parseInt(match[2], 10);
63
+ const projectId = match[3] || undefined;
64
+
65
+ return { kind, id, projectId };
66
+ }
67
+
68
+ // ── Serialization ────────────────────────────────────────────────
69
+
70
+ /**
71
+ * Serialize a MemoryRef to its canonical string form.
72
+ *
73
+ * Examples:
74
+ * { kind: 'obs', id: 42 } → "obs:42"
75
+ * { kind: 'skill', id: 3 } → "skill:3"
76
+ * { kind: 'obs', id: 42, projectId: 'o/p' } → "obs:42@o/p"
77
+ */
78
+ export function serializeMemoryRef(ref: MemoryRef): string {
79
+ const base = `${ref.kind}:${ref.id}`;
80
+ return ref.projectId ? `${base}@${ref.projectId}` : base;
81
+ }
82
+
83
+ // ── Display ──────────────────────────────────────────────────────
84
+
85
+ /**
86
+ * Format a MemoryRef for human-readable display.
87
+ *
88
+ * Short forms:
89
+ * obs:42 → "#42"
90
+ * skill:3 → "S3"
91
+ */
92
+ export function displayRef(ref: MemoryRef): string {
93
+ return ref.kind === 'obs' ? `#${ref.id}` : `S${ref.id}`;
94
+ }
@@ -1,79 +1,79 @@
1
- /**
2
- * Secret Filter
3
- *
4
- * Conservative credential detection and redaction for Memorix memory content.
5
- * Designed for low false-positive risk: only matches explicit credential
6
- * assignments (key=value / key: value), not generic discussion of auth concepts.
7
- *
8
- * sanitizeCredentials() — store-time: called inside storeObservation() before write
9
- * redactCredentials() — retrieval-time: called in format/output paths for legacy safety
10
- * containsCredential() — predicate used for testing and optional logging
11
- *
12
- * Both sanitize and redact share the same pattern logic. They are kept as
13
- * separate named exports so call-site semantics remain clear.
14
- */
15
-
16
- /**
17
- * Replaces the VALUE portion of matched credential patterns with [REDACTED].
18
- * The credential key name is preserved so context is not lost.
19
- *
20
- * Pattern categories:
21
- * 1. key=value / key: value forms — password, token, api_key, secret, etc.
22
- * Requires a real value (6+ non-whitespace chars) to avoid matching
23
- * discussion text like "password must be 8 chars".
24
- * 2. Bearer authorization header tokens (20+ chars)
25
- * 3. Structured token prefixes: GitHub (ghp_/ghs_/gho_), OpenAI (sk-), JWT (eyJ)
26
- */
27
- function applyRedaction(text: string): string {
28
- if (!text) return text;
29
- let out = text;
30
-
31
- // key=value / key: "value" / key='value' — preserve key, redact value
32
- out = out.replace(
33
- /((?:password|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret)\s*[:=]\s*["']?)([^\s"',;\n]{6,})/gi,
34
- '$1[REDACTED]',
35
- );
36
-
37
- // Bearer <token> — preserve "Bearer ", redact token
38
- out = out.replace(
39
- /(Bearer\s+)([A-Za-z0-9._\-/+]{20,})/g,
40
- '$1[REDACTED]',
41
- );
42
-
43
- // GitHub tokens (ghp_, ghs_, gho_, github_pat_)
44
- out = out.replace(/\b(?:ghp|ghs|gho|github_pat)_[A-Za-z0-9]{36,}\b/g, '[REDACTED]');
45
-
46
- // OpenAI / similar keys (sk-...)
47
- out = out.replace(/\bsk-[A-Za-z0-9T-]{20,}\b/g, '[REDACTED]');
48
-
49
- // JWT tokens (base64url header eyJ...)
50
- out = out.replace(/\beyJ[A-Za-z0-9._-]{40,}\b/g, '[REDACTED]');
51
-
52
- return out;
53
- }
54
-
55
- /**
56
- * Store-time sanitization: strips credential values before any durable write.
57
- * Called inside storeObservation() / upsertObservation() so that every write
58
- * path (hooks, git-ingest, CLI, reasoning, compact-on-write) is covered.
59
- */
60
- export function sanitizeCredentials(text: string): string {
61
- return applyRedaction(text);
62
- }
63
-
64
- /**
65
- * Retrieval-time redaction: masks credential values in display output.
66
- * Applied in all output formatters (detail, index table, timeline, session context)
67
- * as a safety net for legacy observations stored before sanitization was in place.
68
- */
69
- export function redactCredentials(text: string): string {
70
- return applyRedaction(text);
71
- }
72
-
73
- /**
74
- * Returns true if the text contains an obvious credential pattern.
75
- * Used for testing and optional diagnostic logging.
76
- */
77
- export function containsCredential(text: string): boolean {
78
- return applyRedaction(text) !== text;
79
- }
1
+ /**
2
+ * Secret Filter
3
+ *
4
+ * Conservative credential detection and redaction for Memorix memory content.
5
+ * Designed for low false-positive risk: only matches explicit credential
6
+ * assignments (key=value / key: value), not generic discussion of auth concepts.
7
+ *
8
+ * sanitizeCredentials() — store-time: called inside storeObservation() before write
9
+ * redactCredentials() — retrieval-time: called in format/output paths for legacy safety
10
+ * containsCredential() — predicate used for testing and optional logging
11
+ *
12
+ * Both sanitize and redact share the same pattern logic. They are kept as
13
+ * separate named exports so call-site semantics remain clear.
14
+ */
15
+
16
+ /**
17
+ * Replaces the VALUE portion of matched credential patterns with [REDACTED].
18
+ * The credential key name is preserved so context is not lost.
19
+ *
20
+ * Pattern categories:
21
+ * 1. key=value / key: value forms — password, token, api_key, secret, etc.
22
+ * Requires a real value (6+ non-whitespace chars) to avoid matching
23
+ * discussion text like "password must be 8 chars".
24
+ * 2. Bearer authorization header tokens (20+ chars)
25
+ * 3. Structured token prefixes: GitHub (ghp_/ghs_/gho_), OpenAI (sk-), JWT (eyJ)
26
+ */
27
+ function applyRedaction(text: string): string {
28
+ if (!text) return text;
29
+ let out = text;
30
+
31
+ // key=value / key: "value" / key='value' — preserve key, redact value
32
+ out = out.replace(
33
+ /((?:password|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret)\s*[:=]\s*["']?)([^\s"',;\n]{6,})/gi,
34
+ '$1[REDACTED]',
35
+ );
36
+
37
+ // Bearer <token> — preserve "Bearer ", redact token
38
+ out = out.replace(
39
+ /(Bearer\s+)([A-Za-z0-9._\-/+]{20,})/g,
40
+ '$1[REDACTED]',
41
+ );
42
+
43
+ // GitHub tokens (ghp_, ghs_, gho_, github_pat_)
44
+ out = out.replace(/\b(?:ghp|ghs|gho|github_pat)_[A-Za-z0-9]{36,}\b/g, '[REDACTED]');
45
+
46
+ // OpenAI / similar keys (sk-...)
47
+ out = out.replace(/\bsk-[A-Za-z0-9T-]{20,}\b/g, '[REDACTED]');
48
+
49
+ // JWT tokens (base64url header eyJ...)
50
+ out = out.replace(/\beyJ[A-Za-z0-9._-]{40,}\b/g, '[REDACTED]');
51
+
52
+ return out;
53
+ }
54
+
55
+ /**
56
+ * Store-time sanitization: strips credential values before any durable write.
57
+ * Called inside storeObservation() / upsertObservation() so that every write
58
+ * path (hooks, git-ingest, CLI, reasoning, compact-on-write) is covered.
59
+ */
60
+ export function sanitizeCredentials(text: string): string {
61
+ return applyRedaction(text);
62
+ }
63
+
64
+ /**
65
+ * Retrieval-time redaction: masks credential values in display output.
66
+ * Applied in all output formatters (detail, index table, timeline, session context)
67
+ * as a safety net for legacy observations stored before sanitization was in place.
68
+ */
69
+ export function redactCredentials(text: string): string {
70
+ return applyRedaction(text);
71
+ }
72
+
73
+ /**
74
+ * Returns true if the text contains an obvious credential pattern.
75
+ * Used for testing and optional diagnostic logging.
76
+ */
77
+ export function containsCredential(text: string): boolean {
78
+ return applyRedaction(text) !== text;
79
+ }
@@ -140,10 +140,20 @@ async function loadAliasActiveSessions(projectIds: Set<string>): Promise<Session
140
140
  return groups.flat();
141
141
  }
142
142
 
143
- async function loadAliasActiveObservations(projectIds: Set<string>): Promise<Observation[]> {
143
+ const SESSION_RESUME_OBSERVATION_LIMIT = 64;
144
+ const SESSION_CONTEXT_OBSERVATION_LIMIT = 128;
145
+
146
+ async function loadAliasActiveObservations(
147
+ projectIds: Set<string>,
148
+ limit: number = SESSION_RESUME_OBSERVATION_LIMIT,
149
+ ): Promise<Observation[]> {
144
150
  const store = getObservationStore();
145
151
  const groups = await Promise.all(
146
- [...projectIds].map((projectId) => store.loadByProject(projectId, { status: 'active' })),
152
+ [...projectIds].map((projectId) => store.loadByProject(projectId, {
153
+ status: 'active',
154
+ limit,
155
+ newestFirst: true,
156
+ })),
147
157
  );
148
158
  return groups.flat();
149
159
  }
@@ -522,7 +532,7 @@ export async function getSessionContext(
522
532
  const aliasSet = await resolveProjectIds(projectId);
523
533
  const [sessions, allObs] = await Promise.all([
524
534
  loadAliasSessions(aliasSet),
525
- loadAliasActiveObservations(aliasSet),
535
+ loadAliasActiveObservations(aliasSet, SESSION_CONTEXT_OBSERVATION_LIMIT),
526
536
  ]);
527
537
  const readableObs = readableAliasObservations(allObs, aliasSet, reader);
528
538
  /** Check if a session summary contains noise/system-self content */