@aion0/forge 0.9.16 → 0.9.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.9.16",
3
+ "version": "0.9.19",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Phase A §15.4 self-check — LocalMemoryStore must satisfy the
3
+ * summarizer's needs end-to-end without Temper.
4
+ *
5
+ * pnpm tsx scripts/test-memory-local.ts
6
+ *
7
+ * Constructs LocalMemoryStore directly (bypasses getMemoryStore so the
8
+ * user's settings don't matter — we always exercise the local SQLite
9
+ * path). Writes summary + fact + cursor + health blocks via the key
10
+ * helpers, then asserts:
11
+ * 1. Summarizer happy-path round trip works (put → get → search)
12
+ * 2. buildMemoryContext recalls user-relevant blocks via search
13
+ * 3. Cursor putBlock(k, v1) then putBlock(k, v2) replaces, not appends
14
+ * 4. INTERNAL_KEY_PREFIXES filtering keeps cursor/health out of context
15
+ */
16
+
17
+ import { LocalMemoryStore } from '../lib/chat/local-memory';
18
+ import { buildMemoryContext } from '../lib/chat/build-memory-context';
19
+ import {
20
+ cursorKey,
21
+ factKey,
22
+ healthKey,
23
+ stableHash,
24
+ summaryKey,
25
+ INTERNAL_KEY_PREFIXES,
26
+ type CursorValue,
27
+ } from '../lib/memory/keys';
28
+
29
+ const NS = '__phaseA_selfcheck__';
30
+ const SID = 'sid-test-1';
31
+ const NOW = Date.now();
32
+
33
+ async function main() {
34
+ const store = new LocalMemoryStore(NS);
35
+ console.log(`Backend: ${store.kind} ns=${store.currentNamespace}`);
36
+
37
+ let failures = 0;
38
+ const fail = (msg: string) => { console.log(` ✗ ${msg}`); failures += 1; };
39
+ const pass = (msg: string) => console.log(` ✓ ${msg}`);
40
+
41
+ // ── 1. Summarizer happy-path round trip ──────────────────────────
42
+ console.log('Test 1 — summarizer round trip');
43
+ const sk = summaryKey(SID, NOW);
44
+ await store.putBlock(
45
+ sk,
46
+ {
47
+ text: 'User asked about Forge architecture. Discussed memory layer + summarizer design.',
48
+ from_ts: NOW - 1000, to_ts: NOW, message_count: 10,
49
+ model: 'haiku', provider: 'anthropic', ingest_ts: NOW,
50
+ },
51
+ { description: 'session summary', scope: 'own' },
52
+ );
53
+
54
+ const fk = factKey('user', 'zliu', stableHash('prefers terse responses with code'));
55
+ await store.putBlock(
56
+ fk,
57
+ {
58
+ content: 'prefers terse responses with code',
59
+ subject_kind: 'preference', subject: 'zliu',
60
+ source_ref: `chat:${SID}@${NOW}`, confidence: null,
61
+ extracted_by: 'summarizer',
62
+ },
63
+ { description: 'prefers terse responses with code', scope: 'own' },
64
+ );
65
+
66
+ const sb = await store.getBlock(sk);
67
+ const fb = await store.getBlock(fk);
68
+ if (sb?.value) pass(`summary readable at ${sk}`); else fail('summary block missing');
69
+ if (fb?.value) pass(`fact readable at ${fk}`); else fail('fact block missing');
70
+
71
+ // ── 2. buildMemoryContext recalls relevant content ───────────────
72
+ console.log('Test 2 — buildMemoryContext recalls summary via search');
73
+ const ctxA = await buildMemoryContext({
74
+ store,
75
+ currentUserMessage: 'tell me about the memory summarizer',
76
+ });
77
+ if (ctxA.hits.length > 0) pass(`got ${ctxA.hits.length} hit(s) for query`);
78
+ else fail('expected search hits for "memory summarizer" query, got 0');
79
+ const hitText = ctxA.hits.map((h) => h.fact ?? '').join(' | ');
80
+ if (/summarizer|memory layer/i.test(hitText)) pass('hit contains expected keywords');
81
+ else fail(`hit text didn't match: ${hitText.slice(0, 200)}`);
82
+
83
+ // ── 3. Cursor upsert replaces ────────────────────────────────────
84
+ console.log('Test 3 — cursor upsert replaces');
85
+ const ck = cursorKey(SID);
86
+ const v1: CursorValue = { last_ingested_ts: 1, last_run_ts: 1, ingest_count: 1 };
87
+ const v2: CursorValue = { last_ingested_ts: 999, last_run_ts: 999, ingest_count: 2 };
88
+ await store.putBlock(ck, v1);
89
+ await store.putBlock(ck, v2);
90
+ const after = (await store.getBlock(ck))?.value as CursorValue | undefined;
91
+ if (after?.last_ingested_ts === 999) pass('cursor replaced (v2 wins)');
92
+ else fail(`expected last_ingested_ts=999, got ${after?.last_ingested_ts}`);
93
+ const rows = (await store.listBlocks()).filter((b) => b.key === ck);
94
+ if (rows.length === 1) pass('cursor row count = 1 (no append)');
95
+ else fail(`expected 1 cursor row, got ${rows.length}`);
96
+
97
+ // ── 4. INTERNAL_KEY_PREFIXES filters cursor/health out of context ─
98
+ console.log('Test 4 — buildMemoryContext excludes cursor/health by prefix');
99
+ await store.putBlock(healthKey(SID), { last_run_ts: NOW, error: null, ingest_count: 1, last_token_estimate: 100 });
100
+
101
+ // Make cursor + health look attractive to search by giving the user
102
+ // message a literal token they'd LIKE-match.
103
+ const ctxB = await buildMemoryContext({
104
+ store,
105
+ currentUserMessage: 'summarizer cursor health status',
106
+ });
107
+ const allKeys = [
108
+ ...ctxB.blocks.map((b) => b.key),
109
+ ...ctxB.hits.map((h) => h.id),
110
+ ];
111
+ const leaked = allKeys.filter((k) =>
112
+ INTERNAL_KEY_PREFIXES.some((p) =>
113
+ k.startsWith(p) || k.startsWith('block:' + p),
114
+ ),
115
+ );
116
+ if (leaked.length === 0) pass('no cursor/health leaked into context');
117
+ else fail(`leaked internal blocks: ${leaked.join(', ')}`);
118
+
119
+ // Sanity: render output must not literally contain the cursor key
120
+ if (!ctxB.text.includes('forge.summarizer.cursor:') && !ctxB.text.includes('forge.summarizer.health:')) {
121
+ pass('rendered context does not contain internal prefixes');
122
+ } else {
123
+ fail('rendered context still mentions internal prefix');
124
+ }
125
+
126
+ console.log('');
127
+ if (failures === 0) {
128
+ console.log('✓ Phase A LocalMemoryStore self-check passed.');
129
+ process.exit(0);
130
+ } else {
131
+ console.log(`✗ ${failures} check(s) failed.`);
132
+ process.exit(1);
133
+ }
134
+ }
135
+
136
+ main().catch((err) => {
137
+ console.error('Self-check crashed:', err);
138
+ process.exit(2);
139
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Memory store upsert idempotency check.
3
+ *
4
+ * pnpm tsx scripts/test-memory-upsert.ts
5
+ *
6
+ * Verifies putBlock semantics needed by the chat summarizer:
7
+ * - Same key written twice → second value wins (replace, not append)
8
+ * - listBlocks shows one row, not two
9
+ * - factKey() produces stable hashes across runs
10
+ *
11
+ * Runs against whichever backend getMemoryStore() picks — typically
12
+ * LocalMemoryStore unless Temper creds are set. Writes to a sentinel
13
+ * namespace key prefix and cleans up after itself.
14
+ */
15
+
16
+ import { getMemoryStore } from '../lib/chat/memory-store';
17
+ import {
18
+ cursorKey,
19
+ factKey,
20
+ summaryKey,
21
+ healthKey,
22
+ stableHash,
23
+ type CursorValue,
24
+ } from '../lib/memory/keys';
25
+
26
+ const SENTINEL_SESSION = '__upsert_test__';
27
+
28
+ async function main() {
29
+ const store = getMemoryStore();
30
+ console.log(`Backend: ${store.kind} (enabled=${store.enabled})`);
31
+ if (!store.enabled) {
32
+ console.error('Store not enabled — cannot run test.');
33
+ process.exit(1);
34
+ }
35
+
36
+ let failures = 0;
37
+ const fail = (msg: string) => {
38
+ console.log(` ✗ ${msg}`);
39
+ failures += 1;
40
+ };
41
+ const pass = (msg: string) => console.log(` ✓ ${msg}`);
42
+
43
+ // ── 1. cursor upsert: v1 then v2 → v2 wins ───────────────────────
44
+ console.log('Test 1 — cursor upsert');
45
+ const ck = cursorKey(SENTINEL_SESSION);
46
+ const v1: CursorValue = { last_ingested_ts: 100, last_run_ts: 200, ingest_count: 1 };
47
+ const v2: CursorValue = { last_ingested_ts: 500, last_run_ts: 600, ingest_count: 2 };
48
+ await store.putBlock(ck, v1);
49
+ await store.putBlock(ck, v2);
50
+ const read = (await store.getBlock(ck))?.value as CursorValue | undefined;
51
+ if (!read) fail('getBlock returned null after putBlock');
52
+ else if (read.last_ingested_ts !== 500) fail(`expected last_ingested_ts=500, got ${read.last_ingested_ts}`);
53
+ else if (read.ingest_count !== 2) fail(`expected ingest_count=2, got ${read.ingest_count}`);
54
+ else pass('second putBlock replaced first');
55
+
56
+ // ── 2. listBlocks has one row for the key, not two ───────────────
57
+ console.log('Test 2 — listBlocks dedup');
58
+ const all = await store.listBlocks();
59
+ const matching = all.filter((b) => b.key === ck);
60
+ if (matching.length === 1) pass(`exactly one row for ${ck}`);
61
+ else fail(`expected 1 row for ${ck}, got ${matching.length}`);
62
+
63
+ // ── 3. factKey is stable across calls ────────────────────────────
64
+ console.log('Test 3 — factKey stable hash');
65
+ const fk1 = factKey('user', 'zliu', stableHash('prefers terse responses'));
66
+ const fk2 = factKey('user', 'zliu', stableHash('prefers terse responses'));
67
+ if (fk1 === fk2) pass(`same content → same key (${fk1})`);
68
+ else fail(`hash drift: ${fk1} vs ${fk2}`);
69
+
70
+ // Different content → different key
71
+ const fk3 = factKey('user', 'zliu', stableHash('uses Chinese in chat'));
72
+ if (fk3 !== fk1) pass('different content → different key');
73
+ else fail('hash collision: distinct content produced same key');
74
+
75
+ // ── 4. summary / health keys round-trip ──────────────────────────
76
+ console.log('Test 4 — summary/health keys round-trip');
77
+ const sk = summaryKey(SENTINEL_SESSION, 12345);
78
+ const hk = healthKey(SENTINEL_SESSION);
79
+ await store.putBlock(sk, { text: 'hello', from_ts: 1, to_ts: 12345, message_count: 5, model: 'm', provider: 'p', ingest_ts: Date.now() });
80
+ await store.putBlock(hk, { last_run_ts: Date.now(), error: null, ingest_count: 1, last_token_estimate: 100 });
81
+ const sb = await store.getBlock(sk);
82
+ const hb = await store.getBlock(hk);
83
+ if (sb) pass('summary block round-tripped');
84
+ else fail('summary block missing after putBlock');
85
+ if (hb) pass('health block round-tripped');
86
+ else fail('health block missing after putBlock');
87
+
88
+ // ── cleanup ──────────────────────────────────────────────────────
89
+ console.log('Cleanup: removing sentinel blocks');
90
+ // No deleteBlock on the interface — leave them; sentinel ns prefix
91
+ // makes them identifiable. Re-running the test overwrites in place.
92
+
93
+ console.log('');
94
+ if (failures === 0) {
95
+ console.log('✓ All checks passed.');
96
+ process.exit(0);
97
+ } else {
98
+ console.log(`✗ ${failures} check(s) failed.`);
99
+ process.exit(1);
100
+ }
101
+ }
102
+
103
+ main().catch((err) => {
104
+ console.error('Test crashed:', err);
105
+ process.exit(2);
106
+ });