@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.
Files changed (54) hide show
  1. package/dist/access-cli.js +4 -4
  2. package/dist/access-http.d.ts +2 -2
  3. package/dist/access-http.js +4 -4
  4. package/dist/access-mcp.d.ts +2 -2
  5. package/dist/access-mcp.js +3 -3
  6. package/dist/{access-service-DFXIlGvZ.d.ts → access-service-DIZRHQ7Q.d.ts} +255 -2
  7. package/dist/access-service.d.ts +2 -2
  8. package/dist/access-service.js +2 -2
  9. package/dist/bootstrap.d.ts +1 -1
  10. package/dist/{chunk-TWVRDGTX.js → chunk-23RYLGYA.js} +185 -55
  11. package/dist/chunk-23RYLGYA.js.map +1 -0
  12. package/dist/{chunk-CNRZ6WJU.js → chunk-3IJEQWQX.js} +4 -4
  13. package/dist/{chunk-XUGQQPGO.js → chunk-AGRPGAKR.js} +12 -1
  14. package/dist/chunk-AGRPGAKR.js.map +1 -0
  15. package/dist/{chunk-6GIKAUTN.js → chunk-MMJANTJX.js} +33 -2
  16. package/dist/{chunk-6GIKAUTN.js.map → chunk-MMJANTJX.js.map} +1 -1
  17. package/dist/{chunk-6BNFVP7Y.js → chunk-RZOBQ23O.js} +2 -2
  18. package/dist/{chunk-AEIZEAP7.js → chunk-TUMH6EDV.js} +12 -15
  19. package/dist/chunk-TUMH6EDV.js.map +1 -0
  20. package/dist/{chunk-FUXV6HSO.js → chunk-TVOPSKOK.js} +3 -3
  21. package/dist/{chunk-5ETA6OAS.js → chunk-YAFSTKTH.js} +608 -80
  22. package/dist/chunk-YAFSTKTH.js.map +1 -0
  23. package/dist/{cli-DrL2Nv4j.d.ts → cli-BG4ybtJr.d.ts} +2 -2
  24. package/dist/cli.d.ts +3 -3
  25. package/dist/cli.js +7 -7
  26. package/dist/explicit-capture.d.ts +1 -1
  27. package/dist/index.d.ts +4 -4
  28. package/dist/index.js +8 -8
  29. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  30. package/dist/{orchestrator-DEQW9j0Z.d.ts → orchestrator-CX-oqwJq.d.ts} +58 -0
  31. package/dist/orchestrator.d.ts +1 -1
  32. package/dist/orchestrator.js +3 -3
  33. package/dist/resume-bundles.js +2 -2
  34. package/dist/transcript.d.ts +18 -1
  35. package/dist/transcript.js +5 -3
  36. package/package.json +1 -1
  37. package/src/access-service-lcm-forgery.test.ts +410 -0
  38. package/src/access-service-observe-lcm-parity.test.ts +1397 -0
  39. package/src/access-service-observe-scope.test.ts +599 -0
  40. package/src/access-service-raw-excerpt-read-gate.test.ts +443 -0
  41. package/src/access-service.ts +1270 -113
  42. package/src/cli.ts +10 -12
  43. package/src/coding/coding-namespace.test.ts +44 -0
  44. package/src/coding/coding-namespace.ts +163 -0
  45. package/src/orchestrator.ts +335 -77
  46. package/src/transcript-day-range.test.ts +101 -0
  47. package/src/transcript.ts +26 -0
  48. package/dist/chunk-5ETA6OAS.js.map +0 -1
  49. package/dist/chunk-AEIZEAP7.js.map +0 -1
  50. package/dist/chunk-TWVRDGTX.js.map +0 -1
  51. package/dist/chunk-XUGQQPGO.js.map +0 -1
  52. /package/dist/{chunk-CNRZ6WJU.js.map → chunk-3IJEQWQX.js.map} +0 -0
  53. /package/dist/{chunk-6BNFVP7Y.js.map → chunk-RZOBQ23O.js.map} +0 -0
  54. /package/dist/{chunk-FUXV6HSO.js.map → chunk-TVOPSKOK.js.map} +0 -0
