@remnic/core 9.3.705 → 9.3.707
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/dist/access-boundary.d.ts +5 -5
- package/dist/access-boundary.js +10 -10
- package/dist/access-cli.js +26 -25
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +14 -14
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +13 -13
- package/dist/access-operations-batch.js +11 -11
- package/dist/access-operations.d.ts +5 -5
- package/dist/access-operations.js +12 -12
- package/dist/{access-service-C8hc8N2i.d.ts → access-service-j1c1gptF.d.ts} +3 -3
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +9 -9
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +5 -5
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-CAeU0e1C.d.ts → catalog-CKxilpzS.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +6 -6
- package/dist/{chunk-JZCKDTAS.js → chunk-24FGNOQS.js} +3 -3
- package/dist/{chunk-C2EHWNP2.js → chunk-2VQYHHWB.js} +9 -3
- package/dist/chunk-2VQYHHWB.js.map +1 -0
- package/dist/{chunk-47LPHBIP.js → chunk-3PTOZJOI.js} +2 -2
- package/dist/{chunk-2SYPOX45.js → chunk-4DZATVK5.js} +3 -3
- package/dist/{chunk-S5APSA3U.js → chunk-4E2QCH46.js} +2 -2
- package/dist/{chunk-KKK7YTYN.js → chunk-5GSUBCZM.js} +6 -2
- package/dist/chunk-5GSUBCZM.js.map +1 -0
- package/dist/{chunk-5DTWZ66X.js → chunk-66TSLESZ.js} +2 -2
- package/dist/{chunk-IREVVFTM.js → chunk-6CIAGZ6X.js} +9 -1
- package/dist/chunk-6CIAGZ6X.js.map +1 -0
- package/dist/{chunk-Y4PMATX2.js → chunk-6HPJMR5I.js} +2 -2
- package/dist/{chunk-M5MWSG4S.js → chunk-BGAHTI4C.js} +5 -5
- package/dist/{chunk-OM6ETLLH.js → chunk-C63MK3WL.js} +2 -2
- package/dist/{chunk-GT4VBKM4.js → chunk-EJDAXR7O.js} +5 -5
- package/dist/{chunk-ULVM34NP.js → chunk-ESMY4RJ4.js} +2 -2
- package/dist/{chunk-MNH2OODZ.js → chunk-GKI6LX5L.js} +2 -2
- package/dist/{chunk-KZMMAVR2.js → chunk-HIV5E57C.js} +2 -2
- package/dist/{chunk-SFOAQQDJ.js → chunk-IONFO7UK.js} +3 -3
- package/dist/{chunk-BMKGG7GC.js → chunk-IVCQW4C4.js} +2 -2
- package/dist/chunk-M7R5R6OJ.js +355 -0
- package/dist/chunk-M7R5R6OJ.js.map +1 -0
- package/dist/{chunk-IX3UQT4H.js → chunk-MLLTU5FX.js} +1 -1
- package/dist/chunk-MLLTU5FX.js.map +1 -0
- package/dist/{chunk-NFGJBEPT.js → chunk-MRX6ZXHZ.js} +2 -2
- package/dist/{chunk-GI3MTYDU.js → chunk-N75N5SNX.js} +3 -3
- package/dist/{chunk-BKBSE3BU.js → chunk-NM6TSEGQ.js} +2 -2
- package/dist/{chunk-S3WYRPSA.js → chunk-PDJQVAGM.js} +68 -31
- package/dist/chunk-PDJQVAGM.js.map +1 -0
- package/dist/{chunk-SY6WG6DB.js → chunk-RH2OSRQY.js} +2 -2
- package/dist/{chunk-YTMDF6S7.js → chunk-SPETAWFE.js} +2 -2
- package/dist/{chunk-JHAYR2JY.js → chunk-STYMKPFT.js} +7 -7
- package/dist/{chunk-OMNTQPZO.js → chunk-TKZ3AL5V.js} +2 -2
- package/dist/{chunk-IFR23TC5.js → chunk-VYZNWH4J.js} +37 -6
- package/dist/chunk-VYZNWH4J.js.map +1 -0
- package/dist/{chunk-KFU2SKDD.js → chunk-W2S3Z5MT.js} +2 -2
- package/dist/{chunk-6GJ2WQRK.js → chunk-W2WQ4LE7.js} +2 -2
- package/dist/{chunk-7ARKLLCV.js → chunk-WDH3KUZU.js} +2 -2
- package/dist/{chunk-7L54YZUI.js → chunk-WQADZ3ZY.js} +17 -17
- package/dist/{chunk-HEHPVBWA.js → chunk-WSWYIKXX.js} +4 -4
- package/dist/{chunk-WN4GHSDH.js → chunk-XKMDDM7P.js} +2 -2
- package/dist/{chunk-LY4FJLPT.js → chunk-Y6YHAGQP.js} +2 -2
- package/dist/{cli-ChE0XlJY.d.ts → cli-CbT-pyM4.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +26 -26
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +5 -5
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +5 -5
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +5 -5
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +5 -5
- package/dist/entity-schema.d.ts +1 -1
- package/dist/event-time.d.ts +35 -1
- package/dist/event-time.js +6 -320
- package/dist/event-time.js.map +1 -1
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +10 -1
- package/dist/extraction.js +2 -2
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/{forget-6SOIPUMQ.js → forget-U66KPDPY.js} +3 -3
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +35 -34
- package/dist/index.js.map +1 -1
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +5 -5
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
- package/dist/maintenance/rebuild-memory-projection.js +6 -6
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +2 -2
- package/dist/namespaces/migrate.js +6 -6
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +5 -5
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +10 -10
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +7 -7
- package/dist/{orchestrator-m_PnhyHj.d.ts → orchestrator-D4ovYV3x.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +20 -19
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/schemas.d.ts +60 -22
- package/dist/schemas.js +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-DVBbq6Wr.d.ts → semantic-consolidation-DyMUCsfN.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +6 -6
- package/dist/semantic-rule-promotion.js +5 -5
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +5 -5
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +5 -1
- package/dist/storage.js +4 -4
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-supersession.js +2 -2
- package/dist/temporal-validity.d.ts +2 -2
- package/dist/temporal-validity.js +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/{types-CJrvHJjx.d.ts → types-Couvz-L3.d.ts} +12 -2
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +5 -5
- package/package.json +2 -2
- package/src/event-time.ts +73 -0
- package/src/extraction-eventtime-wiring.test.ts +269 -0
- package/src/extraction.ts +50 -3
- package/src/issue-1492-coding-mode-consistency.test.ts +315 -0
- package/src/orchestrator.ts +71 -17
- package/src/schemas.ts +12 -0
- package/src/storage.ts +13 -2
- package/src/temporal-validity.test.ts +33 -0
- package/src/temporal-validity.ts +24 -1
- package/src/types.ts +12 -2
- package/dist/chunk-C2EHWNP2.js.map +0 -1
- package/dist/chunk-IFR23TC5.js.map +0 -1
- package/dist/chunk-IREVVFTM.js.map +0 -1
- package/dist/chunk-IX3UQT4H.js.map +0 -1
- package/dist/chunk-KKK7YTYN.js.map +0 -1
- package/dist/chunk-S3WYRPSA.js.map +0 -1
- /package/dist/{chunk-JZCKDTAS.js.map → chunk-24FGNOQS.js.map} +0 -0
- /package/dist/{chunk-47LPHBIP.js.map → chunk-3PTOZJOI.js.map} +0 -0
- /package/dist/{chunk-2SYPOX45.js.map → chunk-4DZATVK5.js.map} +0 -0
- /package/dist/{chunk-S5APSA3U.js.map → chunk-4E2QCH46.js.map} +0 -0
- /package/dist/{chunk-5DTWZ66X.js.map → chunk-66TSLESZ.js.map} +0 -0
- /package/dist/{chunk-Y4PMATX2.js.map → chunk-6HPJMR5I.js.map} +0 -0
- /package/dist/{chunk-M5MWSG4S.js.map → chunk-BGAHTI4C.js.map} +0 -0
- /package/dist/{chunk-OM6ETLLH.js.map → chunk-C63MK3WL.js.map} +0 -0
- /package/dist/{chunk-GT4VBKM4.js.map → chunk-EJDAXR7O.js.map} +0 -0
- /package/dist/{chunk-ULVM34NP.js.map → chunk-ESMY4RJ4.js.map} +0 -0
- /package/dist/{chunk-MNH2OODZ.js.map → chunk-GKI6LX5L.js.map} +0 -0
- /package/dist/{chunk-KZMMAVR2.js.map → chunk-HIV5E57C.js.map} +0 -0
- /package/dist/{chunk-SFOAQQDJ.js.map → chunk-IONFO7UK.js.map} +0 -0
- /package/dist/{chunk-BMKGG7GC.js.map → chunk-IVCQW4C4.js.map} +0 -0
- /package/dist/{chunk-NFGJBEPT.js.map → chunk-MRX6ZXHZ.js.map} +0 -0
- /package/dist/{chunk-GI3MTYDU.js.map → chunk-N75N5SNX.js.map} +0 -0
- /package/dist/{chunk-BKBSE3BU.js.map → chunk-NM6TSEGQ.js.map} +0 -0
- /package/dist/{chunk-SY6WG6DB.js.map → chunk-RH2OSRQY.js.map} +0 -0
- /package/dist/{chunk-YTMDF6S7.js.map → chunk-SPETAWFE.js.map} +0 -0
- /package/dist/{chunk-JHAYR2JY.js.map → chunk-STYMKPFT.js.map} +0 -0
- /package/dist/{chunk-OMNTQPZO.js.map → chunk-TKZ3AL5V.js.map} +0 -0
- /package/dist/{chunk-KFU2SKDD.js.map → chunk-W2S3Z5MT.js.map} +0 -0
- /package/dist/{chunk-6GJ2WQRK.js.map → chunk-W2WQ4LE7.js.map} +0 -0
- /package/dist/{chunk-7ARKLLCV.js.map → chunk-WDH3KUZU.js.map} +0 -0
- /package/dist/{chunk-7L54YZUI.js.map → chunk-WQADZ3ZY.js.map} +0 -0
- /package/dist/{chunk-HEHPVBWA.js.map → chunk-WSWYIKXX.js.map} +0 -0
- /package/dist/{chunk-WN4GHSDH.js.map → chunk-XKMDDM7P.js.map} +0 -0
- /package/dist/{chunk-LY4FJLPT.js.map → chunk-Y6YHAGQP.js.map} +0 -0
- /package/dist/{forget-6SOIPUMQ.js.map → forget-U66KPDPY.js.map} +0 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
6
|
+
|
|
7
|
+
import { resolveFactEventTime } from "./event-time.js";
|
|
8
|
+
import { ExtractionEngine } from "./extraction.js";
|
|
9
|
+
import { ExtractedFactSchema } from "./schemas.js";
|
|
10
|
+
import { StorageManager } from "./storage.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Issue #1578 PR 2 — extraction event-time wiring.
|
|
14
|
+
*
|
|
15
|
+
* Proves three contracts:
|
|
16
|
+
* 1. resolveFactEventTime anchors to the SOURCE TURN timestamp, never
|
|
17
|
+
* wall-clock — replay/import of an old transcript yields old-era event
|
|
18
|
+
* times (the core bi-temporal invariant from the #1578 design).
|
|
19
|
+
* 2. Extraction normalization passes the per-fact `eventTime` expression
|
|
20
|
+
* through from raw LLM JSON (camelCase and snake_case payloads) so the
|
|
21
|
+
* persist path can resolve it at write time.
|
|
22
|
+
* 3. Storage round-trips `invalid_at` so the bi-temporal interval survives
|
|
23
|
+
* write → read (complementing PR 1's observedAt/eventTimeSource test).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// ── 1. Replay anchoring ──────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
test("resolveFactEventTime: replay of a 2025 transcript anchors 'last March' to 2025-03, not today", () => {
|
|
29
|
+
// The anchor is the source turn timestamp from an old (2025) transcript.
|
|
30
|
+
const anchor2025 = "2025-04-10T12:00:00.000Z";
|
|
31
|
+
const result = resolveFactEventTime("last March", anchor2025);
|
|
32
|
+
assert.equal(result.eventTimeSource, "extracted");
|
|
33
|
+
assert.ok(result.validFrom, "validFrom must be set for a resolvable expression");
|
|
34
|
+
// "last March" relative to April 2025 → March 2025.
|
|
35
|
+
assert.ok(result.validFrom!.startsWith("2025-03"), `expected 2025-03, got ${result.validFrom}`);
|
|
36
|
+
// Critical bi-temporal invariant: NOT anchored to today's wall-clock.
|
|
37
|
+
assert.ok(!result.validFrom!.startsWith("2026"), "must not anchor to wall-clock");
|
|
38
|
+
assert.equal(result.observedAt, anchor2025);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("resolveFactEventTime: same expression, different anchors → different validFrom", () => {
|
|
42
|
+
// The identical phrase resolves differently depending on the anchor —
|
|
43
|
+
// proving the resolver is anchor-driven, not Date.now()-driven.
|
|
44
|
+
const old = resolveFactEventTime("yesterday", "2025-01-15T10:00:00.000Z");
|
|
45
|
+
const recent = resolveFactEventTime("yesterday", "2026-07-05T10:00:00.000Z");
|
|
46
|
+
assert.ok(old.validFrom!.startsWith("2025-01-14"), `got ${old.validFrom}`);
|
|
47
|
+
assert.ok(recent.validFrom!.startsWith("2026-07-04"), `got ${recent.validFrom}`);
|
|
48
|
+
assert.notEqual(old.validFrom, recent.validFrom);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("resolveFactEventTime: absent expression → 'assumed' with validFrom = anchor", () => {
|
|
52
|
+
const anchor = "2025-06-01T00:00:00.000Z";
|
|
53
|
+
const result = resolveFactEventTime(undefined, anchor);
|
|
54
|
+
assert.equal(result.eventTimeSource, "assumed");
|
|
55
|
+
assert.equal(result.validFrom, anchor);
|
|
56
|
+
assert.equal(result.observedAt, anchor);
|
|
57
|
+
assert.equal(result.validUntil, undefined);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("resolveFactEventTime: 'since 2024' → validFrom only (open-ended start)", () => {
|
|
61
|
+
const result = resolveFactEventTime("since 2024", "2025-06-01T00:00:00.000Z");
|
|
62
|
+
assert.equal(result.eventTimeSource, "extracted");
|
|
63
|
+
assert.ok(result.validFrom!.startsWith("2024-01-01"));
|
|
64
|
+
assert.equal(result.validUntil, undefined);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("resolveFactEventTime: unresolvable garbage → 'assumed' fallback", () => {
|
|
68
|
+
const anchor = "2025-06-01T00:00:00.000Z";
|
|
69
|
+
const result = resolveFactEventTime("sometime maybe???", anchor);
|
|
70
|
+
assert.equal(result.eventTimeSource, "assumed");
|
|
71
|
+
assert.equal(result.validFrom, anchor);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ── 2. Extraction normalization passes eventTime through ─────────────────
|
|
75
|
+
|
|
76
|
+
function makeEngine(): ExtractionEngine {
|
|
77
|
+
// Minimal construction: the facts normalizer is a pure data transform
|
|
78
|
+
// (no LLM / I/O). Stub the optional deps so the constructor doesn't
|
|
79
|
+
// spin up real clients.
|
|
80
|
+
return new ExtractionEngine(
|
|
81
|
+
{ memoryDir: "/tmp" } as any,
|
|
82
|
+
undefined, // profiler → safe default
|
|
83
|
+
{} as any, // localLlm stub
|
|
84
|
+
undefined, // gatewayConfig
|
|
85
|
+
{} as any, // modelRegistry stub
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
test("extraction normalization: camelCase 'eventTime' passes through to ExtractedFact", () => {
|
|
90
|
+
const engine = makeEngine();
|
|
91
|
+
const result = (engine as any).normalizeExtractionResultPayload({
|
|
92
|
+
facts: [
|
|
93
|
+
{ content: "We moved offices in March.", category: "fact", eventTime: "last March" },
|
|
94
|
+
],
|
|
95
|
+
entities: [],
|
|
96
|
+
});
|
|
97
|
+
assert.equal(result.facts.length, 1);
|
|
98
|
+
assert.equal(result.facts[0].eventTime, "last March");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("extraction normalization: snake_case 'event_time' also captured", () => {
|
|
102
|
+
const engine = makeEngine();
|
|
103
|
+
const result = (engine as any).normalizeExtractionResultPayload({
|
|
104
|
+
facts: [
|
|
105
|
+
{ content: "Switched to PostgreSQL.", category: "fact", event_time: "2025-01-15" },
|
|
106
|
+
],
|
|
107
|
+
entities: [],
|
|
108
|
+
});
|
|
109
|
+
assert.equal(result.facts[0].eventTime, "2025-01-15");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("extraction normalization: absent eventTime → undefined (assumed deferred to write time)", () => {
|
|
113
|
+
const engine = makeEngine();
|
|
114
|
+
const result = (engine as any).normalizeExtractionResultPayload({
|
|
115
|
+
facts: [{ content: "The sky is blue.", category: "fact" }],
|
|
116
|
+
entities: [],
|
|
117
|
+
});
|
|
118
|
+
assert.equal(result.facts[0].eventTime, undefined);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── 2b. Schema preserves snake_case event_time (gateway fix, chatgpt-codex) ─
|
|
122
|
+
|
|
123
|
+
test("ExtractedFactSchema: snake_case event_time survives schema parse (gateway strip-unknown fix)", () => {
|
|
124
|
+
// Zod .object() strips unknown keys by default. Before the event_time alias
|
|
125
|
+
// was added, the gateway fallback path (parseWithSchemaDetailed → schema.parse)
|
|
126
|
+
// silently dropped event_time, losing the temporal bound (#1578).
|
|
127
|
+
const parsed = ExtractedFactSchema.parse({
|
|
128
|
+
content: "Switched to PostgreSQL.",
|
|
129
|
+
category: "fact",
|
|
130
|
+
confidence: 0.9,
|
|
131
|
+
tags: [],
|
|
132
|
+
event_time: "2025-01-15",
|
|
133
|
+
});
|
|
134
|
+
assert.equal(parsed.event_time, "2025-01-15");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("ExtractedFactSchema: camelCase eventTime also survives schema parse (no regression)", () => {
|
|
138
|
+
const parsed = ExtractedFactSchema.parse({
|
|
139
|
+
content: "We moved offices in March.",
|
|
140
|
+
category: "fact",
|
|
141
|
+
confidence: 0.8,
|
|
142
|
+
tags: [],
|
|
143
|
+
eventTime: "2025-03",
|
|
144
|
+
});
|
|
145
|
+
assert.equal(parsed.eventTime, "2025-03");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ── 3. Storage round-trips invalid_at ────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
test("storage: invalid_at round-trips through writeMemoryFrontmatter → readAllMemories", async () => {
|
|
151
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-et-invalidAt-"));
|
|
152
|
+
try {
|
|
153
|
+
const storage = new StorageManager(dir);
|
|
154
|
+
await storage.ensureDirectories();
|
|
155
|
+
|
|
156
|
+
const id = await storage.writeMemory("fact", "Old stack was Node 18.", {
|
|
157
|
+
validAt: "2025-01-01T00:00:00.000Z",
|
|
158
|
+
observedAt: "2025-06-01T00:00:00.000Z",
|
|
159
|
+
eventTimeSource: "extracted",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const before = await storage.readAllMemories();
|
|
163
|
+
const mem = before.find((m) => m.frontmatter.id === id);
|
|
164
|
+
assert.ok(mem);
|
|
165
|
+
|
|
166
|
+
const ok = await storage.writeMemoryFrontmatter(mem!, {
|
|
167
|
+
invalid_at: "2025-06-15T00:00:00.000Z",
|
|
168
|
+
});
|
|
169
|
+
assert.equal(ok, true);
|
|
170
|
+
|
|
171
|
+
const after = await storage.readAllMemories();
|
|
172
|
+
const patched = after.find((m) => m.frontmatter.id === id);
|
|
173
|
+
assert.ok(patched, "patched fact must be readable");
|
|
174
|
+
assert.equal(patched!.frontmatter.invalid_at, "2025-06-15T00:00:00.000Z");
|
|
175
|
+
// Bi-temporal provenance preserved through the frontmatter patch.
|
|
176
|
+
assert.equal(patched!.frontmatter.observedAt, "2025-06-01T00:00:00.000Z");
|
|
177
|
+
assert.equal(patched!.frontmatter.eventTimeSource, "extracted");
|
|
178
|
+
assert.equal(patched!.frontmatter.valid_at, "2025-01-01T00:00:00.000Z");
|
|
179
|
+
} finally {
|
|
180
|
+
await rm(dir, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("storage: writeMemory accepts invalidAt directly (end-bound at extraction write time)", async () => {
|
|
185
|
+
// Proves Fix C: writeMemory now accepts invalidAt so "until X" expressions
|
|
186
|
+
// persist invalid_at at write time without a separate updateMemoryFrontmatter call.
|
|
187
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-et-writeMem-invalid-"));
|
|
188
|
+
try {
|
|
189
|
+
const storage = new StorageManager(dir);
|
|
190
|
+
await storage.ensureDirectories();
|
|
191
|
+
const id = await storage.writeMemory("fact", "We used MongoDB until June 2025.", {
|
|
192
|
+
validAt: "2024-01-01T00:00:00.000Z",
|
|
193
|
+
invalidAt: "2025-06-01T00:00:00.000Z",
|
|
194
|
+
observedAt: "2025-06-20T00:00:00.000Z",
|
|
195
|
+
eventTimeSource: "extracted",
|
|
196
|
+
});
|
|
197
|
+
const mems = await storage.readAllMemories();
|
|
198
|
+
const mem = mems.find((m) => m.frontmatter.id === id);
|
|
199
|
+
assert.ok(mem);
|
|
200
|
+
assert.equal(mem!.frontmatter.valid_at, "2024-01-01T00:00:00.000Z");
|
|
201
|
+
assert.equal(mem!.frontmatter.invalid_at, "2025-06-01T00:00:00.000Z");
|
|
202
|
+
assert.equal(mem!.frontmatter.observedAt, "2025-06-20T00:00:00.000Z");
|
|
203
|
+
assert.equal(mem!.frontmatter.eventTimeSource, "extracted");
|
|
204
|
+
} finally {
|
|
205
|
+
await rm(dir, { recursive: true, force: true });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("resolveFactEventTime: 'through 2026' → validUntil only (open-ended end bound)", () => {
|
|
210
|
+
const result = resolveFactEventTime("through 2026", "2025-06-20T00:00:00.000Z");
|
|
211
|
+
assert.equal(result.eventTimeSource, "extracted");
|
|
212
|
+
assert.equal(result.validFrom, undefined);
|
|
213
|
+
assert.ok(result.validUntil, "validUntil must be set for an end-bound expression");
|
|
214
|
+
// "through 2026" → exclusive end = start of 2027.
|
|
215
|
+
assert.equal(result.validUntil, "2027-01-01T00:00:00.000Z");
|
|
216
|
+
});
|
|
217
|
+
test("storage: writeChunk propagates invalidAt/observedAt/eventTimeSource from the parent fact (#1578 PR2)", async () => {
|
|
218
|
+
// Proves the cursor-bugbot fix: an independently-surfaced chunk expires at the
|
|
219
|
+
// same invalid_at window as its parent so recall's isValidityExpiredNow check
|
|
220
|
+
// fires on chunks too, not just the chunked parent write.
|
|
221
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-et-chunk-invalid-"));
|
|
222
|
+
try {
|
|
223
|
+
const storage = new StorageManager(dir);
|
|
224
|
+
await storage.ensureDirectories();
|
|
225
|
+
const parentId = await storage.writeMemory("fact", "We used MongoDB until June 2025.", {
|
|
226
|
+
validAt: "2024-01-01T00:00:00.000Z",
|
|
227
|
+
invalidAt: "2025-06-01T00:00:00.000Z",
|
|
228
|
+
observedAt: "2025-06-20T00:00:00.000Z",
|
|
229
|
+
eventTimeSource: "extracted",
|
|
230
|
+
});
|
|
231
|
+
const chunkId = await storage.writeChunk(
|
|
232
|
+
parentId,
|
|
233
|
+
0,
|
|
234
|
+
1,
|
|
235
|
+
"fact",
|
|
236
|
+
"We used MongoDB until June 2025.",
|
|
237
|
+
{
|
|
238
|
+
validAt: "2024-01-01T00:00:00.000Z",
|
|
239
|
+
invalidAt: "2025-06-01T00:00:00.000Z",
|
|
240
|
+
observedAt: "2025-06-20T00:00:00.000Z",
|
|
241
|
+
eventTimeSource: "extracted",
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
const mems = await storage.readAllMemories();
|
|
245
|
+
const chunk = mems.find((m) => m.frontmatter.id === chunkId);
|
|
246
|
+
assert.ok(chunk, "chunk must be readable");
|
|
247
|
+
assert.equal(chunk!.frontmatter.valid_at, "2024-01-01T00:00:00.000Z");
|
|
248
|
+
assert.equal(chunk!.frontmatter.invalid_at, "2025-06-01T00:00:00.000Z",
|
|
249
|
+
"chunk must carry the parent's end bound so it expires independently");
|
|
250
|
+
assert.equal(chunk!.frontmatter.observedAt, "2025-06-20T00:00:00.000Z");
|
|
251
|
+
assert.equal(chunk!.frontmatter.eventTimeSource, "extracted");
|
|
252
|
+
} finally {
|
|
253
|
+
await rm(dir, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
test("resolveFactEventTime: 'until 2025-06' (YYYY-MM) → invalidAt = start of following month (#1578 r3)", () => {
|
|
257
|
+
const result = resolveFactEventTime("until 2025-06", "2025-06-20T00:00:00.000Z");
|
|
258
|
+
assert.equal(result.eventTimeSource, "extracted");
|
|
259
|
+
assert.equal(result.validFrom, undefined);
|
|
260
|
+
assert.equal(result.validUntil, "2025-07-01T00:00:00.000Z",
|
|
261
|
+
"YYYY-MM end bound must span the whole month, not fall back to assumed");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("resolveFactEventTime: bare '2025-03' (YYYY-MM) → validFrom = first of month (#1578 r3)", () => {
|
|
265
|
+
const result = resolveFactEventTime("2025-03", "2025-06-20T00:00:00.000Z");
|
|
266
|
+
assert.equal(result.eventTimeSource, "extracted");
|
|
267
|
+
assert.equal(result.validFrom, "2025-03-01T00:00:00.000Z");
|
|
268
|
+
assert.equal(result.validUntil, undefined);
|
|
269
|
+
});
|
package/src/extraction.ts
CHANGED
|
@@ -279,6 +279,12 @@ export class ExtractionEngine {
|
|
|
279
279
|
typeof f?.quote === "string" && f.quote.trim().length > 0
|
|
280
280
|
? f.quote
|
|
281
281
|
: undefined,
|
|
282
|
+
eventTime:
|
|
283
|
+
typeof f?.eventTime === "string" && f.eventTime.trim().length > 0
|
|
284
|
+
? f.eventTime.trim()
|
|
285
|
+
: typeof f?.event_time === "string" && f.event_time.trim().length > 0
|
|
286
|
+
? f.event_time.trim()
|
|
287
|
+
: undefined,
|
|
282
288
|
}))
|
|
283
289
|
.filter((f: any) => f.content.length > 0)
|
|
284
290
|
: [];
|
|
@@ -750,6 +756,11 @@ export class ExtractionEngine {
|
|
|
750
756
|
this.config.provenance?.enabled
|
|
751
757
|
? '- Source quotes: For each fact, include a "quote" field with the EXACT verbatim words from the conversation that support the fact (a contiguous span from a single turn, not a paraphrase). Cap at ~300 chars.'
|
|
752
758
|
: "",
|
|
759
|
+
// #1578: emit the same event-time guidance as the primary extraction
|
|
760
|
+
// paths so proactive-recovered facts also carry an optional eventTime
|
|
761
|
+
// (chatgpt-codex thread on extraction.ts:1607). Returns "" when the
|
|
762
|
+
// bi-temporal gate is off, keeping the prompt unchanged by default.
|
|
763
|
+
this.eventTimePromptInstruction(),
|
|
753
764
|
"",
|
|
754
765
|
"Base extracted facts (do not repeat):",
|
|
755
766
|
factsPreview || "(none)",
|
|
@@ -1250,10 +1261,23 @@ export class ExtractionEngine {
|
|
|
1250
1261
|
// reasoningTrace through normalizeReasoningTrace before passing it on so
|
|
1251
1262
|
// gateway output matches the shape local/direct-client paths produce.
|
|
1252
1263
|
const normalizedFacts = result.facts.map((f: any) => {
|
|
1253
|
-
if (!f
|
|
1264
|
+
if (!f) return f;
|
|
1265
|
+
// Gateway tolerance: collapse snake_case event_time → camelCase
|
|
1266
|
+
// eventTime so the gateway path matches the local/direct/proactive
|
|
1267
|
+
// normalization (#1578 r3 — cursor bugbot).
|
|
1268
|
+
const eventTime =
|
|
1269
|
+
typeof f.eventTime === "string" && f.eventTime.trim().length > 0
|
|
1270
|
+
? f.eventTime.trim()
|
|
1271
|
+
: typeof f.event_time === "string" && f.event_time.trim().length > 0
|
|
1272
|
+
? f.event_time.trim()
|
|
1273
|
+
: undefined;
|
|
1274
|
+
if (!f.reasoningTrace && eventTime === undefined) return f;
|
|
1254
1275
|
return {
|
|
1255
1276
|
...f,
|
|
1256
|
-
|
|
1277
|
+
...(f.reasoningTrace
|
|
1278
|
+
? { reasoningTrace: normalizeReasoningTrace(f.reasoningTrace) ?? undefined }
|
|
1279
|
+
: {}),
|
|
1280
|
+
...(eventTime !== undefined ? { eventTime } : {}),
|
|
1257
1281
|
};
|
|
1258
1282
|
});
|
|
1259
1283
|
const sanitized = this.sanitizeExtractionResult({
|
|
@@ -1394,7 +1418,7 @@ Examples of when to add structuredAttributes:
|
|
|
1394
1418
|
- Decisions: {"chosen": "PostgreSQL", "rejected": "MongoDB", "reason": "ACID compliance"}
|
|
1395
1419
|
- Quantities/measurements: {"budget": "50000", "team_size": "5", "deadline": "2024-06-01"}
|
|
1396
1420
|
Only add structuredAttributes when there are concrete values. Skip for abstract or narrative facts.
|
|
1397
|
-
|
|
1421
|
+
${this.eventTimePromptInstruction()}
|
|
1398
1422
|
Also generate:
|
|
1399
1423
|
1. 1-3 genuine questions you're curious about from this conversation
|
|
1400
1424
|
2. Profile updates about user patterns/behaviors (if any)
|
|
@@ -1595,6 +1619,28 @@ ${truncatedConversation}`;
|
|
|
1595
1619
|
return { facts, entities, profileUpdates: [], questions: [] };
|
|
1596
1620
|
}
|
|
1597
1621
|
|
|
1622
|
+
/**
|
|
1623
|
+
* Bi-temporal event-time extraction instruction (#1578 PR2). Emitted on
|
|
1624
|
+
* every extraction entry path when `temporal.biTemporal` is on so the LLM
|
|
1625
|
+
* emits an optional per-fact `eventTime` expression. The expression is
|
|
1626
|
+
* resolved against the source turn timestamp at write time — never
|
|
1627
|
+
* wall-clock — so replay/import of old transcripts anchors correctly.
|
|
1628
|
+
* Returns an empty string when the gate is off (byte-identical prompt).
|
|
1629
|
+
*/
|
|
1630
|
+
private eventTimePromptInstruction(): string {
|
|
1631
|
+
if (!this.config.temporalBiTemporal) return "";
|
|
1632
|
+
return `
|
|
1633
|
+
=== Event Time (bi-temporal) ===
|
|
1634
|
+
When a fact has an explicit temporal anchor — a date, month, season, or relative time expression stating WHEN the fact became (or stopped being) true — capture it verbatim in an "eventTime" field on that fact. Examples:
|
|
1635
|
+
- "We moved offices in March" → "eventTime": "last March"
|
|
1636
|
+
- "The API has been rate-limited since 2024" → "eventTime": "since 2024"
|
|
1637
|
+
- "I switched to PostgreSQL on 2025-01-15" → "eventTime": "2025-01-15"
|
|
1638
|
+
- "We used MongoDB until June 2025" → "eventTime": "until 2025-06"
|
|
1639
|
+
Accepted forms: ISO dates ("2025-03-01"), year-month ("2025-03"), month/season + year ("March 2025", "summer 2024"), relative ("yesterday", "last week", "this month", "next year", "last December"), and open-ended ("since 2024", "until 2025-06").
|
|
1640
|
+
Omit "eventTime" when the fact has no explicit temporal anchor — do NOT guess or infer dates. The system resolves the expression against the conversation's own timestamp, not today's date.
|
|
1641
|
+
`;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1598
1644
|
/**
|
|
1599
1645
|
* Build extraction instructions shared between local and cloud LLM.
|
|
1600
1646
|
*/
|
|
@@ -1663,6 +1709,7 @@ ${existingEntities.join(", ")}
|
|
|
1663
1709
|
|
|
1664
1710
|
When you see something that matches a known entity, use THAT name exactly. Only create a NEW entity if nothing in this list represents it.
|
|
1665
1711
|
` : ""}
|
|
1712
|
+
${this.eventTimePromptInstruction()}
|
|
1666
1713
|
Also extract relationships between entities mentioned in the conversation.
|
|
1667
1714
|
- Format: {source: "entity-name", target: "entity-name", label: "relationship description"}
|
|
1668
1715
|
- Max 5 relationships per extraction
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1492 — Remnic Coding mode inconsistencies.
|
|
3
|
+
*
|
|
4
|
+
* Cross-subsystem characterization of the three coding-mode drifts an external
|
|
5
|
+
* reporter found when wiring per-repo/project namespace isolation for a shared
|
|
6
|
+
* multi-user Remnic deployment. Each reported symptom was fixed by a dedicated
|
|
7
|
+
* child issue; this file is the single #1492-named anchor that ties them
|
|
8
|
+
* together so a regression in ANY of the three subsystems is caught and
|
|
9
|
+
* attributed here.
|
|
10
|
+
*
|
|
11
|
+
* Symptom A (observe handler ignored coding context)
|
|
12
|
+
* → fixed by #1495: observe resolves ONE effective scope plan
|
|
13
|
+
* (`resolveMemoryScopePlan`) whose `writeNamespace` is byte-identical to
|
|
14
|
+
* what `memory_store`/`suggestion_submit` resolve via
|
|
15
|
+
* `resolveCodingScopedWriteNamespace`, so observed turns and explicit
|
|
16
|
+
* writes on the same project-scoped session land in the SAME store a
|
|
17
|
+
* same-session recall reads. Deep plan-level parity is guarded by
|
|
18
|
+
* `access-service-observe-scope.test.ts` (#1495). Here we pin the
|
|
19
|
+
* coding-namespace derivation invariant the drift violated: the scoped
|
|
20
|
+
* namespace is built by exactly ONE `combineNamespaces(principal, overlay)`
|
|
21
|
+
* and is stable/idempotent — never a second ad-hoc combination that could
|
|
22
|
+
* drift between the write and read paths.
|
|
23
|
+
*
|
|
24
|
+
* Symptom B (transcript/summarizer session-key parser conflated sessions)
|
|
25
|
+
* → fixed by #1496: arbitrary session keys (`pi-geek:abc123`, `pi:abc123`,
|
|
26
|
+
* `myuser:abc123`) are first-class `session/<hash>` identities — never the
|
|
27
|
+
* legacy `other/default` bucket — for BOTH transcript.ts and summarizer.ts.
|
|
28
|
+
*
|
|
29
|
+
* Symptom C (collectActiveMemoryPaths scanned only 4 of 12+ category dirs)
|
|
30
|
+
* → fixed by #1497: the QMD-unavailable filesystem fallback recall scans
|
|
31
|
+
* `RECALL_FALLBACK_DIRS` (every recall category dir), so a memory written
|
|
32
|
+
* to e.g. `preferences/` is found when QMD is disabled/missing/unhealthy.
|
|
33
|
+
*
|
|
34
|
+
* The architectural gaps the reporter also called out — the maintenance
|
|
35
|
+
* pipeline being blind to dynamic project-scoped namespaces (#1500), and the
|
|
36
|
+
* admin/webui not surfacing coding context (#1502) — were tracked by their own
|
|
37
|
+
* issues and are both CLOSED.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import assert from "node:assert/strict";
|
|
41
|
+
import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
|
|
42
|
+
import type { Dirent } from "node:fs";
|
|
43
|
+
import os from "node:os";
|
|
44
|
+
import path from "node:path";
|
|
45
|
+
import test from "node:test";
|
|
46
|
+
|
|
47
|
+
import {
|
|
48
|
+
combineNamespaces,
|
|
49
|
+
projectNamespaceName,
|
|
50
|
+
projectTagProjectId,
|
|
51
|
+
resolveCodingNamespaceOverlay,
|
|
52
|
+
} from "./coding/coding-namespace.js";
|
|
53
|
+
import {
|
|
54
|
+
SESSION_CHANNEL_TYPE,
|
|
55
|
+
parseSessionIdentity,
|
|
56
|
+
sessionStoragePaths,
|
|
57
|
+
} from "./session-identity.js";
|
|
58
|
+
import { HourlySummarizer } from "./summarizer.js";
|
|
59
|
+
import type { HourlySummary, PluginConfig } from "./types.js";
|
|
60
|
+
import { StorageManager } from "./storage.js";
|
|
61
|
+
import { RECALL_FALLBACK_DIRS } from "./utils/category-dir.js";
|
|
62
|
+
|
|
63
|
+
/** Recursively collect the immediate child directory names under `root` that
|
|
64
|
+
* contain at least one file, into `out`. Used to verify the summarizer wrote
|
|
65
|
+
* each session's summary into its OWN dir (no conflation). */
|
|
66
|
+
async function walkCollectDirs(root: string, out: Set<string>): Promise<void> {
|
|
67
|
+
let entries: Dirent[];
|
|
68
|
+
try {
|
|
69
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
70
|
+
} catch {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (!entry.isDirectory()) continue;
|
|
75
|
+
const child = path.join(root, entry.name);
|
|
76
|
+
let childEntries: Dirent[];
|
|
77
|
+
try {
|
|
78
|
+
childEntries = await readdir(child, { withFileTypes: true });
|
|
79
|
+
} catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (childEntries.some((e) => e.isFile())) {
|
|
83
|
+
out.add(entry.name);
|
|
84
|
+
} else {
|
|
85
|
+
await walkCollectDirs(child, out);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Symptom A: coding-namespace derivation is stable across write/read ────
|
|
91
|
+
|
|
92
|
+
test("#1492 Symptom A: a project-scoped session derives ONE stable overlay namespace — the write/read convergence point", () => {
|
|
93
|
+
// The reporter's scenario: user "pi-geek" in project "Blend/Supply". Both the
|
|
94
|
+
// observe write path and the recall read path must resolve the SAME scoped
|
|
95
|
+
// namespace. They do so by combining the principal base with the project
|
|
96
|
+
// overlay through exactly ONE helper (combineNamespaces); a second ad-hoc
|
|
97
|
+
// combination is what caused the original drift.
|
|
98
|
+
const principal = "pi-geek";
|
|
99
|
+
const projectId = projectTagProjectId("Blend/Supply");
|
|
100
|
+
const codingContext = {
|
|
101
|
+
projectId,
|
|
102
|
+
branch: null,
|
|
103
|
+
rootPath: projectId,
|
|
104
|
+
defaultBranch: null,
|
|
105
|
+
};
|
|
106
|
+
const overlay = resolveCodingNamespaceOverlay(
|
|
107
|
+
codingContext,
|
|
108
|
+
{ projectScope: true, branchScope: false, globalFallback: false },
|
|
109
|
+
"default",
|
|
110
|
+
);
|
|
111
|
+
assert.ok(overlay, "projectScope=true must yield a coding overlay");
|
|
112
|
+
|
|
113
|
+
const scopedWrite = combineNamespaces(principal, overlay.namespace);
|
|
114
|
+
const scopedRead = combineNamespaces(principal, overlay.namespace);
|
|
115
|
+
|
|
116
|
+
// Write and read converge on the identical scoped namespace.
|
|
117
|
+
assert.equal(scopedRead, scopedWrite);
|
|
118
|
+
// It is well-formed: principal base + project overlay, not the flat default.
|
|
119
|
+
assert.equal(scopedWrite, combineNamespaces(principal, projectNamespaceName(projectId)));
|
|
120
|
+
assert.ok(
|
|
121
|
+
scopedWrite.startsWith(`${principal}-`),
|
|
122
|
+
"scoped namespace must be principal-prefixed, preserving per-user isolation",
|
|
123
|
+
);
|
|
124
|
+
assert.notEqual(scopedWrite, principal, "must layer the project overlay onto the principal base");
|
|
125
|
+
assert.notEqual(scopedWrite, "default", "must not collapse to the flat default namespace");
|
|
126
|
+
// Two principals in the same repo get distinct scoped namespaces.
|
|
127
|
+
const other = combineNamespaces("pi-friend", overlay.namespace);
|
|
128
|
+
assert.notEqual(scopedWrite, other, "per-user isolation: distinct principals stay distinct");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("#1492 Symptom A: combineNamespaces is idempotent under re-derivation (no double-combination drift)", () => {
|
|
132
|
+
// A read path that (incorrectly) re-combined an already-scoped namespace with
|
|
133
|
+
// the overlay would produce `pi-friend-project-x-project-x` and miss the data.
|
|
134
|
+
// Guard the single-combination contract: combineNamespaces joins base+overlay
|
|
135
|
+
// with `-`, so re-combining must change the result (drift detector).
|
|
136
|
+
const principal = "pi-friend";
|
|
137
|
+
const overlayNs = projectNamespaceName(projectTagProjectId("Remnic"));
|
|
138
|
+
const once = combineNamespaces(principal, overlayNs);
|
|
139
|
+
const twice = combineNamespaces(once, overlayNs);
|
|
140
|
+
assert.notEqual(once, twice, "re-combining a scoped namespace must change it (drift detector)");
|
|
141
|
+
assert.ok(once.startsWith(`${principal}-`), "scoped namespace is principal-prefixed");
|
|
142
|
+
assert.ok(!once.includes(`${overlayNs}-${overlayNs}`), "no duplicated overlay fragment");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ─── Symptom B: arbitrary session keys never conflate ──────────────────────
|
|
146
|
+
|
|
147
|
+
test("#1492 Symptom B: arbitrary session keys get distinct transcript AND summarizer paths, never other/default", async () => {
|
|
148
|
+
// Exact keys from the issue body. Pre-fix these all collapsed into
|
|
149
|
+
// transcripts/other/default/*.jsonl AND summaries/other/default/*.md,
|
|
150
|
+
// conflating every non-agent-prefixed session in BOTH subsystems.
|
|
151
|
+
const keys = ["pi-geek:abc123", "pi:abc123", "myuser:abc123", "pi-friend:def456"];
|
|
152
|
+
|
|
153
|
+
// ── Transcript path (sessionStoragePaths, consumed by transcript.ts) ──
|
|
154
|
+
const transcriptDirs = new Set<string>();
|
|
155
|
+
for (const key of keys) {
|
|
156
|
+
const id = parseSessionIdentity(key);
|
|
157
|
+
assert.equal(id.legacy, false, `${key} must not be treated as a legacy agent: key`);
|
|
158
|
+
assert.equal(id.channelType, SESSION_CHANNEL_TYPE, `${key} channelType must be "session"`);
|
|
159
|
+
assert.notEqual(id.channelType, "other", `${key} must not fall back to "other"`);
|
|
160
|
+
assert.notEqual(id.channelId, "default", `${key} must not fall back to "default"`);
|
|
161
|
+
const paths = sessionStoragePaths(key);
|
|
162
|
+
assert.match(paths.dir, /^session\/[0-9a-f]{16}$/, `${key} transcript dir must be session/<hash>`);
|
|
163
|
+
transcriptDirs.add(paths.dir);
|
|
164
|
+
}
|
|
165
|
+
assert.equal(transcriptDirs.size, keys.length, "each session key must map to a distinct transcript dir");
|
|
166
|
+
|
|
167
|
+
// ── Summarizer path: exercise the REAL HourlySummarizer.saveSummary() so a
|
|
168
|
+
// regression in summarizer.ts itself (reverting to the old parsed-session
|
|
169
|
+
// bucket) is caught here, not just a regression in session-identity.ts.
|
|
170
|
+
// Pre-fix the summarizer derived its dir from the same brittle split(':')
|
|
171
|
+
// parser; this round-trip proves the written summary files land in
|
|
172
|
+
// distinct per-session dirs (no other/default conflation). ──
|
|
173
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "engram-1492-sum-"));
|
|
174
|
+
try {
|
|
175
|
+
const config = {
|
|
176
|
+
memoryDir,
|
|
177
|
+
localLlmEnabled: false,
|
|
178
|
+
localLlmFallback: false,
|
|
179
|
+
localLlmUrl: "http://localhost:1234/v1",
|
|
180
|
+
localLlmModel: "local-model",
|
|
181
|
+
} as PluginConfig;
|
|
182
|
+
const summarizer = new HourlySummarizer(config);
|
|
183
|
+
|
|
184
|
+
const summary: HourlySummary = {
|
|
185
|
+
hour: "2026-06-28T14:00:00.000Z",
|
|
186
|
+
sessionKey: "", // set per key below
|
|
187
|
+
bullets: ["worked on shared memory pool"],
|
|
188
|
+
turnCount: 2,
|
|
189
|
+
generatedAt: "2026-06-28T14:05:00.000Z",
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
for (const key of keys) {
|
|
193
|
+
await summarizer.saveSummary({ ...summary, sessionKey: key });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Walk summaries/hourly/ and collect the distinct per-session dirs that
|
|
197
|
+
// actually received a file. Each must be the encoded session key, never
|
|
198
|
+
// "other" or "default".
|
|
199
|
+
const hourlyRoot = path.join(memoryDir, "summaries", "hourly");
|
|
200
|
+
const sessionDirs = new Set<string>();
|
|
201
|
+
await walkCollectDirs(hourlyRoot, sessionDirs);
|
|
202
|
+
assert.ok(sessionDirs.size >= keys.length, "each session key must produce a distinct summary dir");
|
|
203
|
+
for (const dir of sessionDirs) {
|
|
204
|
+
assert.notEqual(dir, "other", `summary dir "${dir}" must not be the legacy "other" bucket`);
|
|
205
|
+
assert.notEqual(dir, "default", `summary dir "${dir}" must not be the legacy "default" bucket`);
|
|
206
|
+
}
|
|
207
|
+
} finally {
|
|
208
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("#1492 Symptom B: legacy agent: keys keep their readable identity (no regression from the fix)", () => {
|
|
213
|
+
// The fix must not break the legacy 5-part agent:<id>:discord:channel:<chan> shape.
|
|
214
|
+
const id = parseSessionIdentity("agent:generalist:discord:channel:998877");
|
|
215
|
+
assert.equal(id.legacy, true);
|
|
216
|
+
assert.equal(id.channelType, "discord");
|
|
217
|
+
assert.equal(id.channelId, "998877");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ─── Symptom C: fallback recall reads EVERY category dir ───────────────────
|
|
221
|
+
|
|
222
|
+
function memoryFile(id: string, category: string, content: string): string {
|
|
223
|
+
const now = new Date().toISOString();
|
|
224
|
+
return [
|
|
225
|
+
"---",
|
|
226
|
+
`id: ${id}`,
|
|
227
|
+
`category: ${category}`,
|
|
228
|
+
`created: ${now}`,
|
|
229
|
+
`updated: ${now}`,
|
|
230
|
+
"confidence: 0.9",
|
|
231
|
+
"importance: 5",
|
|
232
|
+
"tags: []",
|
|
233
|
+
"---",
|
|
234
|
+
"",
|
|
235
|
+
content,
|
|
236
|
+
"",
|
|
237
|
+
].join("\n");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function makeStorage(prefix = "engram-1492-"): Promise<{
|
|
241
|
+
storage: StorageManager;
|
|
242
|
+
baseDir: string;
|
|
243
|
+
cleanup: () => Promise<void>;
|
|
244
|
+
}> {
|
|
245
|
+
const baseDir = await mkdtemp(path.join(os.tmpdir(), prefix));
|
|
246
|
+
const storage = new StorageManager(baseDir);
|
|
247
|
+
await storage.ensureDirectories();
|
|
248
|
+
StorageManager.clearAllStaticCaches();
|
|
249
|
+
storage.invalidateAllMemoriesCacheForDir();
|
|
250
|
+
return {
|
|
251
|
+
storage,
|
|
252
|
+
baseDir,
|
|
253
|
+
cleanup: async () => {
|
|
254
|
+
StorageManager.clearAllStaticCaches();
|
|
255
|
+
await rm(baseDir, { recursive: true, force: true });
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
test("#1492 Symptom C: a memory in a non-legacy-4 category (preferences/) is found by fallback disk recall", async () => {
|
|
261
|
+
// The reporter found collectActiveMemoryPaths scanned only
|
|
262
|
+
// facts/procedures/reasoning-traces/corrections. A memory written to
|
|
263
|
+
// preferences/ was invisible to the QMD-unavailable fallback, so recall
|
|
264
|
+
// returned empty even though the file was on disk.
|
|
265
|
+
const { storage, baseDir, cleanup } = await makeStorage();
|
|
266
|
+
try {
|
|
267
|
+
const preferencesDir = path.join(baseDir, "preferences");
|
|
268
|
+
await mkdir(preferencesDir, { recursive: true });
|
|
269
|
+
await writeFile(
|
|
270
|
+
path.join(preferencesDir, "pref-1.md"),
|
|
271
|
+
memoryFile("pref-1", "preference", "Prefer tabs over spaces in this repo."),
|
|
272
|
+
"utf-8",
|
|
273
|
+
);
|
|
274
|
+
storage.invalidateAllMemoriesCacheForDir();
|
|
275
|
+
|
|
276
|
+
const memories = await storage.readAllMemories();
|
|
277
|
+
const ids = memories.map((m) => m.frontmatter?.id ?? "");
|
|
278
|
+
assert.ok(
|
|
279
|
+
ids.includes("pref-1"),
|
|
280
|
+
"fallback disk recall must read preferences/ — the 4-dir scan bug is fixed",
|
|
281
|
+
);
|
|
282
|
+
} finally {
|
|
283
|
+
await cleanup();
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("#1492 Symptom C: RECALL_FALLBACK_DIRS covers the categories the issue named as missed", () => {
|
|
288
|
+
// The issue enumerated questions/, entities/, decisions/, preferences/,
|
|
289
|
+
// commitments/, principles/, skills/, relationships/, moments/, rules/,
|
|
290
|
+
// artifacts/ as directories the system writes to but the old 4-dir scan
|
|
291
|
+
// missed. Assert the recall-memory categories among them are now covered.
|
|
292
|
+
// (questions/ is intentionally a non-memory queue dir; artifacts/ and
|
|
293
|
+
// entities/ are non-category content dirs — see category-dir.ts. The recall
|
|
294
|
+
// *memory* categories must all be present.)
|
|
295
|
+
const requiredRecallCategories = [
|
|
296
|
+
"decisions",
|
|
297
|
+
"preferences",
|
|
298
|
+
"commitments",
|
|
299
|
+
"principles",
|
|
300
|
+
"skills",
|
|
301
|
+
"relationships",
|
|
302
|
+
"moments",
|
|
303
|
+
"rules",
|
|
304
|
+
];
|
|
305
|
+
for (const dir of requiredRecallCategories) {
|
|
306
|
+
assert.ok(
|
|
307
|
+
RECALL_FALLBACK_DIRS.includes(dir),
|
|
308
|
+
`RECALL_FALLBACK_DIRS must include ${dir}/ (missed by the pre-fix 4-dir scan)`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
// The legacy four are still present.
|
|
312
|
+
for (const dir of ["facts", "procedures", "reasoning-traces", "corrections"]) {
|
|
313
|
+
assert.ok(RECALL_FALLBACK_DIRS.includes(dir), `legacy recall dir ${dir}/ must remain covered`);
|
|
314
|
+
}
|
|
315
|
+
});
|