@remnic/core 9.3.649 → 9.3.651
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-cli.js +36 -35
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +16 -16
- package/dist/access-mcp.d.ts +2 -2
- package/dist/access-mcp.js +15 -15
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-DFXIlGvZ.d.ts → access-service-DIZRHQ7Q.d.ts} +255 -2
- package/dist/access-service.d.ts +2 -2
- package/dist/access-service.js +13 -13
- package/dist/{auto-sync-54QQHOG5.js → auto-sync-5CJBJMPZ.js} +5 -5
- package/dist/bootstrap.d.ts +1 -1
- package/dist/briefing.js +3 -3
- package/dist/calibration.js +2 -2
- package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
- package/dist/causal-consolidation.js +6 -6
- package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
- package/dist/{chunk-OAZ5MFUB.js → chunk-3XGWCZ63.js} +45 -28
- package/dist/chunk-3XGWCZ63.js.map +1 -0
- package/dist/{chunk-QKE4LHNR.js → chunk-4HYSMH7D.js} +2 -2
- package/dist/{chunk-NMIOW7XG.js → chunk-4PTKFBST.js} +2 -2
- package/dist/{chunk-DDRNDPX4.js → chunk-4SKKVWLQ.js} +2 -2
- package/dist/chunk-5FOCXX5E.js +34 -0
- package/dist/chunk-5FOCXX5E.js.map +1 -0
- package/dist/{chunk-XUGVP7ZU.js → chunk-5WSDHTBO.js} +166 -47
- package/dist/chunk-5WSDHTBO.js.map +1 -0
- package/dist/{chunk-WPCCNSWO.js → chunk-6UKL6IXM.js} +4 -4
- package/dist/{chunk-DB5A3NHS.js → chunk-7LWRCOP7.js} +9 -2
- package/dist/chunk-7LWRCOP7.js.map +1 -0
- package/dist/{chunk-APJQ6UEA.js → chunk-AGNBY3VG.js} +4 -4
- package/dist/{chunk-4BISW7RX.js → chunk-AJE7FJVE.js} +2 -2
- package/dist/{chunk-ZXWAQFDE.js → chunk-CFOCZPIQ.js} +2 -2
- package/dist/{chunk-NT5TINK5.js → chunk-DHGSZ3UD.js} +2 -2
- package/dist/{chunk-OTC2KOZ2.js → chunk-EHQLDFSH.js} +2 -2
- package/dist/{chunk-AMACWKM4.js → chunk-IJHLC5CH.js} +2 -2
- package/dist/{chunk-OR7R6M5Z.js → chunk-IVYSVAC6.js} +2 -2
- package/dist/{chunk-UMKPSD35.js → chunk-JF7SFXTG.js} +2 -2
- package/dist/{chunk-MCYT2RNT.js → chunk-KJDKZVF3.js} +3 -3
- package/dist/{chunk-BUKK5SWA.js → chunk-KQAFEZQX.js} +2 -2
- package/dist/{chunk-PQFUUXWK.js → chunk-KWM33SPU.js} +2 -2
- package/dist/{chunk-A3BS64GV.js → chunk-LCC5EZTT.js} +4 -4
- package/dist/{chunk-ZT6R3WR3.js → chunk-LFTLXOFX.js} +4 -4
- package/dist/{chunk-CNRZ6WJU.js → chunk-MF32AL7N.js} +5 -5
- package/dist/{chunk-6GIKAUTN.js → chunk-MMJANTJX.js} +33 -2
- package/dist/{chunk-6GIKAUTN.js.map → chunk-MMJANTJX.js.map} +1 -1
- package/dist/{chunk-D6WVJIS3.js → chunk-ORGWWNJG.js} +2 -2
- package/dist/{chunk-Z3PZRDLW.js → chunk-PRQXUSQV.js} +2 -2
- package/dist/{chunk-VWT3F4IV.js → chunk-PS3SYNHP.js} +12 -4
- package/dist/chunk-PS3SYNHP.js.map +1 -0
- package/dist/{chunk-IMWFHBG2.js → chunk-QWRC7GIO.js} +2 -2
- package/dist/{chunk-FQYFMIKG.js → chunk-RKN5J4RO.js} +26 -26
- package/dist/{chunk-FUXV6HSO.js → chunk-RSS2KWN6.js} +5 -5
- package/dist/{chunk-U3GQ33JC.js → chunk-SLTKP5WJ.js} +2 -2
- package/dist/{chunk-5ETA6OAS.js → chunk-SLYD3AH4.js} +617 -89
- package/dist/chunk-SLYD3AH4.js.map +1 -0
- package/dist/{chunk-6NKAQ74D.js → chunk-UU6MVCJ6.js} +1 -1
- package/dist/chunk-UU6MVCJ6.js.map +1 -0
- package/dist/{chunk-WEPMT6SC.js → chunk-V25ZAOSB.js} +5 -5
- package/dist/{chunk-UMTG2BN2.js → chunk-V4UDXYGG.js} +2 -2
- package/dist/{chunk-RRRCNIPK.js → chunk-WJK75OCH.js} +4 -4
- package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
- package/dist/{chunk-OZKZ2TRP.js → chunk-XBIACVCO.js} +9 -2
- package/dist/chunk-XBIACVCO.js.map +1 -0
- package/dist/{chunk-ALUZN7BE.js → chunk-XMN6MMTU.js} +2 -2
- package/dist/{chunk-A4BTPHIN.js → chunk-Y7NWBBHV.js} +6 -6
- package/dist/{chunk-M75TBFKQ.js → chunk-Z2OXSMZK.js} +2 -2
- package/dist/{cli-DrL2Nv4j.d.ts → cli-BG4ybtJr.d.ts} +2 -2
- package/dist/cli.d.ts +3 -3
- package/dist/cli.js +31 -31
- package/dist/compounding/engine.js +3 -3
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/index.js +3 -3
- package/dist/entity-retrieval.js +3 -3
- package/dist/event-order-recall.js +1 -1
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/explicit-cue-recall.d.ts +7 -0
- package/dist/explicit-cue-recall.js +2 -1
- package/dist/extraction-judge.js +3 -3
- package/dist/extraction.js +3 -3
- package/dist/fallback-llm.js +2 -2
- package/dist/focused-list-recall.d.ts +6 -0
- package/dist/focused-list-recall.js +2 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +84 -83
- package/dist/index.js.map +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +5 -5
- package/dist/lcm-fallback-read.d.ts +71 -0
- package/dist/lcm-fallback-read.js +10 -0
- package/dist/lcm-fallback-read.js.map +1 -0
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/namespaces/migrate.js +7 -7
- package/dist/namespaces/search.js +3 -3
- package/dist/namespaces/storage.js +3 -3
- package/dist/operator-toolkit.js +9 -9
- package/dist/{orchestrator-DEQW9j0Z.d.ts → orchestrator-CX-oqwJq.d.ts} +58 -0
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +30 -29
- package/dist/recall-planner-llm.js +2 -2
- package/dist/response-guidance-recall.d.ts +6 -0
- package/dist/response-guidance-recall.js +2 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/search/factory.js +2 -2
- package/dist/search/index.js +4 -4
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/storage.js +2 -2
- package/dist/summarizer.js +3 -3
- package/dist/targeted-fact-recall.d.ts +6 -0
- package/dist/targeted-fact-recall.js +2 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/import-sqlite.js +2 -2
- package/dist/transfer/types.d.ts +12 -12
- package/dist/verified-recall.js +3 -3
- package/package.json +1 -1
- package/src/access-service-lcm-forgery.test.ts +410 -0
- package/src/access-service-observe-lcm-parity.test.ts +1397 -0
- package/src/access-service-observe-scope.test.ts +599 -0
- package/src/access-service-raw-excerpt-read-gate.test.ts +443 -0
- package/src/access-service.ts +1270 -113
- package/src/coding/coding-namespace.test.ts +44 -0
- package/src/coding/coding-namespace.ts +163 -0
- package/src/event-order-recall.ts +8 -0
- package/src/explicit-cue-recall.ts +70 -29
- package/src/focused-list-recall.ts +23 -1
- package/src/lcm-fallback-read.ts +113 -0
- package/src/orchestrator.ts +331 -26
- package/src/response-guidance-recall.ts +21 -1
- package/src/targeted-fact-recall.ts +24 -3
- package/dist/chunk-5ETA6OAS.js.map +0 -1
- package/dist/chunk-6NKAQ74D.js.map +0 -1
- package/dist/chunk-DB5A3NHS.js.map +0 -1
- package/dist/chunk-OAZ5MFUB.js.map +0 -1
- package/dist/chunk-OZKZ2TRP.js.map +0 -1
- package/dist/chunk-VWT3F4IV.js.map +0 -1
- package/dist/chunk-XUGVP7ZU.js.map +0 -1
- /package/dist/{auto-sync-54QQHOG5.js.map → auto-sync-5CJBJMPZ.js.map} +0 -0
- /package/dist/{capsule-crypto-GWVG7LGC.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
- /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
- /package/dist/{chunk-QKE4LHNR.js.map → chunk-4HYSMH7D.js.map} +0 -0
- /package/dist/{chunk-NMIOW7XG.js.map → chunk-4PTKFBST.js.map} +0 -0
- /package/dist/{chunk-DDRNDPX4.js.map → chunk-4SKKVWLQ.js.map} +0 -0
- /package/dist/{chunk-WPCCNSWO.js.map → chunk-6UKL6IXM.js.map} +0 -0
- /package/dist/{chunk-APJQ6UEA.js.map → chunk-AGNBY3VG.js.map} +0 -0
- /package/dist/{chunk-4BISW7RX.js.map → chunk-AJE7FJVE.js.map} +0 -0
- /package/dist/{chunk-ZXWAQFDE.js.map → chunk-CFOCZPIQ.js.map} +0 -0
- /package/dist/{chunk-NT5TINK5.js.map → chunk-DHGSZ3UD.js.map} +0 -0
- /package/dist/{chunk-OTC2KOZ2.js.map → chunk-EHQLDFSH.js.map} +0 -0
- /package/dist/{chunk-AMACWKM4.js.map → chunk-IJHLC5CH.js.map} +0 -0
- /package/dist/{chunk-OR7R6M5Z.js.map → chunk-IVYSVAC6.js.map} +0 -0
- /package/dist/{chunk-UMKPSD35.js.map → chunk-JF7SFXTG.js.map} +0 -0
- /package/dist/{chunk-MCYT2RNT.js.map → chunk-KJDKZVF3.js.map} +0 -0
- /package/dist/{chunk-BUKK5SWA.js.map → chunk-KQAFEZQX.js.map} +0 -0
- /package/dist/{chunk-PQFUUXWK.js.map → chunk-KWM33SPU.js.map} +0 -0
- /package/dist/{chunk-A3BS64GV.js.map → chunk-LCC5EZTT.js.map} +0 -0
- /package/dist/{chunk-ZT6R3WR3.js.map → chunk-LFTLXOFX.js.map} +0 -0
- /package/dist/{chunk-CNRZ6WJU.js.map → chunk-MF32AL7N.js.map} +0 -0
- /package/dist/{chunk-D6WVJIS3.js.map → chunk-ORGWWNJG.js.map} +0 -0
- /package/dist/{chunk-Z3PZRDLW.js.map → chunk-PRQXUSQV.js.map} +0 -0
- /package/dist/{chunk-IMWFHBG2.js.map → chunk-QWRC7GIO.js.map} +0 -0
- /package/dist/{chunk-FQYFMIKG.js.map → chunk-RKN5J4RO.js.map} +0 -0
- /package/dist/{chunk-FUXV6HSO.js.map → chunk-RSS2KWN6.js.map} +0 -0
- /package/dist/{chunk-U3GQ33JC.js.map → chunk-SLTKP5WJ.js.map} +0 -0
- /package/dist/{chunk-WEPMT6SC.js.map → chunk-V25ZAOSB.js.map} +0 -0
- /package/dist/{chunk-UMTG2BN2.js.map → chunk-V4UDXYGG.js.map} +0 -0
- /package/dist/{chunk-RRRCNIPK.js.map → chunk-WJK75OCH.js.map} +0 -0
- /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
- /package/dist/{chunk-ALUZN7BE.js.map → chunk-XMN6MMTU.js.map} +0 -0
- /package/dist/{chunk-A4BTPHIN.js.map → chunk-Y7NWBBHV.js.map} +0 -0
- /package/dist/{chunk-M75TBFKQ.js.map → chunk-Z2OXSMZK.js.map} +0 -0
|
@@ -365,3 +365,47 @@ test("resolveCodingNamespaceOverlay: read path and write path see identical name
|
|
|
365
365
|
const writeOverlay = resolveCodingNamespaceOverlay(input[0], input[1]);
|
|
366
366
|
assert.deepEqual(readOverlay, writeOverlay);
|
|
367
367
|
});
|
|
368
|
+
|
|
369
|
+
test("#1505 codex P2: lcmReadSessionIdsForNamespaces preserves UNDEFINED for a sessionless read (never the literal 'default' session id)", async () => {
|
|
370
|
+
const { lcmReadSessionIdsForNamespaces } = await import("./coding-namespace.js");
|
|
371
|
+
// Sessionless ⇒ a single archive-wide read (`undefined`), so the LCM builders
|
|
372
|
+
// run with no exact session_id filter — NOT filtered to a session literally
|
|
373
|
+
// named "default" (which would silently drop explicit-cue/targeted/focused/
|
|
374
|
+
// response/event LCM sections for every recall that omits a session key).
|
|
375
|
+
assert.deepEqual(
|
|
376
|
+
lcmReadSessionIdsForNamespaces(["default"], undefined, "default"),
|
|
377
|
+
[undefined],
|
|
378
|
+
"sessionless ⇒ [undefined], never ['default']",
|
|
379
|
+
);
|
|
380
|
+
assert.deepEqual(
|
|
381
|
+
lcmReadSessionIdsForNamespaces(["acme", "default"], "", "default"),
|
|
382
|
+
[undefined],
|
|
383
|
+
"empty-string sessionKey is treated as sessionless ⇒ [undefined]",
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("#1505 codex P2: lcmReadSessionIdsForNamespaces keeps the raw sessionKey + overlay keys when a session IS present", async () => {
|
|
388
|
+
const { lcmReadSessionIdsForNamespaces, lcmSessionKeyForNamespace } = await import(
|
|
389
|
+
"./coding-namespace.js"
|
|
390
|
+
);
|
|
391
|
+
// Shape-agnostic expectation: derive each expected key through the SAME shared
|
|
392
|
+
// encoder production uses (#1495 P1 made the namespaced encoding sentinel-framed
|
|
393
|
+
// and unforgeable; rule 22: never re-hardcode the join).
|
|
394
|
+
const enc = (ns: string, sk: string) =>
|
|
395
|
+
lcmSessionKeyForNamespace(ns, sk, "default") ?? sk;
|
|
396
|
+
// Single-store / default namespace ⇒ raw sessionKey (byte-for-byte prior).
|
|
397
|
+
assert.deepEqual(
|
|
398
|
+
lcmReadSessionIdsForNamespaces(["default"], "sk", "default"),
|
|
399
|
+
["sk"],
|
|
400
|
+
);
|
|
401
|
+
// Non-default namespaces ⇒ sentinel-framed keys, deduped, ordered. The default
|
|
402
|
+
// key collapses to the raw `sk`, and the namespaced key is disjoint from it.
|
|
403
|
+
assert.deepEqual(
|
|
404
|
+
lcmReadSessionIdsForNamespaces(["acme", "default", "acme"], "sk", "default"),
|
|
405
|
+
[enc("acme", "sk"), "sk"],
|
|
406
|
+
);
|
|
407
|
+
// Security invariant: the namespaced key cannot equal a raw default key, so a
|
|
408
|
+
// forged default read can never collide with the overlay key.
|
|
409
|
+
assert.notEqual(enc("acme", "sk"), "acme:sk");
|
|
410
|
+
assert.notEqual(enc("acme", "sk"), "sk");
|
|
411
|
+
});
|
|
@@ -418,3 +418,166 @@ export function describeCodingScope(
|
|
|
418
418
|
disabledReason: null,
|
|
419
419
|
};
|
|
420
420
|
}
|
|
421
|
+
|
|
422
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
423
|
+
// LCM session-key namespacing (#1495)
|
|
424
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Reserved structural sentinel for the namespaced LCM `session_id` encoding
|
|
428
|
+
* (#1495 P1). U+001F (UNIT SEPARATOR) is a C0 control character that CANNOT
|
|
429
|
+
* occur in a route namespace (`isSafeRouteNamespace` ⇒ `[A-Za-z0-9._-]{1,64}`,
|
|
430
|
+
* see routing/engine.ts) and does not occur in any legitimate session key, so
|
|
431
|
+
* it is an unforgeable structural marker for the namespace boundary.
|
|
432
|
+
*
|
|
433
|
+
* SECURITY — why this is unforgeable (the #1495 P1 fix):
|
|
434
|
+
* The LCM archive is keyed by the `session_id` STRING (exact `session_id = ?`
|
|
435
|
+
* and prefix `session_id LIKE '<prefix>%'`), NOT physically partitioned by
|
|
436
|
+
* namespace. The previous encoding `${namespace}:${sessionKey}` shared the SAME
|
|
437
|
+
* string space as a raw default-store key, so a caller authorized for the
|
|
438
|
+
* `default` store could pass a raw `sessionKey` equal to another namespace's
|
|
439
|
+
* encoded key (`"<overlay-ns>:<victim-session>"`) and exact-match the victim's
|
|
440
|
+
* rows — a cross-tenant read leak.
|
|
441
|
+
*
|
|
442
|
+
* The new encoding makes the namespaced and default key-spaces PROVABLY
|
|
443
|
+
* DISJOINT:
|
|
444
|
+
* - Overlay key = `\x1f<namespace>\x1f<sessionKey>` — always begins with
|
|
445
|
+
* `\x1f` followed by a NON-`\x1f` character (the namespace is non-empty and
|
|
446
|
+
* `\x1f`-free). The leading `\x1f<namespace>\x1f` is an unambiguous,
|
|
447
|
+
* injective frame: the namespace cannot contain `\x1f`, so the second `\x1f`
|
|
448
|
+
* terminates it without any escaping of the (raw) session key.
|
|
449
|
+
* - Default key = the raw `sessionKey`, UNLESS it already begins with the
|
|
450
|
+
* sentinel, in which case it is escaped to begin with `\x1f\x1f` (see
|
|
451
|
+
* `escapeDefaultLcmKey`). A default key therefore NEVER matches the overlay
|
|
452
|
+
* frame `\x1f<non-\x1f>…`.
|
|
453
|
+
* Hence no caller-controlled raw `sessionKey` (default path) can reproduce an
|
|
454
|
+
* overlay key, closing the forgery for BOTH the exact-`session_id` match and the
|
|
455
|
+
* `sessionPrefix` LIKE match (an overlay prefix `\x1f<ns>\x1f<rawPrefix>` stays a
|
|
456
|
+
* valid LIKE-prefix of the overlay full keys, and a default prefix can only
|
|
457
|
+
* LIKE-match default keys).
|
|
458
|
+
*
|
|
459
|
+
* Existing default-store rows need NO migration: legitimate session keys never
|
|
460
|
+
* contain `\x1f`, so `escapeDefaultLcmKey` is a no-op for them and they remain
|
|
461
|
+
* byte-for-byte their raw form. The namespaced encoding is NEW in this
|
|
462
|
+
* unreleased PR, so changing its shape costs nothing.
|
|
463
|
+
*/
|
|
464
|
+
const LCM_NS_SENTINEL = "\u001f";
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Make a default-store (raw) LCM key disjoint from the namespaced key-space.
|
|
468
|
+
*
|
|
469
|
+
* Namespaced overlay keys always begin with `\x1f` followed by a non-`\x1f`
|
|
470
|
+
* namespace character. A raw default key collides with that frame ONLY if it
|
|
471
|
+
* begins with `\x1f`. Legitimate session keys never contain `\x1f`, so this is a
|
|
472
|
+
* pure no-op for them; a forged key that begins with `\x1f` is escaped to begin
|
|
473
|
+
* with `\x1f\x1f`, which can never equal an overlay key (whose second character
|
|
474
|
+
* is a `[A-Za-z0-9._-]` namespace char, never `\x1f`).
|
|
475
|
+
*/
|
|
476
|
+
function escapeDefaultLcmKey(sessionKey: string): string {
|
|
477
|
+
return sessionKey.startsWith(LCM_NS_SENTINEL)
|
|
478
|
+
? `${LCM_NS_SENTINEL}${sessionKey}`
|
|
479
|
+
: sessionKey;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Build the LCM/structured-history `session_id` that a write-producing surface
|
|
484
|
+
* archives under, and that a same-session reader must search under, so reads
|
|
485
|
+
* and writes never drift (#1495, CLAUDE.md rule 42).
|
|
486
|
+
*
|
|
487
|
+
* The LCM archive filters strictly by the `session_id` string, so the writer's
|
|
488
|
+
* archival key and the reader's lookup key MUST agree byte-for-byte. The
|
|
489
|
+
* encoding frames the namespace with the reserved {@link LCM_NS_SENTINEL}
|
|
490
|
+
* (`\x1f<namespace>\x1f<sessionKey>`) whenever that namespace diverges from the
|
|
491
|
+
* single-store default; otherwise it passes the (escaped) raw `sessionKey` so
|
|
492
|
+
* single-user / no-overlay deployments keep pre-#1495 behavior exactly. The two
|
|
493
|
+
* key-spaces are provably disjoint, so a caller-controlled raw `sessionKey`
|
|
494
|
+
* cannot forge another namespace's encoded id (see the {@link LCM_NS_SENTINEL}
|
|
495
|
+
* doc comment for the full security rationale).
|
|
496
|
+
*
|
|
497
|
+
* `observe`, compaction flush/record, and the orchestrator recall readers all
|
|
498
|
+
* route through this one helper so a project-scoped (cwd/projectTag) or
|
|
499
|
+
* explicit-namespace session reads its own compressed-history / structured /
|
|
500
|
+
* targeted-fact evidence instead of missing it.
|
|
501
|
+
*/
|
|
502
|
+
export function lcmSessionKeyForNamespace(
|
|
503
|
+
namespace: string | undefined,
|
|
504
|
+
sessionKey: string | undefined,
|
|
505
|
+
defaultNamespace: string,
|
|
506
|
+
): string | undefined {
|
|
507
|
+
if (typeof sessionKey !== "string" || sessionKey.length === 0) return sessionKey;
|
|
508
|
+
if (
|
|
509
|
+
typeof namespace === "string" &&
|
|
510
|
+
namespace.length > 0 &&
|
|
511
|
+
namespace !== defaultNamespace
|
|
512
|
+
) {
|
|
513
|
+
// Namespaced (overlay / explicit) key: frame the namespace with the reserved
|
|
514
|
+
// sentinel so the boundary is unambiguous AND unforgeable from the default
|
|
515
|
+
// key-space. The namespace is guaranteed `\x1f`-free by `isSafeRouteNamespace`.
|
|
516
|
+
return `${LCM_NS_SENTINEL}${namespace}${LCM_NS_SENTINEL}${sessionKey}`;
|
|
517
|
+
}
|
|
518
|
+
// Default store: raw sessionKey, escaped only if it would otherwise intrude on
|
|
519
|
+
// the namespaced key-space (no-op for every legitimate key).
|
|
520
|
+
return escapeDefaultLcmKey(sessionKey);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Map an ORDERED, read-authorized namespace set (the SAME set normal QMD/file
|
|
525
|
+
* recall searches) to the ordered set of LCM `session_id`s a same-session reader
|
|
526
|
+
* must query (#1505 thread "Include coding fallback namespaces in LCM reads").
|
|
527
|
+
*
|
|
528
|
+
* The LCM archive filters strictly by `session_id`, and `observe` archives each
|
|
529
|
+
* turn under `${effectiveNamespace}:${sessionKey}` for the namespace that was
|
|
530
|
+
* effective when it was written. A branch-scoped session that overlays
|
|
531
|
+
* `${base-project-*-branch-*}` only sees rows written under THAT namespace if it
|
|
532
|
+
* reads a single overlay key — but normal recall ALSO searches the
|
|
533
|
+
* `codingOverlay.readFallbacks` (project / root) namespaces, so rows archived at
|
|
534
|
+
* project/root scope are surfaced by QMD/file recall yet MISSED by a single-key
|
|
535
|
+
* LCM read. Deriving the LCM read keys from the SAME `recallNamespaces` set keeps
|
|
536
|
+
* the LCM read path from diverging: every namespace recall is authorized to read
|
|
537
|
+
* (read-auth gate already applied upstream in `recallNamespaces`) contributes one
|
|
538
|
+
* LCM key, ordered primary-overlay-first then fallbacks. Unreadable namespaces
|
|
539
|
+
* are never in `recallNamespaces`, so they are never searched here either (no
|
|
540
|
+
* cross-tenant read leak).
|
|
541
|
+
*
|
|
542
|
+
* Single-user / no-overlay recall passes a single-namespace set that collapses to
|
|
543
|
+
* the raw `sessionKey`, so the result is `[sessionKey]` — byte-for-byte the
|
|
544
|
+
* pre-#1505 single-key behavior.
|
|
545
|
+
*
|
|
546
|
+
* SESSIONLESS recall (`sessionKey === undefined`): returns `[undefined]` so the
|
|
547
|
+
* caller issues ONE archive-wide LCM read with no exact `session_id` filter —
|
|
548
|
+
* byte-for-byte the pre-#1505 sessionless behavior. It must NOT substitute the
|
|
549
|
+
* literal `"default"` session id (codex P2 "Preserve unscoped LCM searches
|
|
550
|
+
* without a session key"): that would filter to a session literally named
|
|
551
|
+
* `default`, silently dropping the explicit-cue / targeted / focused / response /
|
|
552
|
+
* event LCM sections for every recall that omits a session key.
|
|
553
|
+
*
|
|
554
|
+
* The result is deduped while preserving first-seen order so the caller can query
|
|
555
|
+
* keys in priority order and short-circuit on the first hit without re-querying an
|
|
556
|
+
* identical key (e.g. when two namespaces both collapse to the default store).
|
|
557
|
+
*/
|
|
558
|
+
export function lcmReadSessionIdsForNamespaces(
|
|
559
|
+
namespaces: readonly string[],
|
|
560
|
+
sessionKey: string | undefined,
|
|
561
|
+
defaultNamespace: string,
|
|
562
|
+
): Array<string | undefined> {
|
|
563
|
+
// Sessionless ⇒ a single archive-wide read (no `session_id` filter). NEVER the
|
|
564
|
+
// literal "default" session id (codex P2).
|
|
565
|
+
if (typeof sessionKey !== "string" || sessionKey.length === 0) {
|
|
566
|
+
return [undefined];
|
|
567
|
+
}
|
|
568
|
+
const out: string[] = [];
|
|
569
|
+
const seen = new Set<string>();
|
|
570
|
+
for (const namespace of namespaces) {
|
|
571
|
+
const key =
|
|
572
|
+
lcmSessionKeyForNamespace(namespace, sessionKey, defaultNamespace) ??
|
|
573
|
+
sessionKey;
|
|
574
|
+
if (!seen.has(key)) {
|
|
575
|
+
seen.add(key);
|
|
576
|
+
out.push(key);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (out.length === 0) {
|
|
580
|
+
out.push(sessionKey);
|
|
581
|
+
}
|
|
582
|
+
return out;
|
|
583
|
+
}
|
|
@@ -3,6 +3,11 @@ import type { ExplicitCueRecallEngine } from "./explicit-cue-recall.js";
|
|
|
3
3
|
|
|
4
4
|
export interface EventOrderRecallOptions {
|
|
5
5
|
engine: ExplicitCueRecallEngine | null | undefined;
|
|
6
|
+
// event-order reads a SINGLE LCM session key. Unlike the relevance-ranked
|
|
7
|
+
// sections, its evidence must not be merged across the #1505 fallback key set:
|
|
8
|
+
// `turn_index` is local to each LCM `session_id`, so interleaving keys would
|
|
9
|
+
// misstate chronology. The orchestrator reads the ordered key set via
|
|
10
|
+
// first-non-empty instead (see the event-order call site).
|
|
6
11
|
sessionId?: string;
|
|
7
12
|
query: string;
|
|
8
13
|
maxChars: number;
|
|
@@ -42,6 +47,9 @@ export async function buildEventOrderRecallSection(
|
|
|
42
47
|
): Promise<string> {
|
|
43
48
|
const budget = normalizePositiveInteger(options.maxChars);
|
|
44
49
|
const maxItems = normalizePositiveInteger(options.maxItems ?? DEFAULT_MAX_ITEMS);
|
|
50
|
+
// event-order reads a SINGLE session key (`turn_index` is local to each LCM
|
|
51
|
+
// `session_id`, so chronology can't be merged across the #1505 fallback set —
|
|
52
|
+
// the orchestrator drives the ordered key set via first-non-empty instead).
|
|
45
53
|
if (!options.engine || !options.sessionId || budget <= 0) {
|
|
46
54
|
return "";
|
|
47
55
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { buildEvidencePack } from "./evidence-pack.js";
|
|
2
|
+
import {
|
|
3
|
+
gatherAcrossReadSessions,
|
|
4
|
+
resolveLcmReadSessionIds,
|
|
5
|
+
} from "./lcm-fallback-read.js";
|
|
2
6
|
|
|
3
7
|
export interface ExplicitCueRecallEngine {
|
|
4
8
|
expandContext(
|
|
@@ -29,6 +33,13 @@ export interface ExplicitCueRecallEngine {
|
|
|
29
33
|
export interface ExplicitCueRecallOptions {
|
|
30
34
|
engine: ExplicitCueRecallEngine | null | undefined;
|
|
31
35
|
sessionId?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Ordered, read-authorized LCM read key set (primary overlay → project/root
|
|
38
|
+
* fallbacks). When present, cue evidence is gathered across EVERY key into one
|
|
39
|
+
* shared accumulator and merged under this section's budget (#1505 codex P2).
|
|
40
|
+
* Falls back to `sessionId`.
|
|
41
|
+
*/
|
|
42
|
+
sessionIds?: readonly (string | undefined)[];
|
|
32
43
|
query: string;
|
|
33
44
|
maxChars: number;
|
|
34
45
|
maxItemChars?: number;
|
|
@@ -324,45 +335,75 @@ export async function buildExplicitCueRecallSection(
|
|
|
324
335
|
}> = [];
|
|
325
336
|
const seenTurns = new Set<string>();
|
|
326
337
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
+
// #1505 codex P2: gather cue evidence across the ordered LCM read key set
|
|
339
|
+
// (primary overlay → project/root fallbacks) into ONE shared accumulator
|
|
340
|
+
// (`evidenceItems` / `seenTurns`), so a branch-scoped session's project/root
|
|
341
|
+
// fallback cues are RECOVERED instead of being skipped by the old
|
|
342
|
+
// first-non-empty short-circuit. `seenTurns` dedupes across keys by
|
|
343
|
+
// `session_id`+`turn_index`; the budget is applied exactly once in the
|
|
344
|
+
// `buildEvidencePack` call below. `gatherAcrossReadSessions` isolates a
|
|
345
|
+
// per-key read failure (a corrupt/locked fallback index must not discard the
|
|
346
|
+
// other keys' cues); single-key recall runs each collector directly, so a
|
|
347
|
+
// failure propagates exactly as before.
|
|
348
|
+
//
|
|
349
|
+
// Ordering: gather by evidence TYPE first (turn references → content cues →
|
|
350
|
+
// lexical cues), then by read-key priority within each type. This is the
|
|
351
|
+
// section's deliberate value order, and it carries across keys — so a fallback
|
|
352
|
+
// key's high-value turn references precede the primary key's lower-value
|
|
353
|
+
// lexical cues under a tight budget, while a single key is byte-for-byte the
|
|
354
|
+
// pre-#1505 insertion order. We intentionally do NOT score-sort the merged set:
|
|
355
|
+
// explicit-cue's highest-value cues (turn references / content cues) are
|
|
356
|
+
// deliberately UNSCORED while lexical search hits carry numeric scores, so a
|
|
357
|
+
// score-DESC sort would invert the priority and demote turn references below
|
|
358
|
+
// weak lexical hits (cursor[bot] / codex P2 on this PR).
|
|
359
|
+
const readSessionIds = resolveLcmReadSessionIds(options);
|
|
360
|
+
await gatherAcrossReadSessions(readSessionIds, (sessionId) =>
|
|
361
|
+
collectTurnReferenceEvidence({
|
|
338
362
|
engine,
|
|
339
|
-
sessionId
|
|
363
|
+
sessionId,
|
|
340
364
|
query,
|
|
341
365
|
maxReferences,
|
|
342
366
|
evidenceItems,
|
|
343
367
|
seenTurns,
|
|
344
|
-
})
|
|
368
|
+
}),
|
|
369
|
+
);
|
|
345
370
|
|
|
346
|
-
|
|
371
|
+
if (options.includeContentLexicalCues) {
|
|
372
|
+
await gatherAcrossReadSessions(readSessionIds, (sessionId) =>
|
|
373
|
+
collectNamedMeetingFactEvidence({
|
|
374
|
+
engine,
|
|
375
|
+
sessionId,
|
|
376
|
+
query,
|
|
377
|
+
maxReferences,
|
|
378
|
+
evidenceItems,
|
|
379
|
+
seenTurns,
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
await gatherAcrossReadSessions(readSessionIds, (sessionId) =>
|
|
384
|
+
collectFocusedTranscriptCueEvidence({
|
|
385
|
+
engine,
|
|
386
|
+
sessionId,
|
|
387
|
+
query,
|
|
388
|
+
evidenceItems,
|
|
389
|
+
seenTurns,
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
await gatherAcrossReadSessions(readSessionIds, (sessionId) =>
|
|
395
|
+
collectLexicalCueEvidence({
|
|
347
396
|
engine,
|
|
348
|
-
sessionId
|
|
397
|
+
sessionId,
|
|
349
398
|
query,
|
|
399
|
+
maxReferences,
|
|
400
|
+
includeBenchmarkAnchorCues: options.includeBenchmarkAnchorCues,
|
|
401
|
+
includeContentLexicalCues: options.includeContentLexicalCues,
|
|
402
|
+
includeStructuredPlanCues: options.includeStructuredPlanCues,
|
|
350
403
|
evidenceItems,
|
|
351
404
|
seenTurns,
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
await collectLexicalCueEvidence({
|
|
356
|
-
engine,
|
|
357
|
-
sessionId: options.sessionId,
|
|
358
|
-
query,
|
|
359
|
-
maxReferences,
|
|
360
|
-
includeBenchmarkAnchorCues: options.includeBenchmarkAnchorCues,
|
|
361
|
-
includeContentLexicalCues: options.includeContentLexicalCues,
|
|
362
|
-
includeStructuredPlanCues: options.includeStructuredPlanCues,
|
|
363
|
-
evidenceItems,
|
|
364
|
-
seenTurns,
|
|
365
|
-
});
|
|
405
|
+
}),
|
|
406
|
+
);
|
|
366
407
|
|
|
367
408
|
const evidenceFocusQuery = buildEvidenceFocusQuery(query, {
|
|
368
409
|
includeBenchmarkAnchorCues: options.includeBenchmarkAnchorCues,
|
|
@@ -4,10 +4,20 @@ import {
|
|
|
4
4
|
type EvidencePackItem,
|
|
5
5
|
} from "./evidence-pack.js";
|
|
6
6
|
import type { ExplicitCueRecallEngine } from "./explicit-cue-recall.js";
|
|
7
|
+
import {
|
|
8
|
+
gatherAcrossReadSessions,
|
|
9
|
+
resolveLcmReadSessionIds,
|
|
10
|
+
} from "./lcm-fallback-read.js";
|
|
7
11
|
|
|
8
12
|
export interface FocusedListRecallOptions {
|
|
9
13
|
engine: ExplicitCueRecallEngine | null | undefined;
|
|
10
14
|
sessionId?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Ordered, read-authorized LCM read key set (primary overlay → project/root
|
|
17
|
+
* fallbacks). When present, evidence is gathered across EVERY key and merged
|
|
18
|
+
* under this section's budget (#1505 codex P2). Falls back to `sessionId`.
|
|
19
|
+
*/
|
|
20
|
+
sessionIds?: readonly (string | undefined)[];
|
|
11
21
|
query: string;
|
|
12
22
|
maxChars: number;
|
|
13
23
|
maxItemChars?: number;
|
|
@@ -52,7 +62,19 @@ export async function buildFocusedListRecallSection(
|
|
|
52
62
|
return "";
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
|
|
65
|
+
// #1505 codex P2: gather candidates across the ordered LCM read key set
|
|
66
|
+
// (primary overlay → project/root fallbacks) and UNION them into the existing
|
|
67
|
+
// rank/dedupe/budget pass, so a stronger project-fallback candidate is not
|
|
68
|
+
// masked by a weak primary-key hit. `rankAndDedupeFocusedListItems` applies
|
|
69
|
+
// the section-appropriate dedupe and relevance rank; the budget is applied
|
|
70
|
+
// exactly once below. `gatherAcrossReadSessions` isolates a per-key read
|
|
71
|
+
// failure so a corrupt/locked fallback index can't discard the primary key's
|
|
72
|
+
// candidates; the single-key path runs exactly one collect and propagates a
|
|
73
|
+
// failure as before — byte-for-byte the pre-#1505 behavior.
|
|
74
|
+
const items: EvidencePackItem[] = [];
|
|
75
|
+
await gatherAcrossReadSessions(resolveLcmReadSessionIds(options), async (sessionId) => {
|
|
76
|
+
items.push(...(await collectFocusedListItems({ ...options, sessionId }, intent)));
|
|
77
|
+
});
|
|
56
78
|
const ranked = rankAndDedupeFocusedListItems(items, options.query, intent)
|
|
57
79
|
.slice(0, maxResults);
|
|
58
80
|
if (ranked.length === 0) {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper for reading LCM-backed recall sections across the ordered,
|
|
3
|
+
* read-authorized fallback key set (#1505 codex P2 "Merge LCM fallback reads
|
|
4
|
+
* instead of short-circuiting").
|
|
5
|
+
*
|
|
6
|
+
* Background: a branch-scoped session archives its LCM rows under whichever
|
|
7
|
+
* coding-overlay namespace was effective at write time, so its evidence can be
|
|
8
|
+
* split across the primary overlay key AND the project / root fallback keys.
|
|
9
|
+
* Normal QMD/file recall already searches the primary namespace PLUS
|
|
10
|
+
* `codingOverlay.readFallbacks` and MERGES the rows. The LCM read path must do
|
|
11
|
+
* the same: query EVERY authorized read key and merge the candidate evidence
|
|
12
|
+
* into each section's existing dedupe + rank + budget pass, instead of stopping
|
|
13
|
+
* at the first key that happens to yield a (possibly weak) hit.
|
|
14
|
+
*
|
|
15
|
+
* Each section already owns a section-appropriate dedupe (a `seen` set or a
|
|
16
|
+
* `rankAndDedupe…` step), so the fan-out only needs to resolve the ordered,
|
|
17
|
+
* deduped read-key set and UNION the per-key candidates into that existing
|
|
18
|
+
* pipeline — the budget is then applied exactly once to the union. Centralizing
|
|
19
|
+
* the key-set resolution here (rather than re-implementing per builder) follows
|
|
20
|
+
* CLAUDE.md rule 22 (scope resolution must be deduplicated).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** A recall section's LCM read target: either a single key or an ordered set. */
|
|
24
|
+
export interface LcmReadSessionTarget {
|
|
25
|
+
/**
|
|
26
|
+
* The single LCM read `session_id` (pre-#1505 behavior). `undefined` means a
|
|
27
|
+
* sessionless, archive-wide read with no `session_id` filter.
|
|
28
|
+
*/
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The ordered, read-authorized LCM read key set (primary overlay key first,
|
|
32
|
+
* then project / root fallbacks) the orchestrator derived from the same
|
|
33
|
+
* readable namespace set normal recall searches. When present and non-empty,
|
|
34
|
+
* it supersedes `sessionId`.
|
|
35
|
+
*/
|
|
36
|
+
sessionIds?: readonly (string | undefined)[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// `undefined` (a sessionless, archive-wide read) is a distinct, legitimate read
|
|
40
|
+
// target, so it needs a non-string sentinel in the dedupe set. A leading space
|
|
41
|
+
// keeps it disjoint from every real session key / namespaced LCM key (which are
|
|
42
|
+
// `[A-Za-z0-9._-]` plus the U+001F namespace sentinel, never leading-space).
|
|
43
|
+
const UNDEFINED_SESSION_SENTINEL = " <lcm-sessionless>";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the ordered, deduped set of LCM read `session_id`s a recall section
|
|
47
|
+
* must query.
|
|
48
|
+
*
|
|
49
|
+
* When `sessionIds` is provided (the #1505 fallback unification), it is used
|
|
50
|
+
* verbatim, deduped while preserving first-seen order so the caller queries
|
|
51
|
+
* keys in priority order (primary overlay → fallbacks) without re-querying an
|
|
52
|
+
* identical key (e.g. when two namespaces both collapse to the default store).
|
|
53
|
+
* Otherwise the section reads under the single `sessionId`, so the result is
|
|
54
|
+
* `[sessionId]` — byte-for-byte the pre-#1505 single-key behavior, including a
|
|
55
|
+
* single `undefined` for a sessionless archive-wide read.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveLcmReadSessionIds(
|
|
58
|
+
target: LcmReadSessionTarget,
|
|
59
|
+
): Array<string | undefined> {
|
|
60
|
+
const source =
|
|
61
|
+
target.sessionIds && target.sessionIds.length > 0
|
|
62
|
+
? target.sessionIds
|
|
63
|
+
: [target.sessionId];
|
|
64
|
+
const seen = new Set<string>();
|
|
65
|
+
const out: Array<string | undefined> = [];
|
|
66
|
+
for (const sessionId of source) {
|
|
67
|
+
const key = sessionId === undefined ? UNDEFINED_SESSION_SENTINEL : sessionId;
|
|
68
|
+
if (seen.has(key)) continue;
|
|
69
|
+
seen.add(key);
|
|
70
|
+
out.push(sessionId);
|
|
71
|
+
}
|
|
72
|
+
// Defensive: an all-empty `sessionIds` still collapses to the single-key path.
|
|
73
|
+
return out.length > 0 ? out : [target.sessionId];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run a per-key LCM `gather` across the resolved read-key set with FAULT
|
|
78
|
+
* ISOLATION across keys (#1505 codex P2 review follow-up).
|
|
79
|
+
*
|
|
80
|
+
* A recall section that reads every key in a bare `for…await` loop loses the
|
|
81
|
+
* WHOLE section if any one key throws (e.g. a `SqliteError` from a corrupt or
|
|
82
|
+
* locked fallback index) — even when the primary overlay key already gathered
|
|
83
|
+
* evidence. The pre-#1505 first-non-empty read never had this problem: it
|
|
84
|
+
* returned the primary key's non-empty result without ever touching a failing
|
|
85
|
+
* fallback. This helper restores that resilience for the merged path: when more
|
|
86
|
+
* than one key is read, a per-key failure is contained so the other keys'
|
|
87
|
+
* evidence survives (best-effort recall — a total failure degrades to an empty
|
|
88
|
+
* section, which the orchestrator already treats as "no evidence").
|
|
89
|
+
*
|
|
90
|
+
* SINGLE-KEY is byte-for-byte the pre-#1505 behavior: the gather runs directly,
|
|
91
|
+
* so a failure PROPAGATES exactly as before (the caller / orchestrator catch
|
|
92
|
+
* still logs it). Fault isolation only engages once there is a fallback key that
|
|
93
|
+
* could fail independently of the primary.
|
|
94
|
+
*/
|
|
95
|
+
export async function gatherAcrossReadSessions(
|
|
96
|
+
sessionIds: ReadonlyArray<string | undefined>,
|
|
97
|
+
gather: (sessionId: string | undefined) => Promise<void>,
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
if (sessionIds.length <= 1) {
|
|
100
|
+
for (const sessionId of sessionIds) {
|
|
101
|
+
await gather(sessionId);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
for (const sessionId of sessionIds) {
|
|
106
|
+
try {
|
|
107
|
+
await gather(sessionId);
|
|
108
|
+
} catch {
|
|
109
|
+
// One read key failed; keep the evidence already gathered from the other
|
|
110
|
+
// keys instead of discarding the whole section.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|