@@ -0,0 +1,599 @@
1
+ /**
2
+ * #1495: `observe` must resolve EVERY memory-producing side effect through ONE
3
+ * effective scope plan, so observed turns and extracted memories land in the
4
+ * SAME namespace that same-session project-scoped recall searches.
5
+ *
6
+ * Before this change, `observe`:
7
+ * - applied the coding overlay to the objective-state snapshot target, but
8
+ * - keyed LCM archival (`lcmSessionKey`) and extraction replay turns off the
9
+ * EARLIER base namespace (`resolveWritableNamespace(undefined, …)` ==
10
+ * `config.defaultNamespace`), and
11
+ * - returned the base namespace in the response.
12
+ *
13
+ * The fix introduces an internal `MemoryScopePlan` resolver. `observe` consumes
14
+ * it so the LCM key, the extraction write target, the objective-state target,
15
+ * and the response `effectiveNamespace` all agree.
16
+ *
17
+ * Invariants verified here (rule 39 / 42 / 47 / 48 / 51):
18
+ * - Agreement: LCM key, extraction writeNamespaceOverride, objective-state
19
+ * target, and response.effectiveNamespace ALL == scope.writeNamespace, and
20
+ * that equals what a same-session project-scoped resolve produces.
21
+ * - Explicit namespace wins and is NOT silently overridden by project context.
22
+ * - No sessionKey ⇒ no overlay (observe requires a sessionKey, so this is the
23
+ * explicit-namespace / namespaces-disabled equivalents).
24
+ * - `codingMode.projectScope: false` ⇒ no overlay.
25
+ * - `namespacesEnabled: false` ⇒ single-store behavior preserved.
26
+ * - Unauthorized explicit namespace throws BEFORE any session-context mutation.
27
+ */
28
+ import assert from "node:assert/strict";
29
+ import { execFileSync } from "node:child_process";
30
+ import { mkdtempSync, rmSync } from "node:fs";
31
+ import { tmpdir } from "node:os";
32
+ import { join } from "node:path";
33
+ import test from "node:test";
34
+
35
+ import { EngramAccessService } from "./access-service.js";
36
+ import { Orchestrator } from "./orchestrator.js";
37
+ import type { EngramAccessObserveRequest } from "./access-service.js";
38
+ import {
39
+ combineNamespaces,
40
+ lcmSessionKeyForNamespace,
41
+ projectNamespaceName,
42
+ projectTagProjectId,
43
+ } from "./coding/coding-namespace.js";
44
+ import { resolveGitContext } from "./coding/git-context.js";
45
+ import type { CodingContext, PluginConfig } from "./types.js";
46
+
47
+ /**
48
+ * Encode the expected namespaced LCM `session_id` via the SAME shared helper
49
+ * production uses, so these assertions stay shape-agnostic after the #1495 P1
50
+ * fix made the namespaced encoding sentinel-framed and unforgeable (rule 22).
51
+ */
52
+ function encodeNs(namespace: string, sessionKey: string): string {
53
+ return lcmSessionKeyForNamespace(namespace, sessionKey, "default") ?? sessionKey;
54
+ }
55
+
56
+ interface ObserveProbe {
57
+ orch: Orchestrator;
58
+ contexts: Map<string, CodingContext>;
59
+ lcmCalls: Array<{ sessionKey: string }>;
60
+ extractionCalls: Array<{
61
+ sessionKeys: string[];
62
+ writeNamespaceOverride?: string;
63
+ principalOverride?: string;
64
+ }>;
65
+ objectiveStateNamespaces: string[];
66
+ }
67
+
68
+ /**
69
+ * Build an orchestrator stub wired to record every namespace-bearing side
70
+ * effect `observe` produces: LCM enqueue, extraction replay, and the storage
71
+ * router lookup that the objective-state snapshot writer goes through.
72
+ */
73
+ function makeObserveProbe(overrides: Partial<PluginConfig> = {}): ObserveProbe {
74
+ const contexts = new Map<string, CodingContext>();
75
+ const lcmCalls: ObserveProbe["lcmCalls"] = [];
76
+ const extractionCalls: ObserveProbe["extractionCalls"] = [];
77
+ const objectiveStateNamespaces: string[] = [];
78
+
79
+ const config = {
80
+ namespacesEnabled: true,
81
+ defaultNamespace: "default",
82
+ sharedNamespace: "shared",
83
+ namespacePolicies: [],
84
+ codingMode: { projectScope: true },
85
+ memoryDir: "/synthetic/remnic-observe-scope",
86
+ objectiveStateMemoryEnabled: true,
87
+ objectiveStateSnapshotWritesEnabled: true,
88
+ principalFromSessionKeyMode: "prefix",
89
+ principalFromSessionKeyRules: [],
90
+ recallCrossNamespaceBudgetEnabled: false,
91
+ recallCrossNamespaceBudgetWindowMs: 60_000,
92
+ recallCrossNamespaceBudgetSoftLimit: 10,
93
+ recallCrossNamespaceBudgetHardLimit: 30,
94
+ ...overrides,
95
+ } as unknown as PluginConfig;
96
+
97
+ const orch = {
98
+ config,
99
+ getCodingContextForSession: (sk: string | undefined) =>
100
+ (sk ? contexts.get(sk) : null) ?? null,
101
+ setCodingContextForSession: (sk: string, ctx: CodingContext | null) => {
102
+ if (ctx === null) contexts.delete(sk);
103
+ else contexts.set(sk, ctx);
104
+ },
105
+ applyCodingNamespaceOverlay: (sk: string | undefined, base: string) =>
106
+ Orchestrator.prototype.applyCodingNamespaceOverlay.call(orch, sk, base),
107
+ // The objective-state snapshot writer goes through getStorage(namespace).
108
+ // Capturing the namespace it resolves lets us assert the objective-state
109
+ // target without touching the filesystem.
110
+ getStorage: async (ns: string) => {
111
+ objectiveStateNamespaces.push(ns);
112
+ return { dir: `/synthetic/storage/${ns}` };
113
+ },
114
+ lcmEngine: {
115
+ enabled: true,
116
+ enqueueObserveMessages: (sessionKey: string) => {
117
+ lcmCalls.push({ sessionKey });
118
+ },
119
+ },
120
+ ingestReplayBatch: async (
121
+ turns: Array<{ sessionKey: string }>,
122
+ options: { writeNamespaceOverride?: string; principalOverride?: string } = {},
123
+ ) => {
124
+ extractionCalls.push({
125
+ sessionKeys: turns.map((t) => t.sessionKey),
126
+ writeNamespaceOverride: options.writeNamespaceOverride,
127
+ principalOverride: options.principalOverride,
128
+ });
129
+ },
130
+ } as unknown as Orchestrator;
131
+
132
+ return { orch, contexts, lcmCalls, extractionCalls, objectiveStateNamespaces };
133
+ }
134
+
135
+ function observeRequest(
136
+ overrides: Partial<EngramAccessObserveRequest>,
137
+ ): EngramAccessObserveRequest {
138
+ return {
139
+ sessionKey: "sess-observe",
140
+ messages: [
141
+ { role: "user", content: "what database are we using?" },
142
+ { role: "assistant", content: "we use postgres for the primary store" },
143
+ ],
144
+ ...overrides,
145
+ } as EngramAccessObserveRequest;
146
+ }
147
+
148
+ /** A principal whose self namespace exists, so the overlay base is non-default. */
149
+ function withSelfPolicyPrefix(principal: string): Partial<PluginConfig> {
150
+ return {
151
+ namespacePolicies: [
152
+ { name: principal, readPrincipals: [principal], writePrincipals: [principal] },
153
+ ],
154
+ principalFromSessionKeyMode: "prefix",
155
+ principalFromSessionKeyRules: [{ match: `${principal}:`, principal }],
156
+ } as Partial<PluginConfig>;
157
+ }
158
+
159
+ test("#1495 projectTag: LCM, extraction, objective-state, and response all agree on the effective namespace", async () => {
160
+ const probe = makeObserveProbe(withSelfPolicyPrefix("pi-geek"));
161
+ const service = new EngramAccessService(probe.orch);
162
+
163
+ const res = await service.observe(
164
+ observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
165
+ );
166
+
167
+ // Effective write namespace == principal self base overlaid with the project,
168
+ // EXACTLY what a same-session project-scoped recall/store resolves.
169
+ const expected = combineNamespaces(
170
+ "pi-geek",
171
+ projectNamespaceName(projectTagProjectId("Blend/Supply")),
172
+ );
173
+
174
+ assert.equal(res.effectiveNamespace, expected, "response effectiveNamespace");
175
+ assert.notEqual(expected, "default", "overlay must change the namespace");
176
+
177
+ // LCM archival key carries the effective namespace prefix.
178
+ assert.equal(probe.lcmCalls.length, 1);
179
+ assert.equal(
180
+ probe.lcmCalls[0].sessionKey,
181
+ encodeNs(expected, "pi-geek:abc123"),
182
+ "LCM key must be prefixed with the EFFECTIVE write namespace",
183
+ );
184
+
185
+ // #1505 thread 1 (identity-vs-routing separation): extraction replay turns
186
+ // carry the ORIGINAL, un-prefixed session key so provenance principal
187
+ // resolution and conversation threading see the real identity — NOT the
188
+ // namespace-prefixed key (which `resolvePrincipal` would collapse to
189
+ // `default`). Storage routing is pinned independently via
190
+ // writeNamespaceOverride, and the authenticated principal via principalOverride.
191
+ assert.equal(probe.extractionCalls.length, 1);
192
+ assert.deepEqual(
193
+ new Set(probe.extractionCalls[0].sessionKeys),
194
+ new Set(["pi-geek:abc123"]),
195
+ "extraction replay turns must carry the ORIGINAL session key (identity), not the namespace-prefixed key",
196
+ );
197
+ assert.equal(
198
+ probe.extractionCalls[0].writeNamespaceOverride,
199
+ expected,
200
+ "extraction must pin the write (routing) to the effective namespace",
201
+ );
202
+ assert.equal(
203
+ probe.extractionCalls[0].principalOverride,
204
+ "pi-geek",
205
+ "extraction must pin provenance to the resolved principal, not a default parsed from a prefixed key",
206
+ );
207
+
208
+ // Objective-state snapshot target == effective namespace.
209
+ assert.ok(
210
+ probe.objectiveStateNamespaces.every((ns) => ns === expected),
211
+ `objective-state target must be the effective namespace, got ${JSON.stringify(probe.objectiveStateNamespaces)}`,
212
+ );
213
+ });
214
+
215
+ test("#1495 cwd (git repo): every observe side effect agrees on the effective namespace", async () => {
216
+ const repoDir = mkdtempSync(join(tmpdir(), "remnic-observe-git-"));
217
+ // A real (synthetic) git repo so resolveGitContext can read rev-parse output.
218
+ // No remote/commit needed — projectId derives from the resolved root path.
219
+ const git = (...args: string[]) =>
220
+ execFileSync("git", args, { cwd: repoDir, stdio: "pipe" });
221
+ git("init", "-q");
222
+ git("config", "user.email", "test@example.com");
223
+ git("config", "user.name", "Test");
224
+ try {
225
+ const gitCtx = await resolveGitContext(repoDir);
226
+ assert.ok(gitCtx, "synthetic repo must resolve a git context");
227
+
228
+ const probe = makeObserveProbe(withSelfPolicyPrefix("pi-geek"));
229
+ const service = new EngramAccessService(probe.orch);
230
+
231
+ const res = await service.observe(
232
+ observeRequest({ sessionKey: "pi-geek:cwd1", cwd: repoDir }),
233
+ );
234
+
235
+ const expected = combineNamespaces(
236
+ "pi-geek",
237
+ projectNamespaceName(gitCtx!.projectId),
238
+ );
239
+
240
+ assert.equal(res.effectiveNamespace, expected);
241
+ assert.equal(probe.lcmCalls[0].sessionKey, encodeNs(expected, "pi-geek:cwd1"));
242
+ // #1505 thread 1: extraction turns carry the ORIGINAL session key (identity);
243
+ // routing + provenance are pinned via the override options.
244
+ assert.deepEqual(
245
+ new Set(probe.extractionCalls[0].sessionKeys),
246
+ new Set(["pi-geek:cwd1"]),
247
+ );
248
+ assert.equal(probe.extractionCalls[0].writeNamespaceOverride, expected);
249
+ assert.equal(probe.extractionCalls[0].principalOverride, "pi-geek");
250
+ assert.ok(probe.objectiveStateNamespaces.every((ns) => ns === expected));
251
+ } finally {
252
+ rmSync(repoDir, { recursive: true, force: true });
253
+ }
254
+ });
255
+
256
+ test("#1495 explicit namespace wins and project context does NOT silently override it", async () => {
257
+ const probe = makeObserveProbe({
258
+ namespacePolicies: [
259
+ { name: "team", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
260
+ ],
261
+ principalFromSessionKeyMode: "prefix",
262
+ principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
263
+ } as Partial<PluginConfig>);
264
+ const service = new EngramAccessService(probe.orch);
265
+
266
+ const res = await service.observe(
267
+ observeRequest({
268
+ sessionKey: "pi-geek:abc123",
269
+ namespace: "team",
270
+ projectTag: "Blend/Supply",
271
+ }),
272
+ );
273
+
274
+ assert.equal(res.effectiveNamespace, "team", "explicit namespace must win");
275
+ assert.equal(probe.lcmCalls[0].sessionKey, encodeNs("team", "pi-geek:abc123"));
276
+ assert.equal(probe.extractionCalls[0].writeNamespaceOverride, "team");
277
+ // #1505 thread 1: extraction turns carry the ORIGINAL session key (identity),
278
+ // even with an explicit namespace; routing is pinned via writeNamespaceOverride.
279
+ assert.deepEqual(
280
+ new Set(probe.extractionCalls[0].sessionKeys),
281
+ new Set(["pi-geek:abc123"]),
282
+ );
283
+ assert.equal(probe.extractionCalls[0].principalOverride, "pi-geek");
284
+ assert.ok(probe.objectiveStateNamespaces.every((ns) => ns === "team"));
285
+ });
286
+
287
+ test("#1495 projectScope:false ⇒ no overlay (unqualified write stays on config.defaultNamespace)", async () => {
288
+ // With projectScope off there is NO coding overlay, so an unqualified observe
289
+ // stays on config.defaultNamespace — exactly the pre-#1434 / memory_store
290
+ // behavior for an unqualified write (rule 39: identical across paths). It must
291
+ // NOT be silently moved to the principal self namespace. lcmSessionKey carries
292
+ // no prefix (effective == default).
293
+ const probe = makeObserveProbe({
294
+ ...withSelfPolicyPrefix("pi-geek"),
295
+ codingMode: { projectScope: false },
296
+ } as Partial<PluginConfig>);
297
+ const service = new EngramAccessService(probe.orch);
298
+
299
+ const res = await service.observe(
300
+ observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
301
+ );
302
+
303
+ assert.equal(res.effectiveNamespace, "default");
304
+ assert.equal(res.scopeDebug!.codingOverlayApplied, false);
305
+ assert.equal(probe.lcmCalls[0].sessionKey, "pi-geek:abc123");
306
+ // #1505 round 3 (codex "Pin default-store extraction writes too"): with
307
+ // namespaces ENABLED, extraction must be pinned to the resolved writeNamespace
308
+ // (config.defaultNamespace here) even though it equals the default store —
309
+ // otherwise an unpinned runExtraction would fall back to
310
+ // defaultNamespaceForPrincipal("pi-geek") == "pi-geek" (the SELF namespace),
311
+ // diverging from where LCM/objective-state/response wrote ("default"). Pinning
312
+ // forces every side effect onto the one scope-plan namespace.
313
+ assert.equal(probe.extractionCalls[0].writeNamespaceOverride, "default");
314
+ });
315
+
316
+ test("#1495 namespacesEnabled:false ⇒ single-store behavior preserved", async () => {
317
+ const probe = makeObserveProbe({ namespacesEnabled: false } as Partial<PluginConfig>);
318
+ const service = new EngramAccessService(probe.orch);
319
+
320
+ const res = await service.observe(
321
+ observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
322
+ );
323
+
324
+ assert.equal(res.effectiveNamespace, "default");
325
+ assert.equal(res.namespace, "default");
326
+ // No namespace prefix on the LCM key when the effective ns is the default.
327
+ assert.equal(probe.lcmCalls[0].sessionKey, "pi-geek:abc123");
328
+ // No override needed when there is only one store.
329
+ assert.equal(probe.extractionCalls[0].writeNamespaceOverride, undefined);
330
+ assert.deepEqual(
331
+ new Set(probe.extractionCalls[0].sessionKeys),
332
+ new Set(["pi-geek:abc123"]),
333
+ );
334
+ });
335
+
336
+ test("#1495 unauthorized explicit namespace throws BEFORE session context is attached", async () => {
337
+ const probe = makeObserveProbe();
338
+ const service = new EngramAccessService(probe.orch);
339
+
340
+ await assert.rejects(
341
+ service.observe(
342
+ observeRequest({
343
+ sessionKey: "pi-geek:abc123",
344
+ namespace: "victim-secret",
345
+ projectTag: "Blend/Supply",
346
+ }),
347
+ ),
348
+ /not writable/,
349
+ );
350
+
351
+ // No orphaned coding context, no side effects after the auth failure.
352
+ assert.equal(probe.contexts.get("pi-geek:abc123"), undefined);
353
+ assert.equal(probe.lcmCalls.length, 0);
354
+ assert.equal(probe.extractionCalls.length, 0);
355
+ assert.equal(probe.objectiveStateNamespaces.length, 0);
356
+ });
357
+
358
+ test("#1505 thread 1/3: unauthorized OVERLAY self-base throws BEFORE coding context is attached (no orphan context)", async () => {
359
+ // Threads 1 & 3 (cursor / codex): a project-scoped observe with NO explicit
360
+ // namespace. Step 1 (resolveWritableNamespace(undefined)) authorizes the
361
+ // DEFAULT namespace and PASSES. The overlay self-base auth only runs inside
362
+ // the scope plan. If the principal has a self namespace policy that EXISTS but
363
+ // is NOT writable, the scope plan throws — and before this fix that happened
364
+ // AFTER maybeAttachCodingContext mutated the session, leaving a project
365
+ // binding from a rejected op. The invariant: an observe that throws leaves NO
366
+ // coding context on the session, matching memory_store's resolve-before-mutate
367
+ // ordering.
368
+ const probe = makeObserveProbe({
369
+ namespacePolicies: [
370
+ // Self namespace exists (so defaultNamespaceForPrincipal → "pi-geek")
371
+ // but pi-geek may NOT write it — only some other principal can.
372
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["other"] },
373
+ ],
374
+ principalFromSessionKeyMode: "prefix",
375
+ principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
376
+ } as Partial<PluginConfig>);
377
+ const service = new EngramAccessService(probe.orch);
378
+
379
+ await assert.rejects(
380
+ service.observe(
381
+ observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
382
+ ),
383
+ /not writable/,
384
+ );
385
+
386
+ // No orphaned coding context, no side effects after the overlay auth failure.
387
+ assert.equal(
388
+ probe.contexts.get("pi-geek:abc123"),
389
+ undefined,
390
+ "a rejected observe must NOT leave a project binding on the session",
391
+ );
392
+ assert.equal(probe.lcmCalls.length, 0);
393
+ assert.equal(probe.extractionCalls.length, 0);
394
+ assert.equal(probe.objectiveStateNamespaces.length, 0);
395
+ });
396
+
397
+ test("#1505 thread jvO: restrictive default-namespace write policy does NOT reject a valid project-scoped observe via the legacy-response path (no orphan binding)", async () => {
398
+ // The legacy `namespace` response field was previously a SECOND
399
+ // `resolveWritableNamespace(request.namespace, …)` call. For an implicit
400
+ // (no explicit namespace) project-scoped observe that re-authorized
401
+ // `undefined ⇒ config.defaultNamespace`. Under a deployment that restricts
402
+ // WRITE to the default namespace while still allowing the principal to write
403
+ // its own self/project namespace, that second auth REJECTED an observe whose
404
+ // effective self/project write target the scope plan had ALREADY authorized
405
+ // (the same target memory_store/suggestion_submit accept). Worse, the scope
406
+ // plan had already SEEDED the coding context to compute the overlay, so the
407
+ // post-plan rejection left an orphaned project binding on the session.
408
+ //
409
+ // After the fix the legacy field is DERIVED from the resolved scope plan, so
410
+ // there is no second authorization: the observe succeeds, and the legacy
411
+ // `namespace` stays byte-for-byte `config.defaultNamespace` (overlay-agnostic
412
+ // pre-#1495 semantics) while every side effect uses the overlay write target.
413
+ const probe = makeObserveProbe({
414
+ namespacePolicies: [
415
+ // Restrictive DEFAULT namespace: only `admin` may write it, NOT pi-geek.
416
+ { name: "default", readPrincipals: ["admin"], writePrincipals: ["admin"] },
417
+ // pi-geek may write its own self (and thus its `pi-geek-project-*`) base.
418
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
419
+ ],
420
+ principalFromSessionKeyMode: "prefix",
421
+ principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
422
+ } as Partial<PluginConfig>);
423
+ const service = new EngramAccessService(probe.orch);
424
+
425
+ const expectedOverlay = combineNamespaces(
426
+ "pi-geek",
427
+ projectNamespaceName(projectTagProjectId("Blend/Supply")),
428
+ );
429
+
430
+ // FAIL-BEFORE: this threw `namespace is not writable: default`. PASS-AFTER:
431
+ // the observe is accepted exactly like memory_store/suggestion_submit would.
432
+ const res = await service.observe(
433
+ observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
434
+ );
435
+
436
+ // Every side effect uses the authorized overlay write target.
437
+ assert.equal(res.effectiveNamespace, expectedOverlay);
438
+ assert.equal(res.scopeDebug!.codingOverlayApplied, true);
439
+ assert.equal(probe.extractionCalls[0].writeNamespaceOverride, expectedOverlay);
440
+ // The legacy `namespace` field stays byte-for-byte pre-#1495: overlay-agnostic,
441
+ // so config.defaultNamespace for an unqualified write — NOT a re-auth result.
442
+ assert.equal(res.namespace, "default");
443
+ // The seeded coding context IS retained on success (the happy path re-binds
444
+ // the identical context after auth passes) — that is correct, not an orphan.
445
+ assert.ok(
446
+ probe.contexts.get("pi-geek:abc123"),
447
+ "a SUCCESSFUL scoped observe binds the project context for later recall",
448
+ );
449
+ });
450
+
451
+ test("#1505 thread jvO: a genuine reject (unwritable self base) under a restrictive default policy still leaves NO orphan binding", async () => {
452
+ // Companion to the jvO fix: when the observe SHOULD reject (the principal
453
+ // cannot write its own self base), the rejection must still come from the
454
+ // scope plan (resolve-before-mutate) and leave NO session binding — never
455
+ // from a post-plan legacy-response re-auth that fires after seeding.
456
+ const probe = makeObserveProbe({
457
+ namespacePolicies: [
458
+ { name: "default", readPrincipals: ["admin"], writePrincipals: ["admin"] },
459
+ // pi-geek's self base EXISTS but is NOT writable by pi-geek.
460
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["other"] },
461
+ ],
462
+ principalFromSessionKeyMode: "prefix",
463
+ principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
464
+ } as Partial<PluginConfig>);
465
+ const service = new EngramAccessService(probe.orch);
466
+
467
+ await assert.rejects(
468
+ service.observe(
469
+ observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
470
+ ),
471
+ /not writable/,
472
+ );
473
+
474
+ assert.equal(
475
+ probe.contexts.get("pi-geek:abc123"),
476
+ undefined,
477
+ "a rejected observe must NOT leave a project binding (resolve-before-mutate)",
478
+ );
479
+ assert.equal(probe.lcmCalls.length, 0);
480
+ assert.equal(probe.extractionCalls.length, 0);
481
+ assert.equal(probe.objectiveStateNamespaces.length, 0);
482
+ });
483
+
484
+ test("#1495 scopeDebug exposes the resolved plan for callers/tests", async () => {
485
+ const probe = makeObserveProbe(withSelfPolicyPrefix("pi-geek"));
486
+ const service = new EngramAccessService(probe.orch);
487
+
488
+ const res = await service.observe(
489
+ observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
490
+ );
491
+
492
+ const expected = combineNamespaces(
493
+ "pi-geek",
494
+ projectNamespaceName(projectTagProjectId("Blend/Supply")),
495
+ );
496
+ assert.ok(res.scopeDebug, "scopeDebug must be present");
497
+ assert.equal(res.scopeDebug!.principal, "pi-geek");
498
+ assert.equal(res.scopeDebug!.baseNamespace, "pi-geek");
499
+ assert.equal(res.scopeDebug!.writeNamespace, expected);
500
+ assert.equal(res.scopeDebug!.codingOverlayApplied, true);
501
+ });
502
+
503
+ test("#1505 (cursor hAp) scopeDebug.baseNamespace reports the principal self base on the implicit no-overlay path", async () => {
504
+ // Regression for the round-4 cursor "Wrong scopeDebug base namespace" thread.
505
+ // Implicit (no explicit namespace) + projectScope OFF ⇒ the no-overlay branch
506
+ // of resolveMemoryScopePlan runs: the general write namespace collapses to
507
+ // config.defaultNamespace ("default") for memory_store parity (rule 39), but
508
+ // the plan's diagnostic baseNamespace must report the principal SELF base
509
+ // ("pi-geek" via defaultNamespaceForPrincipal) — the same base
510
+ // objectiveStateNamespace already targets — NOT the write namespace. Before the
511
+ // fix, scopeDebug.baseNamespace misstated the self base as "default".
512
+ const probe = makeObserveProbe({
513
+ ...withSelfPolicyPrefix("pi-geek"),
514
+ codingMode: { projectScope: false },
515
+ } as Partial<PluginConfig>);
516
+ const service = new EngramAccessService(probe.orch);
517
+
518
+ const res = await service.observe(
519
+ observeRequest({ sessionKey: "pi-geek:abc123" }),
520
+ );
521
+
522
+ assert.ok(res.scopeDebug, "scopeDebug must be present");
523
+ assert.equal(
524
+ res.scopeDebug!.codingOverlayApplied,
525
+ false,
526
+ "no overlay on this path",
527
+ );
528
+ // Write/effective namespace collapses to the default store (memory_store parity)…
529
+ assert.equal(res.scopeDebug!.writeNamespace, "default");
530
+ assert.equal(res.effectiveNamespace, "default");
531
+ // …but the diagnostic base must be the principal SELF base, not the write ns.
532
+ assert.equal(
533
+ res.scopeDebug!.baseNamespace,
534
+ "pi-geek",
535
+ "scopeDebug.baseNamespace must be the principal self base on the implicit no-overlay path",
536
+ );
537
+ });
538
+
539
+ test("#1495 the scope plan's writeNamespace matches resolveCodingScopedWriteNamespace (memory_store / suggestion_submit parity, rule 39)", async () => {
540
+ // Regression guard: observe's effective scope MUST be identical to what the
541
+ // explicit-write tools (memory_store / suggestion_submit) resolve via
542
+ // resolveCodingScopedWriteNamespace. If these ever diverge, observed turns and
543
+ // explicit writes on the same session/project would land in different stores.
544
+ const probe = makeObserveProbe(withSelfPolicyPrefix("pi-geek"));
545
+ // Bind a session coding context so both resolvers see the same project.
546
+ probe.contexts.set("pi-geek:abc123", {
547
+ projectId: projectTagProjectId("Blend/Supply"),
548
+ branch: null,
549
+ rootPath: projectTagProjectId("Blend/Supply"),
550
+ defaultBranch: null,
551
+ });
552
+ const service = new EngramAccessService(probe.orch);
553
+
554
+ const internals = service as unknown as {
555
+ resolveMemoryScopePlan: (r: unknown) => Promise<{ writeNamespace: string }>;
556
+ resolveCodingScopedWriteNamespace: (r: unknown) => Promise<string>;
557
+ };
558
+
559
+ for (const req of [
560
+ { sessionKey: "pi-geek:abc123", authenticatedPrincipal: "pi-geek" },
561
+ {
562
+ sessionKey: "pi-geek:abc123",
563
+ authenticatedPrincipal: "pi-geek",
564
+ namespace: "pi-geek",
565
+ },
566
+ ]) {
567
+ const plan = await internals.resolveMemoryScopePlan.call(service, req);
568
+ const explicit = await internals.resolveCodingScopedWriteNamespace.call(
569
+ service,
570
+ req,
571
+ );
572
+ assert.equal(
573
+ plan.writeNamespace,
574
+ explicit,
575
+ `observe and explicit-write resolvers must agree for ${JSON.stringify(req)}`,
576
+ );
577
+ }
578
+ });
579
+
580
+ test("#1495 skipExtraction does not enqueue extraction but still archives LCM under the effective namespace", async () => {
581
+ const probe = makeObserveProbe(withSelfPolicyPrefix("pi-geek"));
582
+ const service = new EngramAccessService(probe.orch);
583
+
584
+ const res = await service.observe(
585
+ observeRequest({
586
+ sessionKey: "pi-geek:abc123",
587
+ projectTag: "Blend/Supply",
588
+ skipExtraction: true,
589
+ }),
590
+ );
591
+
592
+ const expected = combineNamespaces(
593
+ "pi-geek",
594
+ projectNamespaceName(projectTagProjectId("Blend/Supply")),
595
+ );
596
+ assert.equal(res.extractionQueued, false);
597
+ assert.equal(probe.extractionCalls.length, 0);
598
+ assert.equal(probe.lcmCalls[0].sessionKey, encodeNs(expected, "pi-geek:abc123"));
599
+ });