@remnic/core 9.3.648 → 9.3.650
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 +4 -4
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +4 -4
- package/dist/access-mcp.d.ts +2 -2
- package/dist/access-mcp.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 +2 -2
- package/dist/bootstrap.d.ts +1 -1
- package/dist/{chunk-TWVRDGTX.js → chunk-23RYLGYA.js} +185 -55
- package/dist/chunk-23RYLGYA.js.map +1 -0
- package/dist/{chunk-CNRZ6WJU.js → chunk-3IJEQWQX.js} +4 -4
- package/dist/{chunk-XUGQQPGO.js → chunk-AGRPGAKR.js} +12 -1
- package/dist/chunk-AGRPGAKR.js.map +1 -0
- 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-6BNFVP7Y.js → chunk-RZOBQ23O.js} +2 -2
- package/dist/{chunk-AEIZEAP7.js → chunk-TUMH6EDV.js} +12 -15
- package/dist/chunk-TUMH6EDV.js.map +1 -0
- package/dist/{chunk-FUXV6HSO.js → chunk-TVOPSKOK.js} +3 -3
- package/dist/{chunk-5ETA6OAS.js → chunk-YAFSTKTH.js} +608 -80
- package/dist/chunk-YAFSTKTH.js.map +1 -0
- package/dist/{cli-DrL2Nv4j.d.ts → cli-BG4ybtJr.d.ts} +2 -2
- package/dist/cli.d.ts +3 -3
- package/dist/cli.js +7 -7
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +8 -8
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/{orchestrator-DEQW9j0Z.d.ts → orchestrator-CX-oqwJq.d.ts} +58 -0
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +3 -3
- package/dist/resume-bundles.js +2 -2
- package/dist/transcript.d.ts +18 -1
- package/dist/transcript.js +5 -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/cli.ts +10 -12
- package/src/coding/coding-namespace.test.ts +44 -0
- package/src/coding/coding-namespace.ts +163 -0
- package/src/orchestrator.ts +335 -77
- package/src/transcript-day-range.test.ts +101 -0
- package/src/transcript.ts +26 -0
- package/dist/chunk-5ETA6OAS.js.map +0 -1
- package/dist/chunk-AEIZEAP7.js.map +0 -1
- package/dist/chunk-TWVRDGTX.js.map +0 -1
- package/dist/chunk-XUGQQPGO.js.map +0 -1
- /package/dist/{chunk-CNRZ6WJU.js.map → chunk-3IJEQWQX.js.map} +0 -0
- /package/dist/{chunk-6BNFVP7Y.js.map → chunk-RZOBQ23O.js.map} +0 -0
- /package/dist/{chunk-FUXV6HSO.js.map → chunk-TVOPSKOK.js.map} +0 -0
package/src/orchestrator.ts
CHANGED
|
@@ -307,6 +307,7 @@ import {
|
|
|
307
307
|
} from "./namespaces/principal.js";
|
|
308
308
|
import {
|
|
309
309
|
combineNamespaces,
|
|
310
|
+
lcmReadSessionIdsForNamespaces,
|
|
310
311
|
resolveCodingNamespaceOverlay,
|
|
311
312
|
} from "./coding/coding-namespace.js";
|
|
312
313
|
import type { CodingContext } from "./types.js";
|
|
@@ -2011,6 +2012,66 @@ export class Orchestrator {
|
|
|
2011
2012
|
return this.applyCodingNamespaceOverlay(sessionKey, base);
|
|
2012
2013
|
}
|
|
2013
2014
|
|
|
2015
|
+
/**
|
|
2016
|
+
* Effective namespace a same-session LCM/structured-history READER must use
|
|
2017
|
+
* to find what the access `observe` surface WROTE (#1495 thread 2).
|
|
2018
|
+
*
|
|
2019
|
+
* This MUST mirror the `observe` scope plan's write-namespace resolution, NOT
|
|
2020
|
+
* `resolveSelfNamespace`: when no coding overlay applies, `observe` archives
|
|
2021
|
+
* under `config.defaultNamespace` (an unqualified observed turn is NOT moved
|
|
2022
|
+
* to the principal self namespace — identical to
|
|
2023
|
+
* `resolveCodingScopedWriteNamespace`/`memory_store`, rule 39). Only when a
|
|
2024
|
+
* coding overlay actually changes the namespace does the writer (and so the
|
|
2025
|
+
* reader) use the overlaid `project-*` namespace. Returning the self base for
|
|
2026
|
+
* the no-overlay case would prefix the read key with a namespace the writer
|
|
2027
|
+
* never used, so the reader would miss its own evidence.
|
|
2028
|
+
*
|
|
2029
|
+
* Honours the access-surface `principalOverride` (#1505 thread 2, codex): when
|
|
2030
|
+
* a recall supplies an authenticated principal NOT encoded in the raw
|
|
2031
|
+
* `sessionKey`, `observe` archived LCM under THAT principal's base namespace.
|
|
2032
|
+
* Deriving the base from `resolvePrincipal(sessionKey)` alone could fall back
|
|
2033
|
+
* to `default`, so principal `alice` observing `sess-1` would write under
|
|
2034
|
+
* `alice` but READ under `default`. Threading the override here keeps the read
|
|
2035
|
+
* base identical to the write base.
|
|
2036
|
+
*
|
|
2037
|
+
* READ-AUTHORIZATION gate (#1505 round 3, codex P2 "Gate LCM recall keys by
|
|
2038
|
+
* readable namespaces"): the overlay LCM read key is a `<principal>-project-*`
|
|
2039
|
+
* sub-namespace of the principal SELF base. The normal recall namespace set
|
|
2040
|
+
* below only substitutes the coding overlay when the principal SELF base is
|
|
2041
|
+
* actually in the readable recall set (`recallNamespacesForPrincipal` — gated
|
|
2042
|
+
* by `defaultRecallNamespaces.includes("self")` AND `canReadNamespace`). If a
|
|
2043
|
+
* principal can WRITE but not READ its self namespace (or `defaultRecall-
|
|
2044
|
+
* Namespaces` omits `self`), QMD/file recall never touches those overlay rows,
|
|
2045
|
+
* so neither may the LCM read key. When the self base is NOT readable, fall
|
|
2046
|
+
* back to the default store — exactly what an unqualified, unauthorized recall
|
|
2047
|
+
* resolves to — rather than injecting overlay rows the rest of recall excludes
|
|
2048
|
+
* (rule 42 read/write parity; rule 48 least-privilege).
|
|
2049
|
+
*/
|
|
2050
|
+
private lcmReadNamespaceForSession(
|
|
2051
|
+
sessionKey?: string,
|
|
2052
|
+
principalOverride?: string,
|
|
2053
|
+
): string {
|
|
2054
|
+
const principal =
|
|
2055
|
+
typeof principalOverride === "string" && principalOverride.length > 0
|
|
2056
|
+
? principalOverride
|
|
2057
|
+
: this.resolvePrincipal(sessionKey);
|
|
2058
|
+
const base = defaultNamespaceForPrincipal(principal, this.config);
|
|
2059
|
+
const overlaid = this.applyCodingNamespaceOverlay(sessionKey, base);
|
|
2060
|
+
// No overlay → collapse to the default store so the LCM key is the raw
|
|
2061
|
+
// sessionKey, exactly what an unqualified observe archived under.
|
|
2062
|
+
if (overlaid === base) return this.config.defaultNamespace;
|
|
2063
|
+
// Overlay applied. Only honour it when the principal SELF base is in the
|
|
2064
|
+
// readable recall set (same gate the recall namespace set uses to
|
|
2065
|
+
// substitute the overlay). Otherwise the overlay rows are unauthorized for
|
|
2066
|
+
// this reader — fall back to the default store so the LCM read matches
|
|
2067
|
+
// what QMD/file recall would surface.
|
|
2068
|
+
const selfReadableInRecall = recallNamespacesForPrincipal(
|
|
2069
|
+
principal,
|
|
2070
|
+
this.config,
|
|
2071
|
+
).includes(base);
|
|
2072
|
+
return selfReadableInRecall ? overlaid : this.config.defaultNamespace;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2014
2075
|
/**
|
|
2015
2076
|
* Attach a coding-agent context to a session (issue #569). Called by the
|
|
2016
2077
|
* Claude Code / Codex / Cursor connectors at session start after
|
|
@@ -6890,6 +6951,10 @@ export class Orchestrator {
|
|
|
6890
6951
|
.digest("hex")
|
|
6891
6952
|
.slice(0, 16);
|
|
6892
6953
|
const sectionBuckets = new Map<string, string[]>();
|
|
6954
|
+
// The effective LCM read session_id SET is computed below from
|
|
6955
|
+
// `recallNamespaces` (the SAME read-authorized namespace set normal QMD/file
|
|
6956
|
+
// recall searches, incl. coding `readFallbacks`). See the
|
|
6957
|
+
// `lcmReadSessionIds` derivation after `recallNamespaces` is built.
|
|
6893
6958
|
const queryPolicy = buildRecallQueryPolicy(prompt, sessionKey, {
|
|
6894
6959
|
cronRecallPolicyEnabled: this.config.cronRecallPolicyEnabled,
|
|
6895
6960
|
cronRecallNormalizedQueryMaxChars:
|
|
@@ -7119,6 +7184,86 @@ export class Orchestrator {
|
|
|
7119
7184
|
} else {
|
|
7120
7185
|
recallNamespaces = readableRecallNamespaces;
|
|
7121
7186
|
}
|
|
7187
|
+
// Effective LCM read NAMESPACE SET (#1505 thread "Include coding fallback
|
|
7188
|
+
// namespaces in LCM reads"). `observe` archives LCM / structured history
|
|
7189
|
+
// under `${effectiveNamespace}:${sessionKey}` for whichever namespace was
|
|
7190
|
+
// effective at write time. A branch-scoped session whose evidence was
|
|
7191
|
+
// archived at project / root scope must still surface it, exactly as normal
|
|
7192
|
+
// QMD/file recall does — QMD/file recall searches the primary overlay key AND
|
|
7193
|
+
// `codingOverlay.readFallbacks` (project / root), NOT just the primary
|
|
7194
|
+
// overlay key. The prior single `lcmReadSessionId` only targeted the primary
|
|
7195
|
+
// overlay, so branch-scoped sessions missed fallback LCM evidence.
|
|
7196
|
+
//
|
|
7197
|
+
// READ-AUTHORIZATION (preserved from the prior round's
|
|
7198
|
+
// `lcmReadNamespaceForSession` gate; rule 39 / 42 / 48): the coding-overlay
|
|
7199
|
+
// namespace AND its fallbacks are `<principal>-project-*` sub-namespaces of
|
|
7200
|
+
// the principal SELF base, authorized transitively by that base. They are
|
|
7201
|
+
// included ONLY when the principal self base is in the readable recall set
|
|
7202
|
+
// (`readableRecallNamespaces` — gated by `defaultRecallNamespaces.includes
|
|
7203
|
+
// ("self")` AND `canReadNamespace`). When the self base is NOT readable (e.g.
|
|
7204
|
+
// a write-only / self-omitted principal), the overlay rows are unauthorized
|
|
7205
|
+
// for this reader, so the LCM read collapses to the default store — exactly
|
|
7206
|
+
// what an unqualified, unauthorized recall resolves to — and NEVER searches a
|
|
7207
|
+
// `<principal>-project-*` key (no cross-tenant read leak). This mirrors what
|
|
7208
|
+
// the rest of recall surfaces for such a principal (its readable
|
|
7209
|
+
// shared/policy namespaces have no per-session LCM key, so they contribute
|
|
7210
|
+
// nothing here). `recallNamespaces` itself appends fallbacks unconditionally
|
|
7211
|
+
// for QMD/file recall; the LCM read keys apply the stricter, self-base gate
|
|
7212
|
+
// so the prior round's authorization invariant is preserved.
|
|
7213
|
+
const codingOverlaySelfReadable =
|
|
7214
|
+
codingOverlay !== null &&
|
|
7215
|
+
readableRecallNamespaces.includes(principalSelfNamespace);
|
|
7216
|
+
let lcmReadNamespaces: string[];
|
|
7217
|
+
if (namespaceOverride) {
|
|
7218
|
+
// Explicit namespace already read-authorized above (canReadNamespace gate).
|
|
7219
|
+
lcmReadNamespaces = [namespaceOverride];
|
|
7220
|
+
} else if (codingOverlay && codingSelfNamespace && codingOverlaySelfReadable) {
|
|
7221
|
+
// Self base readable → overlay rows authorized. Read the primary overlay
|
|
7222
|
+
// key first, then each coding read fallback (project → root), combined with
|
|
7223
|
+
// the principal base for isolation — the SAME ordered set QMD/file recall
|
|
7224
|
+
// searches for this authorized coding session.
|
|
7225
|
+
const fallbackNs = codingOverlay.readFallbacks.map((fallback) =>
|
|
7226
|
+
combineNamespaces(principalSelfNamespace, fallback),
|
|
7227
|
+
);
|
|
7228
|
+
lcmReadNamespaces = [codingSelfNamespace, ...fallbackNs];
|
|
7229
|
+
} else {
|
|
7230
|
+
// No overlay, OR overlay present but self base unreadable → collapse to the
|
|
7231
|
+
// default store (raw sessionKey), exactly as the prior round did. No
|
|
7232
|
+
// `<principal>-project-*` overlay key is searched.
|
|
7233
|
+
lcmReadNamespaces = [this.config.defaultNamespace];
|
|
7234
|
+
}
|
|
7235
|
+
// Map the ordered, read-authorized namespace set → ordered, deduped LCM read
|
|
7236
|
+
// session_id set. Single-user / no-overlay recall passes a single-namespace
|
|
7237
|
+
// set that collapses to the raw `sessionKey`, so this is `[sessionKey]` —
|
|
7238
|
+
// byte-for-byte the pre-#1495 single-key behavior.
|
|
7239
|
+
const lcmReadSessionIds = lcmReadSessionIdsForNamespaces(
|
|
7240
|
+
lcmReadNamespaces,
|
|
7241
|
+
sessionKey,
|
|
7242
|
+
this.config.defaultNamespace,
|
|
7243
|
+
);
|
|
7244
|
+
// Query an LCM-backed read across the ordered read key set and return the
|
|
7245
|
+
// FIRST non-empty result (#1505 fallback-namespace unification). The primary
|
|
7246
|
+
// overlay key is tried first; if a branch-scoped session has no rows under
|
|
7247
|
+
// its branch key, the project / root fallback keys are tried in order. Each
|
|
7248
|
+
// builder applies its own per-session budget/limit, so taking the first hit
|
|
7249
|
+
// (rather than concatenating across keys) preserves existing budgets while
|
|
7250
|
+
// recovering fallback evidence. When the set is a single key (single-user /
|
|
7251
|
+
// no-overlay / explicit-namespace), this is exactly one call — unchanged.
|
|
7252
|
+
// `lcmSessionId` is `string | undefined`: a SESSIONLESS recall yields the
|
|
7253
|
+
// single `undefined` key so the LCM builders run ONE archive-wide read with
|
|
7254
|
+
// no `session_id` filter (pre-#1505 behavior; the builders accept an optional
|
|
7255
|
+
// `sessionId`). NEVER the literal "default" session id (codex P2).
|
|
7256
|
+
const firstNonEmptyLcmRead = async <T>(
|
|
7257
|
+
read: (lcmSessionId: string | undefined) => Promise<T>,
|
|
7258
|
+
isEmpty: (value: T) => boolean,
|
|
7259
|
+
empty: T,
|
|
7260
|
+
): Promise<T> => {
|
|
7261
|
+
for (const lcmSessionId of lcmReadSessionIds) {
|
|
7262
|
+
const value = await read(lcmSessionId);
|
|
7263
|
+
if (!isEmpty(value)) return value;
|
|
7264
|
+
}
|
|
7265
|
+
return empty;
|
|
7266
|
+
};
|
|
7122
7267
|
const qmdAvailable = this.qmd.isAvailable();
|
|
7123
7268
|
let graphDecisionStatus: IntentDebugSnapshot["graphDecision"]["status"] =
|
|
7124
7269
|
recallDecision.plannedMode === "graph_mode" ? "skipped" : "not_requested";
|
|
@@ -9368,15 +9513,24 @@ export class Orchestrator {
|
|
|
9368
9513
|
(recallMode as RecallPlanMode) !== "no_recall"
|
|
9369
9514
|
) {
|
|
9370
9515
|
try {
|
|
9371
|
-
const explicitCueSection = await
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9516
|
+
const explicitCueSection = await firstNonEmptyLcmRead(
|
|
9517
|
+
(lcmSessionId) =>
|
|
9518
|
+
buildExplicitCueRecallSection({
|
|
9519
|
+
engine: this.lcmEngine,
|
|
9520
|
+
// #1495 thread 3 + #1505 fallback unification: read across the
|
|
9521
|
+
// ordered LCM read key set (primary overlay → coding fallbacks)
|
|
9522
|
+
// so a branch-scoped session finds its own explicit-cue evidence
|
|
9523
|
+
// even when archived at project/root scope (rule 39).
|
|
9524
|
+
sessionId: lcmSessionId,
|
|
9525
|
+
query: retrievalQuery,
|
|
9526
|
+
maxChars: explicitCueMaxChars,
|
|
9527
|
+
maxReferences:
|
|
9528
|
+
this.getRecallSectionNumber("explicit-cue", "maxResults") ??
|
|
9529
|
+
this.config.explicitCueRecallMaxReferences,
|
|
9530
|
+
}),
|
|
9531
|
+
(s) => !s,
|
|
9532
|
+
"",
|
|
9533
|
+
);
|
|
9380
9534
|
if (explicitCueSection) {
|
|
9381
9535
|
this.appendRecallSection(
|
|
9382
9536
|
sectionBuckets,
|
|
@@ -9406,21 +9560,29 @@ export class Orchestrator {
|
|
|
9406
9560
|
shouldRecallTargetedFactEvidence(retrievalQuery)
|
|
9407
9561
|
) {
|
|
9408
9562
|
try {
|
|
9409
|
-
const targetedFactSection = await
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9563
|
+
const targetedFactSection = await firstNonEmptyLcmRead(
|
|
9564
|
+
(lcmSessionId) =>
|
|
9565
|
+
buildTargetedFactRecallSection({
|
|
9566
|
+
engine: this.lcmEngine,
|
|
9567
|
+
// #1495 + #1505 fallback unification: read across the ordered LCM
|
|
9568
|
+
// read key set so a branch-scoped session finds its own
|
|
9569
|
+
// targeted-fact evidence even when archived at project/root scope.
|
|
9570
|
+
sessionId: lcmSessionId,
|
|
9571
|
+
query: retrievalQuery,
|
|
9572
|
+
maxChars: targetedFactMaxChars,
|
|
9573
|
+
maxSearchResults:
|
|
9574
|
+
this.getRecallSectionNumber("targeted-facts", "maxResults") ??
|
|
9575
|
+
this.config.targetedFactRecallMaxResults,
|
|
9576
|
+
maxScanWindowTurns:
|
|
9577
|
+
this.getRecallSectionNumber("targeted-facts", "maxTurns") ??
|
|
9578
|
+
this.config.targetedFactRecallScanWindowTurns,
|
|
9579
|
+
maxScanWindowTokens:
|
|
9580
|
+
this.getRecallSectionNumber("targeted-facts", "maxTokens") ??
|
|
9581
|
+
this.config.targetedFactRecallScanWindowTokens,
|
|
9582
|
+
}),
|
|
9583
|
+
(s) => !s,
|
|
9584
|
+
"",
|
|
9585
|
+
);
|
|
9424
9586
|
if (targetedFactSection) {
|
|
9425
9587
|
this.appendRecallSection(
|
|
9426
9588
|
sectionBuckets,
|
|
@@ -9451,21 +9613,29 @@ export class Orchestrator {
|
|
|
9451
9613
|
shouldRecallFocusedListEvidence(retrievalQuery)
|
|
9452
9614
|
) {
|
|
9453
9615
|
try {
|
|
9454
|
-
const focusedListSection = await
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9616
|
+
const focusedListSection = await firstNonEmptyLcmRead(
|
|
9617
|
+
(lcmSessionId) =>
|
|
9618
|
+
buildFocusedListRecallSection({
|
|
9619
|
+
engine: this.lcmEngine,
|
|
9620
|
+
// #1495 thread 3 + #1505 fallback unification: read across the
|
|
9621
|
+
// ordered LCM read key set so a branch-scoped session reads its own
|
|
9622
|
+
// focused-list/count evidence even at project/root scope (rule 39).
|
|
9623
|
+
sessionId: lcmSessionId,
|
|
9624
|
+
query: retrievalQuery,
|
|
9625
|
+
maxChars: focusedListMaxChars,
|
|
9626
|
+
maxSearchResults:
|
|
9627
|
+
this.getRecallSectionNumber("focused-list", "maxResults") ??
|
|
9628
|
+
this.config.focusedListRecallMaxResults,
|
|
9629
|
+
maxScanWindowTurns:
|
|
9630
|
+
this.getRecallSectionNumber("focused-list", "maxTurns") ??
|
|
9631
|
+
this.config.focusedListRecallScanWindowTurns,
|
|
9632
|
+
maxScanWindowTokens:
|
|
9633
|
+
this.getRecallSectionNumber("focused-list", "maxTokens") ??
|
|
9634
|
+
this.config.focusedListRecallScanWindowTokens,
|
|
9635
|
+
}),
|
|
9636
|
+
(s) => !s,
|
|
9637
|
+
"",
|
|
9638
|
+
);
|
|
9469
9639
|
if (focusedListSection) {
|
|
9470
9640
|
this.appendRecallSection(
|
|
9471
9641
|
sectionBuckets,
|
|
@@ -9500,22 +9670,30 @@ export class Orchestrator {
|
|
|
9500
9670
|
(responseGuidanceMatchesQuery || responseGuidanceForcedByPipeline)
|
|
9501
9671
|
) {
|
|
9502
9672
|
try {
|
|
9503
|
-
const responseGuidanceSection = await
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9673
|
+
const responseGuidanceSection = await firstNonEmptyLcmRead(
|
|
9674
|
+
(lcmSessionId) =>
|
|
9675
|
+
buildResponseGuidanceRecallSection({
|
|
9676
|
+
engine: this.lcmEngine,
|
|
9677
|
+
// #1495 thread 3 + #1505 fallback unification: read across the
|
|
9678
|
+
// ordered LCM read key set so a branch-scoped session reads its own
|
|
9679
|
+
// response-guidance evidence even at project/root scope (rule 39).
|
|
9680
|
+
sessionId: lcmSessionId,
|
|
9681
|
+
query: retrievalQuery,
|
|
9682
|
+
maxChars: responseGuidanceMaxChars,
|
|
9683
|
+
maxSearchResults:
|
|
9684
|
+
this.getRecallSectionNumber("response-guidance", "maxResults") ??
|
|
9685
|
+
this.config.responseGuidanceRecallMaxResults,
|
|
9686
|
+
maxScanWindowTurns:
|
|
9687
|
+
this.getRecallSectionNumber("response-guidance", "maxTurns") ??
|
|
9688
|
+
this.config.responseGuidanceRecallScanWindowTurns,
|
|
9689
|
+
maxScanWindowTokens:
|
|
9690
|
+
this.getRecallSectionNumber("response-guidance", "maxTokens") ??
|
|
9691
|
+
this.config.responseGuidanceRecallScanWindowTokens,
|
|
9692
|
+
forceGeneric: responseGuidanceForcedByPipeline,
|
|
9693
|
+
}),
|
|
9694
|
+
(s) => !s,
|
|
9695
|
+
"",
|
|
9696
|
+
);
|
|
9519
9697
|
if (responseGuidanceSection) {
|
|
9520
9698
|
this.appendRecallSection(
|
|
9521
9699
|
sectionBuckets,
|
|
@@ -9544,21 +9722,29 @@ export class Orchestrator {
|
|
|
9544
9722
|
shouldRecallEventOrderEvidence(retrievalQuery)
|
|
9545
9723
|
) {
|
|
9546
9724
|
try {
|
|
9547
|
-
const eventOrderSection = await
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9725
|
+
const eventOrderSection = await firstNonEmptyLcmRead(
|
|
9726
|
+
(lcmSessionId) =>
|
|
9727
|
+
buildEventOrderRecallSection({
|
|
9728
|
+
engine: this.lcmEngine,
|
|
9729
|
+
// #1495 thread 3 + #1505 fallback unification: read across the
|
|
9730
|
+
// ordered LCM read key set so a branch-scoped session reads its own
|
|
9731
|
+
// chronological event-order evidence even at project/root scope.
|
|
9732
|
+
sessionId: lcmSessionId,
|
|
9733
|
+
query: retrievalQuery,
|
|
9734
|
+
maxChars: eventOrderMaxChars,
|
|
9735
|
+
maxItems:
|
|
9736
|
+
this.getRecallSectionNumber("event-order", "maxResults") ??
|
|
9737
|
+
this.config.eventOrderRecallMaxResults,
|
|
9738
|
+
maxScanWindowTurns:
|
|
9739
|
+
this.getRecallSectionNumber("event-order", "maxTurns") ??
|
|
9740
|
+
this.config.eventOrderRecallScanWindowTurns,
|
|
9741
|
+
maxScanWindowTokens:
|
|
9742
|
+
this.getRecallSectionNumber("event-order", "maxTokens") ??
|
|
9743
|
+
this.config.eventOrderRecallScanWindowTokens,
|
|
9744
|
+
}),
|
|
9745
|
+
(s) => !s,
|
|
9746
|
+
"",
|
|
9747
|
+
);
|
|
9562
9748
|
if (eventOrderSection) {
|
|
9563
9749
|
this.appendRecallSection(
|
|
9564
9750
|
sectionBuckets,
|
|
@@ -9732,9 +9918,20 @@ export class Orchestrator {
|
|
|
9732
9918
|
(recallMode as RecallPlanMode) !== "no_recall"
|
|
9733
9919
|
) {
|
|
9734
9920
|
try {
|
|
9735
|
-
const structuredMatches = await
|
|
9736
|
-
|
|
9737
|
-
|
|
9921
|
+
const structuredMatches = await firstNonEmptyLcmRead(
|
|
9922
|
+
(lcmSessionId) =>
|
|
9923
|
+
this.lcmEngine!.searchStructuredParts(
|
|
9924
|
+
// #1495 + #1505 fallback unification: read across the ordered LCM
|
|
9925
|
+
// read key set so a branch-scoped session reads its own structured
|
|
9926
|
+
// message-part evidence even when archived at project/root scope.
|
|
9927
|
+
// Structured parts are inherently per-session (the DAG is keyed by
|
|
9928
|
+
// session_id), so a SESSIONLESS read (`undefined`) normalizes to
|
|
9929
|
+
// empty → no section, the correct pre-#1505 behavior (codex P2).
|
|
9930
|
+
lcmSessionId ?? "",
|
|
9931
|
+
retrievalQuery,
|
|
9932
|
+
),
|
|
9933
|
+
(matches) => matches.length === 0,
|
|
9934
|
+
[],
|
|
9738
9935
|
);
|
|
9739
9936
|
const structuredSection = this.lcmEngine.formatStructuredRecall(
|
|
9740
9937
|
structuredMatches,
|
|
@@ -9758,9 +9955,20 @@ export class Orchestrator {
|
|
|
9758
9955
|
}
|
|
9759
9956
|
}
|
|
9760
9957
|
}
|
|
9761
|
-
const lcmSection = await
|
|
9762
|
-
|
|
9763
|
-
|
|
9958
|
+
const lcmSection = await firstNonEmptyLcmRead(
|
|
9959
|
+
(lcmSessionId) =>
|
|
9960
|
+
this.lcmEngine!.assembleRecall(
|
|
9961
|
+
// #1495 + #1505 fallback unification: read across the ordered LCM
|
|
9962
|
+
// read key set so a branch-scoped session reads its own
|
|
9963
|
+
// compressed-history evidence even at project/root scope.
|
|
9964
|
+
// Compressed history is inherently per-session (a per-session DAG),
|
|
9965
|
+
// so a SESSIONLESS read (`undefined`) normalizes to empty → no
|
|
9966
|
+
// section, the correct pre-#1505 behavior (codex P2).
|
|
9967
|
+
lcmSessionId ?? "",
|
|
9968
|
+
this.config.recallBudgetChars,
|
|
9969
|
+
),
|
|
9970
|
+
(s) => !s,
|
|
9971
|
+
"",
|
|
9764
9972
|
);
|
|
9765
9973
|
if (lcmSection) {
|
|
9766
9974
|
this.appendRecallSection(
|
|
@@ -11235,6 +11443,28 @@ export class Orchestrator {
|
|
|
11235
11443
|
deadlineMs?: number;
|
|
11236
11444
|
archiveLcm?: boolean;
|
|
11237
11445
|
abortSignal?: AbortSignal;
|
|
11446
|
+
/**
|
|
11447
|
+
* Pin extraction writes to this namespace instead of deriving one from
|
|
11448
|
+
* `defaultNamespaceForPrincipal(resolvePrincipal(sessionKey))` + the
|
|
11449
|
+
* coding overlay (#1495). The access `observe` surface resolves a single
|
|
11450
|
+
* effective scope plan and passes its `writeNamespace` here so the
|
|
11451
|
+
* extracted memories land in the SAME namespace as LCM archival,
|
|
11452
|
+
* objective-state snapshots, and project-scoped recall — without relying
|
|
11453
|
+
* on re-deriving the namespace from a namespace-prefixed session key.
|
|
11454
|
+
* Same hook bulk-import uses (#460).
|
|
11455
|
+
*/
|
|
11456
|
+
writeNamespaceOverride?: string;
|
|
11457
|
+
/**
|
|
11458
|
+
* Pin the provenance PRINCIPAL instead of deriving it from
|
|
11459
|
+
* `resolvePrincipal(turn.sessionKey)` (#1495 thread 1). The access
|
|
11460
|
+
* `observe` surface authenticates the caller at the transport layer and
|
|
11461
|
+
* passes its resolved principal here so extracted-memory provenance uses
|
|
11462
|
+
* the SAME identity the surface authorized — independent of storage
|
|
11463
|
+
* routing (`writeNamespaceOverride`) and of whatever `resolvePrincipal`
|
|
11464
|
+
* would parse from the raw session key. Mirrors the recall path's
|
|
11465
|
+
* `principalOverride` (issue #570 PR 4).
|
|
11466
|
+
*/
|
|
11467
|
+
principalOverride?: string;
|
|
11238
11468
|
} = {},
|
|
11239
11469
|
): Promise<void> {
|
|
11240
11470
|
if (!Array.isArray(turns) || turns.length === 0) return;
|
|
@@ -11321,6 +11551,8 @@ export class Orchestrator {
|
|
|
11321
11551
|
bufferKey,
|
|
11322
11552
|
extractionDeadlineMs: options.deadlineMs,
|
|
11323
11553
|
abortSignal: options.abortSignal,
|
|
11554
|
+
writeNamespaceOverride: options.writeNamespaceOverride,
|
|
11555
|
+
principalOverride: options.principalOverride,
|
|
11324
11556
|
onTaskSettled: (err) => (err ? reject(err) : resolve()),
|
|
11325
11557
|
}).catch(reject);
|
|
11326
11558
|
}),
|
|
@@ -11717,6 +11949,12 @@ export class Orchestrator {
|
|
|
11717
11949
|
* regardless of user-configured principal routing rules.
|
|
11718
11950
|
*/
|
|
11719
11951
|
writeNamespaceOverride?: string;
|
|
11952
|
+
/**
|
|
11953
|
+
* Pin the provenance principal (#1495 thread 1). Forwarded to
|
|
11954
|
+
* `runExtraction` so access `observe` can record provenance under the
|
|
11955
|
+
* authenticated principal instead of `resolvePrincipal(sessionKey)`.
|
|
11956
|
+
*/
|
|
11957
|
+
principalOverride?: string;
|
|
11720
11958
|
} = {},
|
|
11721
11959
|
): Promise<void> {
|
|
11722
11960
|
const bufferKey = options.bufferKey ?? turnsToExtract[0]?.sessionKey ?? "default";
|
|
@@ -11790,6 +12028,7 @@ export class Orchestrator {
|
|
|
11790
12028
|
abortSignal: options.abortSignal,
|
|
11791
12029
|
failOnExtractionFailure: options.failOnExtractionFailure === true,
|
|
11792
12030
|
writeNamespaceOverride: options.writeNamespaceOverride,
|
|
12031
|
+
principalOverride: options.principalOverride,
|
|
11793
12032
|
});
|
|
11794
12033
|
settleTask(undefined, result);
|
|
11795
12034
|
} catch (err) {
|
|
@@ -11946,6 +12185,14 @@ export class Orchestrator {
|
|
|
11946
12185
|
* for provenance; only the storage target is overridden.
|
|
11947
12186
|
*/
|
|
11948
12187
|
writeNamespaceOverride?: string;
|
|
12188
|
+
/**
|
|
12189
|
+
* Pin the provenance principal instead of deriving it from
|
|
12190
|
+
* `resolvePrincipal(sessionKey)` (#1495 thread 1). When set, this is the
|
|
12191
|
+
* identity an access surface already authenticated; used so observed-turn
|
|
12192
|
+
* provenance is correct even though `turn.sessionKey` is the ORIGINAL
|
|
12193
|
+
* (un-prefixed) key and storage is pinned via `writeNamespaceOverride`.
|
|
12194
|
+
*/
|
|
12195
|
+
principalOverride?: string;
|
|
11949
12196
|
} = {},
|
|
11950
12197
|
): Promise<ExtractionRunResult> {
|
|
11951
12198
|
log.debug(`running extraction on ${turns.length} turns`);
|
|
@@ -12039,7 +12286,18 @@ export class Orchestrator {
|
|
|
12039
12286
|
};
|
|
12040
12287
|
}
|
|
12041
12288
|
|
|
12042
|
-
|
|
12289
|
+
// Provenance principal honours the access-surface override (#1495 thread 1,
|
|
12290
|
+
// mirroring the recall path's `principalOverride`, issue #570 PR 4). Access
|
|
12291
|
+
// surfaces that authenticated the caller at the transport layer pass their
|
|
12292
|
+
// resolved principal so provenance uses the SAME identity the surface
|
|
12293
|
+
// authorized, instead of `resolvePrincipal(sessionKey)` — which on a
|
|
12294
|
+
// namespace-prefixed key would collapse to `default`. The ORIGINAL,
|
|
12295
|
+
// un-prefixed session key still drives threading.
|
|
12296
|
+
const principal =
|
|
12297
|
+
typeof options.principalOverride === "string" &&
|
|
12298
|
+
options.principalOverride.length > 0
|
|
12299
|
+
? options.principalOverride
|
|
12300
|
+
: resolvePrincipal(sessionKey, this.config);
|
|
12043
12301
|
// Write path — overlay the coding-agent namespace (issue #569) when the
|
|
12044
12302
|
// session has a codingContext and `codingMode.projectScope` is true.
|
|
12045
12303
|
// Explicit `writeNamespaceOverride` from callers still wins, matching
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the CLI transcript day-view window helper `utcDayRange`.
|
|
3
|
+
*
|
|
4
|
+
* The `transcript --date <day>` and default "today" CLI views read a single
|
|
5
|
+
* calendar day via {@link TranscriptManager.readRange}, which uses a half-open
|
|
6
|
+
* `[start, end)` interval (`entryTime < end`, CLAUDE.md rule #35). The day end
|
|
7
|
+
* must therefore be the NEXT day's `00:00:00Z`, not `${date}T23:59:59Z`:
|
|
8
|
+
* a literal `23:59:59Z` end (== `.000Z`) silently drops any entry stamped in
|
|
9
|
+
* the final second of the day (`23:59:59.000Z`–`23:59:59.999Z`).
|
|
10
|
+
*
|
|
11
|
+
* (Pre-existing boundary gap flagged as a P2 on PR #1507, fixed out of band.)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { mkdtemp, realpath, rm } from "node:fs/promises";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import test from "node:test";
|
|
19
|
+
|
|
20
|
+
import { TranscriptManager, utcDayRange } from "./transcript.js";
|
|
21
|
+
import type { PluginConfig, TranscriptEntry } from "./types.js";
|
|
22
|
+
|
|
23
|
+
function makeConfig(memoryDir: string): PluginConfig {
|
|
24
|
+
// TranscriptManager only reads memoryDir + transcriptSkipChannelTypes.
|
|
25
|
+
return {
|
|
26
|
+
memoryDir,
|
|
27
|
+
transcriptSkipChannelTypes: [],
|
|
28
|
+
} as unknown as PluginConfig;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// On macOS `os.tmpdir()` is a `/var/folders/...` symlink to `/private/var/...`.
|
|
32
|
+
// Canonicalize the test root upfront (issue #691 symlink convention).
|
|
33
|
+
async function makeMemoryDir(): Promise<string> {
|
|
34
|
+
return realpath(await mkdtemp(path.join(os.tmpdir(), "remnic-tx-dayrange-")));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function entryAt(timestamp: string, turnId: string): TranscriptEntry {
|
|
38
|
+
return {
|
|
39
|
+
sessionKey: "agent:generalist:main",
|
|
40
|
+
turnId,
|
|
41
|
+
role: "user",
|
|
42
|
+
content: `content-${turnId}`,
|
|
43
|
+
timestamp,
|
|
44
|
+
} as TranscriptEntry;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── utcDayRange: half-open [start, next-day-00:00:00Z) ───────────────────────
|
|
48
|
+
|
|
49
|
+
test("utcDayRange end is the next day's 00:00:00Z (half-open day window)", () => {
|
|
50
|
+
assert.deepEqual(utcDayRange("2025-03-15"), {
|
|
51
|
+
start: "2025-03-15T00:00:00Z",
|
|
52
|
+
end: "2025-03-16T00:00:00Z",
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("utcDayRange rolls over month boundaries", () => {
|
|
57
|
+
assert.deepEqual(utcDayRange("2025-01-31"), {
|
|
58
|
+
start: "2025-01-31T00:00:00Z",
|
|
59
|
+
end: "2025-02-01T00:00:00Z",
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("utcDayRange rolls over leap-day and year boundaries", () => {
|
|
64
|
+
assert.deepEqual(utcDayRange("2024-02-28"), {
|
|
65
|
+
start: "2024-02-28T00:00:00Z",
|
|
66
|
+
end: "2024-02-29T00:00:00Z",
|
|
67
|
+
});
|
|
68
|
+
assert.deepEqual(utcDayRange("2025-12-31"), {
|
|
69
|
+
start: "2025-12-31T00:00:00Z",
|
|
70
|
+
end: "2026-01-01T00:00:00Z",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ── Day-view range includes the final second of the day (the bug fix) ────────
|
|
75
|
+
|
|
76
|
+
test("day-view range returns an entry stamped at 23:59:59.500Z of the queried day", async () => {
|
|
77
|
+
const memoryDir = await makeMemoryDir();
|
|
78
|
+
try {
|
|
79
|
+
const tm = new TranscriptManager(makeConfig(memoryDir));
|
|
80
|
+
const date = "2025-03-15";
|
|
81
|
+
const channel = "agent:generalist:main";
|
|
82
|
+
|
|
83
|
+
// Entry in the final second of the day — dropped by the old
|
|
84
|
+
// `${date}T23:59:59Z` (== `.000Z`) exclusive upper bound.
|
|
85
|
+
await tm.append(entryAt(`${date}T23:59:59.500Z`, "final-second"));
|
|
86
|
+
// Midday control entry, comfortably inside the window.
|
|
87
|
+
await tm.append(entryAt(`${date}T12:00:00.000Z`, "midday"));
|
|
88
|
+
// Start-of-next-day entry must stay OUT — the upper bound is exclusive
|
|
89
|
+
// (`[start, end)`, rule #35), so `nextDate T00:00:00.000Z` is not the
|
|
90
|
+
// queried day.
|
|
91
|
+
await tm.append(entryAt("2025-03-16T00:00:00.000Z", "next-day"));
|
|
92
|
+
|
|
93
|
+
const { start, end } = utcDayRange(date);
|
|
94
|
+
const entries = await tm.readRange(start, end, channel);
|
|
95
|
+
const ids = entries.map((e) => e.turnId).sort();
|
|
96
|
+
|
|
97
|
+
assert.deepEqual(ids, ["final-second", "midday"]);
|
|
98
|
+
} finally {
|
|
99
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
package/src/transcript.ts
CHANGED
|
@@ -33,6 +33,32 @@ function makeRawLineDeduper(): (rawLine: string) => boolean {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Compute the half-open UTC instant range `[start, end)` that covers an entire
|
|
38
|
+
* calendar day, suitable for {@link TranscriptManager.readRange}.
|
|
39
|
+
*
|
|
40
|
+
* `readRange` filters with an EXCLUSIVE upper bound (`entryTime < end`, CLAUDE.md
|
|
41
|
+
* rule #35 / AGENTS.md rule 23). The end is therefore the NEXT day's
|
|
42
|
+
* `00:00:00Z`, not `${date}T23:59:59Z`: a literal `23:59:59Z` end (== `.000Z`)
|
|
43
|
+
* would drop any entry stamped in the final second of the day
|
|
44
|
+
* (`23:59:59.000Z`–`23:59:59.999Z`). Using the next day's midnight keeps the
|
|
45
|
+
* `[start, end)` semantics intact while including the whole day.
|
|
46
|
+
*
|
|
47
|
+
* @param date - A `YYYY-MM-DD` calendar day (UTC).
|
|
48
|
+
*/
|
|
49
|
+
export function utcDayRange(date: string): { start: string; end: string } {
|
|
50
|
+
const start = `${date}T00:00:00Z`;
|
|
51
|
+
const startMs = Date.parse(start);
|
|
52
|
+
if (Number.isNaN(startMs)) {
|
|
53
|
+
// Malformed date: preserve the pre-existing "empty range" behavior (an
|
|
54
|
+
// unparseable bound makes `readRange` match nothing) rather than throwing.
|
|
55
|
+
return { start, end: start };
|
|
56
|
+
}
|
|
57
|
+
const next = new Date(startMs);
|
|
58
|
+
next.setUTCDate(next.getUTCDate() + 1);
|
|
59
|
+
return { start, end: `${next.toISOString().slice(0, 10)}T00:00:00Z` };
|
|
60
|
+
}
|
|
61
|
+
|
|
36
62
|
/**
|
|
37
63
|
* Manages conversation transcript storage, checkpointing, and recall formatting.
|
|
38
64
|
*
|