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.
- package/CHANGELOG.md +35 -0
- package/TEAM.md +86 -86
- package/dist/cli/index.js +2582 -1845
- package/dist/index.js +1355 -683
- package/dist/maintenance-runner.js +1621 -1096
- package/dist/media-audio-runner.js +1612 -1088
- package/dist/media-video-runner.js +1612 -1088
- package/dist/memcode-runtime/CHANGELOG.md +35 -0
- package/dist/sdk.js +2007 -1396
- package/dist/vector-backfill-runner.js +1610 -1086
- package/docs/DESIGN_DECISIONS.md +357 -357
- package/docs/PERFORMANCE.md +15 -0
- package/package.json +2 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/audit/index.ts +156 -156
- package/src/cli/commands/background.ts +51 -5
- package/src/cli/commands/formation.ts +48 -48
- package/src/cli/commands/git-hook-install.ts +111 -111
- package/src/cli/commands/handoff.ts +54 -54
- package/src/cli/commands/hooks-status.ts +63 -63
- package/src/cli/commands/ingest.ts +44 -44
- package/src/cli/commands/integrate-shared.ts +15 -15
- package/src/cli/commands/lock.ts +82 -82
- package/src/cli/commands/message.ts +104 -104
- package/src/cli/commands/operator-shared.ts +18 -11
- package/src/cli/commands/poll.ts +58 -58
- package/src/cli/commands/reasoning.ts +118 -118
- package/src/cli/commands/serve-http.ts +29 -7
- package/src/cli/commands/serve-shared.ts +49 -6
- package/src/cli/commands/serve.ts +4 -0
- package/src/cli/commands/skills.ts +114 -114
- package/src/cli/commands/task.ts +167 -167
- package/src/cli/commands/transfer.ts +47 -47
- package/src/cli/index.ts +406 -406
- package/src/cli/tui/ChatView.tsx +234 -234
- package/src/cli/tui/CommandBar.tsx +312 -312
- package/src/cli/tui/ContextRail.tsx +118 -118
- package/src/cli/tui/HeaderBar.tsx +72 -72
- package/src/cli/tui/LogoBanner.tsx +51 -51
- package/src/cli/tui/index.ts +41 -41
- package/src/cli/version.ts +7 -7
- package/src/cli/workbench.ts +1 -1
- package/src/dashboard/project-classification.ts +64 -64
- package/src/embedding/api-provider.ts +503 -27
- package/src/embedding/provider.ts +20 -3
- package/src/git/hooks-path.ts +85 -85
- package/src/hooks/lifecycle.ts +1 -0
- package/src/hooks/pattern-detector.ts +173 -173
- package/src/hooks/significance-filter.ts +250 -250
- package/src/llm/memory-manager.ts +328 -328
- package/src/llm/quality.ts +248 -248
- package/src/memory/attribution-guard.ts +249 -249
- package/src/memory/disclosure-policy.ts +135 -135
- package/src/memory/entity-extractor.ts +197 -197
- package/src/memory/formation/evaluate.ts +217 -217
- package/src/memory/freshness.ts +122 -122
- package/src/memory/graph.ts +197 -197
- package/src/memory/observations.ts +68 -18
- package/src/memory/refs.ts +94 -94
- package/src/memory/secret-filter.ts +79 -79
- package/src/memory/session.ts +13 -3
- package/src/orchestrate/adapters/claude-stream.ts +192 -192
- package/src/orchestrate/adapters/claude.ts +111 -111
- package/src/orchestrate/adapters/codex-stream.ts +134 -134
- package/src/orchestrate/adapters/codex.ts +41 -41
- package/src/orchestrate/adapters/gemini-stream.ts +166 -166
- package/src/orchestrate/adapters/gemini.ts +42 -42
- package/src/orchestrate/adapters/index.ts +73 -73
- package/src/orchestrate/adapters/opencode-stream.ts +143 -143
- package/src/orchestrate/adapters/opencode.ts +47 -47
- package/src/orchestrate/adapters/types.ts +77 -77
- package/src/orchestrate/capability-router.ts +284 -284
- package/src/orchestrate/cost-tracker.ts +219 -219
- package/src/orchestrate/error-recovery.ts +191 -191
- package/src/orchestrate/evidence.ts +140 -140
- package/src/orchestrate/ledger.ts +110 -110
- package/src/orchestrate/memorix-bridge.ts +343 -343
- package/src/orchestrate/output-budget.ts +80 -80
- package/src/orchestrate/permission.ts +152 -152
- package/src/orchestrate/pipeline-trace.ts +131 -131
- package/src/orchestrate/prompt-builder.ts +155 -155
- package/src/orchestrate/ring-buffer.ts +37 -37
- package/src/orchestrate/task-graph.ts +389 -389
- package/src/project/aliases.ts +374 -374
- package/src/project/launch-root.ts +115 -0
- package/src/rules/adapters/trae.ts +56 -56
- package/src/rules/syncer.ts +235 -235
- package/src/runtime/isolated-maintenance.ts +12 -1
- package/src/sdk.ts +12 -8
- package/src/search/query-expansion.ts +52 -52
- package/src/server/formation-timeout.ts +27 -27
- package/src/server.ts +38 -5
- package/src/skills/mini-skills.ts +386 -386
- package/src/store/chat-store.ts +119 -119
- package/src/store/graph-store.ts +249 -249
- package/src/store/mini-skill-store.ts +349 -349
- package/src/store/obs-store.ts +2 -2
- package/src/store/orama-store.ts +21 -3
- package/src/store/persistence-json.ts +212 -212
- package/src/store/persistence.ts +291 -291
- package/src/store/project-affinity.ts +195 -195
- package/src/store/sqlite-db.ts +2 -0
- package/src/store/sqlite-store.ts +19 -4
- package/src/team/event-bus.ts +76 -76
- package/src/team/handoff.ts +161 -161
- package/src/team/poll.ts +132 -132
- package/src/workspace/mcp-adapters/codex.ts +191 -191
- package/src/workspace/mcp-adapters/copilot.ts +105 -105
- package/src/workspace/mcp-adapters/cursor.ts +53 -53
- package/src/workspace/mcp-adapters/kiro.ts +64 -64
- package/src/workspace/mcp-adapters/opencode.ts +123 -123
- package/src/workspace/mcp-adapters/trae.ts +134 -134
- package/src/workspace/mcp-adapters/windsurf.ts +91 -91
- 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
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
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;
|
package/src/memory/refs.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/memory/session.ts
CHANGED
|
@@ -140,10 +140,20 @@ async function loadAliasActiveSessions(projectIds: Set<string>): Promise<Session
|
|
|
140
140
|
return groups.flat();
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
|
|
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, {
|
|
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 */
|