@remnic/core 9.63.3 → 9.63.4

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 (128) hide show
  1. package/dist/access-admin-ops-surface.d.ts +4 -4
  2. package/dist/access-admin-ops-surface.js +1 -1
  3. package/dist/access-authorization-probe.d.ts +4 -4
  4. package/dist/access-authorization-probe.js +2 -2
  5. package/dist/access-boundary.d.ts +4 -4
  6. package/dist/access-boundary.js +1 -1
  7. package/dist/access-cli.js +19 -18
  8. package/dist/access-cli.js.map +1 -1
  9. package/dist/access-extraction-force-flush.d.ts +4 -4
  10. package/dist/access-extraction-force-flush.js +1 -1
  11. package/dist/access-http-lcm-compaction.d.ts +4 -4
  12. package/dist/access-http-lifecycle-flush.d.ts +4 -4
  13. package/dist/access-http-offline-stream.d.ts +4 -4
  14. package/dist/access-http-query.d.ts +13 -1
  15. package/dist/access-http-query.js +5 -2
  16. package/dist/access-http.d.ts +5 -5
  17. package/dist/access-http.js +12 -11
  18. package/dist/access-identity-continuity-surface.d.ts +3 -3
  19. package/dist/access-identity-continuity-surface.js +1 -1
  20. package/dist/access-lcm-surface.d.ts +4 -4
  21. package/dist/access-lcm-surface.js +1 -1
  22. package/dist/access-mcp.d.ts +4 -4
  23. package/dist/access-mcp.js +6 -6
  24. package/dist/access-namespace-preflight.js +1 -1
  25. package/dist/access-observe-write-surface.d.ts +4 -4
  26. package/dist/access-observe-write-surface.js +1 -1
  27. package/dist/access-offline-manifest.d.ts +3 -3
  28. package/dist/access-operations-batch.js +2 -2
  29. package/dist/access-operations.d.ts +7 -7
  30. package/dist/access-operations.js +5 -5
  31. package/dist/access-recall-concurrency.d.ts +4 -4
  32. package/dist/access-recall-concurrency.js +1 -1
  33. package/dist/access-recall-response.d.ts +4 -4
  34. package/dist/access-recall-response.js +1 -1
  35. package/dist/access-recall-surface.d.ts +4 -4
  36. package/dist/access-recall-surface.js +1 -1
  37. package/dist/access-schema.d.ts +88 -88
  38. package/dist/{access-service-M4LgO141.d.ts → access-service-D-bi3r01.d.ts} +66 -72
  39. package/dist/access-service-helpers.d.ts +4 -4
  40. package/dist/access-service.d.ts +4 -4
  41. package/dist/access-service.js +1 -1
  42. package/dist/access-surface-catalog.d.ts +4 -4
  43. package/dist/access-surface-catalog.js +3 -3
  44. package/dist/bootstrap.d.ts +3 -3
  45. package/dist/buffer.js +1 -1
  46. package/dist/{catalog-CyYTf_ok.d.ts → catalog-BYIHQw1l.d.ts} +2 -10
  47. package/dist/{chunk-JJ7JW62R.js → chunk-2JL7WPXL.js} +1 -11
  48. package/dist/chunk-2JL7WPXL.js.map +1 -0
  49. package/dist/{chunk-AJSZ2EK6.js → chunk-3USQWITL.js} +2 -2
  50. package/dist/{chunk-RYGJGXL6.js → chunk-4EDAHS6D.js} +2 -2
  51. package/dist/{chunk-5W5N7XJZ.js → chunk-A4HXWNST.js} +19 -752
  52. package/dist/chunk-A4HXWNST.js.map +1 -0
  53. package/dist/{chunk-KWN6NOHQ.js → chunk-BXOZALJ6.js} +2 -2
  54. package/dist/{chunk-6F2UMP4H.js → chunk-CKWOMJDY.js} +2 -2
  55. package/dist/{chunk-MR5HWQHS.js → chunk-DFEYRCG2.js} +2 -2
  56. package/dist/{chunk-6UZR7KEK.js → chunk-DQV3LUQL.js} +7 -1
  57. package/dist/chunk-DQV3LUQL.js.map +1 -0
  58. package/dist/{chunk-EJQXDKYK.js → chunk-EJCN5ATI.js} +3 -3
  59. package/dist/{chunk-TRPX6F2G.js → chunk-GNKY5JEP.js} +10 -13
  60. package/dist/chunk-GNKY5JEP.js.map +1 -0
  61. package/dist/{chunk-XZPSBGHA.js → chunk-ISFUFLFM.js} +2 -2
  62. package/dist/{chunk-4NMVX5U7.js → chunk-JSH2TV34.js} +2 -2
  63. package/dist/{chunk-R74NCKEY.js → chunk-LNFYISZ6.js} +2 -2
  64. package/dist/{chunk-6ZYQ36IB.js → chunk-MRJ43KSG.js} +15 -2
  65. package/dist/chunk-MRJ43KSG.js.map +1 -0
  66. package/dist/{chunk-IM56VPKO.js → chunk-OR7KEGLR.js} +2 -2
  67. package/dist/{chunk-A73QHOZP.js → chunk-QOMUE4FS.js} +2 -2
  68. package/dist/chunk-RSLRGRBJ.js +727 -0
  69. package/dist/chunk-RSLRGRBJ.js.map +1 -0
  70. package/dist/{chunk-44KHFMCM.js → chunk-VG6DC2GO.js} +32 -17
  71. package/dist/{chunk-44KHFMCM.js.map → chunk-VG6DC2GO.js.map} +1 -1
  72. package/dist/{chunk-5UFQCU6M.js → chunk-ZUZD2DRV.js} +5 -5
  73. package/dist/{cli-Bw9z-KqX.d.ts → cli-DEoABKXH.d.ts} +2 -2
  74. package/dist/cli.d.ts +5 -5
  75. package/dist/cli.js +15 -14
  76. package/dist/explicit-capture.d.ts +3 -3
  77. package/dist/external-wiki-access.d.ts +4 -4
  78. package/dist/external-wiki-access.js +2 -2
  79. package/dist/external-wiki-mcp-tools.d.ts +4 -4
  80. package/dist/index.d.ts +684 -684
  81. package/dist/index.js +21 -20
  82. package/dist/{maintenance-uJb74j64.d.ts → maintenance-N5ur13Al.d.ts} +1 -1
  83. package/dist/mcp-memory-inspector-app.d.ts +4 -4
  84. package/dist/namespaces/migrate.d.ts +1 -1
  85. package/dist/namespaces/storage.d.ts +1 -1
  86. package/dist/operator-toolkit.js +3 -2
  87. package/dist/orchestration/maintenance.d.ts +2 -2
  88. package/dist/orchestration/maintenance.js +3 -2
  89. package/dist/{orchestrator-CFi-5JSR.d.ts → orchestrator-ByLrpav4.d.ts} +2 -2
  90. package/dist/orchestrator.d.ts +3 -3
  91. package/dist/orchestrator.js +19 -18
  92. package/dist/{public-http-DK-QwaBu.d.ts → public-http-DusietP2.d.ts} +1 -1
  93. package/dist/schemas.d.ts +108 -108
  94. package/dist/shared-context/manager.d.ts +8 -8
  95. package/dist/support-passport/index.d.ts +7 -7
  96. package/dist/support-passport/index.js +4 -4
  97. package/dist/transfer/types.d.ts +66 -66
  98. package/package.json +2 -2
  99. package/src/access-http-query.ts +12 -0
  100. package/src/access-http.test.ts +32 -0
  101. package/src/access-http.ts +3 -7
  102. package/src/admin/admin-surfaces.test.ts +5 -10
  103. package/src/admin/admin-surfaces.ts +0 -16
  104. package/src/binary-lifecycle/pipeline.test.ts +1 -1
  105. package/src/binary-lifecycle/pipeline.ts +23 -9
  106. package/src/buffer.ts +6 -0
  107. package/src/maintenance/namespace-planner.ts +0 -2
  108. package/src/namespaces/catalog.test.ts +53 -8
  109. package/src/namespaces/catalog.ts +5 -37
  110. package/src/orchestration/turn-ingestion-snapshot.test.ts +167 -0
  111. package/src/orchestration/turn-ingestion.ts +2 -1
  112. package/dist/chunk-5W5N7XJZ.js.map +0 -1
  113. package/dist/chunk-6UZR7KEK.js.map +0 -1
  114. package/dist/chunk-6ZYQ36IB.js.map +0 -1
  115. package/dist/chunk-JJ7JW62R.js.map +0 -1
  116. package/dist/chunk-TRPX6F2G.js.map +0 -1
  117. /package/dist/{chunk-AJSZ2EK6.js.map → chunk-3USQWITL.js.map} +0 -0
  118. /package/dist/{chunk-RYGJGXL6.js.map → chunk-4EDAHS6D.js.map} +0 -0
  119. /package/dist/{chunk-KWN6NOHQ.js.map → chunk-BXOZALJ6.js.map} +0 -0
  120. /package/dist/{chunk-6F2UMP4H.js.map → chunk-CKWOMJDY.js.map} +0 -0
  121. /package/dist/{chunk-MR5HWQHS.js.map → chunk-DFEYRCG2.js.map} +0 -0
  122. /package/dist/{chunk-EJQXDKYK.js.map → chunk-EJCN5ATI.js.map} +0 -0
  123. /package/dist/{chunk-XZPSBGHA.js.map → chunk-ISFUFLFM.js.map} +0 -0
  124. /package/dist/{chunk-4NMVX5U7.js.map → chunk-JSH2TV34.js.map} +0 -0
  125. /package/dist/{chunk-R74NCKEY.js.map → chunk-LNFYISZ6.js.map} +0 -0
  126. /package/dist/{chunk-IM56VPKO.js.map → chunk-OR7KEGLR.js.map} +0 -0
  127. /package/dist/{chunk-A73QHOZP.js.map → chunk-QOMUE4FS.js.map} +0 -0
  128. /package/dist/{chunk-5UFQCU6M.js.map → chunk-ZUZD2DRV.js.map} +0 -0
@@ -200,20 +200,15 @@ test("listAdminNamespaces lists configured + discovered namespaces and marks sta
200
200
  }
201
201
  });
202
202
 
203
- test("listAdminNamespaces filters by principal", async () => {
203
+ test("listAdminNamespaces filters by kind", async () => {
204
204
  const memoryDir = await makeTempDir();
205
205
  try {
206
- const config = makeConfig({
207
- memoryDir,
208
- namespacePolicies: [
209
- { name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
210
- { name: "bob", readPrincipals: ["bob"], writePrincipals: ["bob"] },
211
- ],
212
- });
206
+ const config = makeConfig({ memoryDir });
213
207
  const catalog = new NamespaceCatalog(config);
214
208
  await catalog.registerConfiguredNamespaces();
215
- const result = await listAdminNamespaces({ catalog, filter: { principal: "alice" } });
216
- assert.ok(result.entries.every((entry) => entry.principal === "alice"));
209
+ const result = await listAdminNamespaces({ catalog, filter: { kind: "default" } });
210
+ assert.ok(result.entries.length > 0, "default kind filter must return the default namespace");
211
+ assert.ok(result.entries.every((entry) => entry.kind === "default"));
217
212
  } finally {
218
213
  await rm(memoryDir, { recursive: true, force: true });
219
214
  }
@@ -322,8 +322,6 @@ export function inspectScope(options: InspectScopeOptions): ScopeInspection {
322
322
  /** Filters the namespace browser accepts (superset of {@link NamespaceCatalogFilter}). */
323
323
  export interface AdminNamespaceFilter {
324
324
  kind?: NamespaceRecord["kind"];
325
- principal?: string;
326
- projectId?: string;
327
325
  discoveredBy?: NamespaceRecord["discoveredBy"];
328
326
  /** Only namespaces with a write at/after this instant. */
329
327
  writtenSince?: Date;
@@ -336,10 +334,6 @@ export interface AdminNamespaceEntry {
336
334
  namespace: string;
337
335
  identityToken: string;
338
336
  kind: NamespaceRecord["kind"];
339
- principal?: string;
340
- projectId?: string;
341
- branch?: string;
342
- parentNamespace?: string;
343
337
  createdAt: string;
344
338
  lastReadAt?: string;
345
339
  lastWriteAt?: string;
@@ -386,12 +380,6 @@ export async function listAdminNamespaces(options: ListAdminNamespacesOptions):
386
380
 
387
381
  let entries: AdminNamespaceEntry[] = records.map((record) => toAdminEntry(record, staleBefore));
388
382
 
389
- if (filter?.principal) {
390
- entries = entries.filter((entry) => entry.principal === filter.principal);
391
- }
392
- if (filter?.projectId) {
393
- entries = entries.filter((entry) => entry.projectId === filter.projectId);
394
- }
395
383
  if (filter?.staleBefore) {
396
384
  const cutoff = filter.staleBefore.getTime();
397
385
  entries = entries.filter((entry) => {
@@ -411,10 +399,6 @@ function toAdminEntry(record: NamespaceRecord, staleBefore: Date): AdminNamespac
411
399
  namespace: record.namespace,
412
400
  identityToken: record.identityToken,
413
401
  kind: record.kind,
414
- principal: record.principal,
415
- projectId: record.projectId,
416
- branch: record.branch,
417
- parentNamespace: record.parentNamespace,
418
402
  createdAt: record.createdAt,
419
403
  lastReadAt: record.lastReadAt,
420
404
  lastWriteAt: record.lastWriteAt,
@@ -587,7 +587,7 @@ test("binary lifecycle resumes redirects after verification read failures", asyn
587
587
  assert.match(firstResult.errors.join("\n"), /injected verification read failure/);
588
588
  assert.equal(await readFile(notePath, "utf8"), "![img](remote/image.png)");
589
589
  assert.equal(manifest.assets[0]?.status, "error");
590
- assert.equal(typeof manifest.assets[0]?.redirectedAt, "string");
590
+ assert.equal(manifest.assets[0]?.redirectedAt, undefined);
591
591
 
592
592
  const secondResult = await runBinaryLifecyclePipeline(memoryDir, baseConfig, nestedRemoteBackend, noopLogger);
593
593
  manifest = JSON.parse(
@@ -294,11 +294,25 @@ async function stageRedirect(
294
294
  }
295
295
 
296
296
  if (asset.redirectedAt === undefined) {
297
- if (!dryRun) {
298
- asset.status = "mirrored";
297
+ let remoteSeen = false;
298
+ for (const mdPath of mdFiles) {
299
+ try {
300
+ const content = await readMarkdownFile(mdPath);
301
+ if (content.includes(asset.mirroredPath)) {
302
+ remoteSeen = true;
303
+ break;
304
+ }
305
+ } catch {
306
+ // Verify already succeeded; a later read error is not a local ref.
307
+ }
308
+ }
309
+ if (!remoteSeen) {
310
+ if (!dryRun) {
311
+ asset.status = "mirrored";
312
+ }
313
+ log.info(`[binary-lifecycle] preserved mirrored asset without redirected marker: ${asset.originalPath}${dryRun ? " [dry-run]" : ""}`);
314
+ continue;
299
315
  }
300
- log.info(`[binary-lifecycle] preserved mirrored asset without redirected marker: ${asset.originalPath}${dryRun ? " [dry-run]" : ""}`);
301
- continue;
302
316
  }
303
317
 
304
318
  if (!Number.isFinite(new Date(asset.mirroredAt).getTime())) {
@@ -400,9 +414,6 @@ async function stageRedirect(
400
414
  continue;
401
415
  }
402
416
 
403
- const redirectedAt = new Date().toISOString();
404
- asset.redirectedAt = redirectedAt;
405
-
406
417
  const verifyResult = await countRemainingLocalReferences(
407
418
  memoryDir,
408
419
  asset,
@@ -411,7 +422,10 @@ async function stageRedirect(
411
422
  readMarkdownFile,
412
423
  );
413
424
  if (verifyResult.errors.length > 0 || verifyResult.remaining > 0) {
414
- asset.status = "error";
425
+ asset.status = "error";
426
+ // Failed verify must not read as a completed redirect (#2471); this also
427
+ // clears stale stamps written by manifests from before that fix.
428
+ delete asset.redirectedAt;
415
429
  for (const msg of verifyResult.errors) {
416
430
  log.error(`[binary-lifecycle] ${msg}`);
417
431
  errors.push(msg);
@@ -424,7 +438,7 @@ async function stageRedirect(
424
438
  continue;
425
439
  }
426
440
  asset.status = "redirected";
427
- asset.redirectedAt = redirectedAt;
441
+ asset.redirectedAt = new Date().toISOString();
428
442
  redirected++;
429
443
  log.info(`[binary-lifecycle] redirected: ${asset.originalPath}`);
430
444
  }
package/src/buffer.ts CHANGED
@@ -619,6 +619,12 @@ export class SmartBuffer {
619
619
  log.debug(
620
620
  `buffer[${bufferKey}]: ${mutation.turnCountInWindow} turns, signal=${mutation.signalLevel}, decision=${decision}`,
621
621
  );
622
+ if (decision !== "keep_buffering" && extractionTurns === undefined) {
623
+ extractionTurns = [
624
+ ...mutation.retainedTurnsSnapshot,
625
+ ...mutation.activeTurnsSnapshot,
626
+ ];
627
+ }
622
628
  return extractionTurns ? { decision, extractionTurns } : { decision };
623
629
  }
624
630
 
@@ -177,8 +177,6 @@ function candidatePriority(candidate: NamespaceMaintenanceCandidate): number {
177
177
  if (candidate.source === "configured") return 2;
178
178
  if (candidate.kind === "team-project") return 3;
179
179
  if (candidate.kind === "project") return 4;
180
- if (candidate.kind === "self") return 5;
181
- if (candidate.kind === "legacy") return 6;
182
180
  if (candidate.kind === "branch") return 8;
183
181
  return 7;
184
182
  }
@@ -600,15 +600,16 @@ test("catalog quarantines malformed JSONL record fields at the parse boundary",
600
600
 
601
601
  assert.deepEqual(
602
602
  list.map((r) => r.namespace),
603
- ["valid"],
604
- "invalid enum/token/timestamp records must not surface",
603
+ ["bad-kind", "valid"],
604
+ "unknown kinds coerce to explicit; other malformed records stay quarantined",
605
605
  );
606
- assert.equal(list[0]?.identityToken, validToken);
607
- assert.equal(list[0]?.kind, "project");
608
- assert.equal(list[0]?.discoveredBy, "write");
609
- assert.equal(list[0]?.lastReadAt, undefined);
610
- assert.equal(list[0]?.lastWriteAt, undefined);
611
- assert.deepEqual(list[0]?.lastMaintenanceAt, { ok: now });
606
+ assert.equal(list.find((r) => r.namespace === "bad-kind")?.kind, "explicit");
607
+ const valid = list.find((r) => r.namespace === "valid");
608
+ assert.equal(valid?.kind, "project");
609
+ assert.equal(valid?.discoveredBy, "write");
610
+ assert.equal(valid?.lastReadAt, undefined);
611
+ assert.equal(valid?.lastWriteAt, undefined);
612
+ assert.deepEqual(valid?.lastMaintenanceAt, { ok: now });
612
613
  } finally {
613
614
  await rm(memoryDir, { recursive: true, force: true });
614
615
  }
@@ -3952,3 +3953,47 @@ test("#1903 cross-instance compaction never drops another instance's rows", asyn
3952
3953
  await rm(memoryDir, { recursive: true, force: true });
3953
3954
  }
3954
3955
  });
3956
+
3957
+ test("#2469 persisted self/legacy kinds coerce to explicit and drop unused hints", async () => {
3958
+ const memoryDir = await mkMemoryDir();
3959
+ try {
3960
+ const ns = "project-origin-dead-kind";
3961
+ const token = namespaceIdentityToken(ns);
3962
+ const stateDir = path.join(memoryDir, "state");
3963
+ await mkdir(stateDir, { recursive: true });
3964
+ const line = JSON.stringify({
3965
+ namespace: ns,
3966
+ identityToken: token,
3967
+ kind: "self",
3968
+ principal: "unused",
3969
+ projectId: "unused",
3970
+ branch: "unused",
3971
+ parentNamespace: "unused",
3972
+ createdAt: "2026-08-17T00:00:00.000Z",
3973
+ storageDir: path.join(memoryDir, "namespaces", token),
3974
+ discoveredBy: "write",
3975
+ });
3976
+ const legacyLine = JSON.stringify({
3977
+ namespace: "shared",
3978
+ identityToken: namespaceIdentityToken("shared"),
3979
+ kind: "legacy",
3980
+ createdAt: "2026-08-17T00:00:00.000Z",
3981
+ storageDir: path.join(memoryDir, "namespaces", namespaceIdentityToken("shared")),
3982
+ discoveredBy: "config",
3983
+ });
3984
+ await writeFile(path.join(stateDir, "namespaces.jsonl"), `${line}\n${legacyLine}\n`, "utf8");
3985
+
3986
+ const catalog = new NamespaceCatalog(makeConfig(memoryDir));
3987
+ const selfRecord = await catalog.getNamespaceRecord(ns);
3988
+ assert.equal(selfRecord?.kind, "explicit");
3989
+ assert.equal(
3990
+ (selfRecord as { principal?: string } | null)?.principal,
3991
+ undefined,
3992
+ );
3993
+ const legacyRecord = await catalog.getNamespaceRecord("shared");
3994
+ assert.equal(legacyRecord?.kind, "explicit");
3995
+ } finally {
3996
+ await rm(memoryDir, { recursive: true, force: true });
3997
+ }
3998
+ });
3999
+
@@ -59,13 +59,11 @@ import { ALL_CATEGORY_DIRS } from "../utils/category-dir.js";
59
59
 
60
60
  export type NamespaceKind =
61
61
  | "default"
62
- | "self"
63
62
  | "shared"
64
63
  | "project"
65
64
  | "branch"
66
65
  | "team-project"
67
- | "explicit"
68
- | "legacy";
66
+ | "explicit";
69
67
 
70
68
  export type NamespaceDiscoverySource = "config" | "write" | "read" | "scan" | "migration";
71
69
 
@@ -73,10 +71,6 @@ export interface NamespaceRecord {
73
71
  namespace: string;
74
72
  identityToken: string;
75
73
  kind: NamespaceKind;
76
- principal?: string;
77
- projectId?: string;
78
- branch?: string;
79
- parentNamespace?: string;
80
74
  createdAt: string;
81
75
  lastReadAt?: string;
82
76
  lastWriteAt?: string;
@@ -95,10 +89,6 @@ export interface NamespaceCatalogFilter {
95
89
  export interface NamespaceTouchMetadata {
96
90
  discoveredBy?: NamespaceDiscoverySource;
97
91
  kind?: NamespaceKind;
98
- principal?: string;
99
- projectId?: string;
100
- branch?: string;
101
- parentNamespace?: string;
102
92
  /** Explicit storage dir (when the caller already resolved it). */
103
93
  storageDir?: string;
104
94
  /** Override the touch timestamp (mainly for tests / migration replay). */
@@ -129,13 +119,11 @@ export interface NamespaceCatalogRebuildResult {
129
119
 
130
120
  const NAMESPACE_KINDS: readonly NamespaceKind[] = [
131
121
  "default",
132
- "self",
133
122
  "shared",
134
123
  "project",
135
124
  "branch",
136
125
  "team-project",
137
126
  "explicit",
138
- "legacy",
139
127
  ];
140
128
 
141
129
  const NAMESPACE_DISCOVERY_SOURCES: readonly NamespaceDiscoverySource[] = [
@@ -275,7 +263,7 @@ function isValidIsoTimestamp(value: string): boolean {
275
263
  return Number.isFinite(ms);
276
264
  }
277
265
 
278
- function isNamespaceKind(value: unknown): value is NamespaceKind {
266
+ export function isNamespaceKind(value: unknown): value is NamespaceKind {
279
267
  return typeof value === "string" && (NAMESPACE_KINDS as readonly string[]).includes(value);
280
268
  }
281
269
 
@@ -301,8 +289,7 @@ function coerceRecord(value: unknown): NamespaceRecord | null {
301
289
  if (typeof v.storageDir !== "string" || v.storageDir.length === 0) return null;
302
290
  if (typeof v.createdAt !== "string" || v.createdAt.length === 0) return null;
303
291
  if (!isValidIsoTimestamp(v.createdAt)) return null;
304
- const kind = v.kind === undefined ? "explicit" : isNamespaceKind(v.kind) ? v.kind : null;
305
- if (!kind) return null;
292
+ const kind = isNamespaceKind(v.kind) ? v.kind : "explicit";
306
293
  const discoveredBy =
307
294
  v.discoveredBy === undefined
308
295
  ? "scan"
@@ -318,10 +305,6 @@ function coerceRecord(value: unknown): NamespaceRecord | null {
318
305
  storageDir: v.storageDir,
319
306
  discoveredBy,
320
307
  };
321
- if (typeof v.principal === "string") record.principal = v.principal;
322
- if (typeof v.projectId === "string") record.projectId = v.projectId;
323
- if (typeof v.branch === "string") record.branch = v.branch;
324
- if (typeof v.parentNamespace === "string") record.parentNamespace = v.parentNamespace;
325
308
  if (typeof v.lastReadAt === "string" && isValidIsoTimestamp(v.lastReadAt)) {
326
309
  record.lastReadAt = v.lastReadAt;
327
310
  }
@@ -792,12 +775,6 @@ export class NamespaceCatalog {
792
775
  if (kind === "read" && cached.lastReadAt === undefined) return true;
793
776
  if (!metadata) return false;
794
777
  if (metadata.kind !== undefined && metadata.kind !== cached.kind) return true;
795
- if (metadata.principal !== undefined && metadata.principal !== cached.principal) return true;
796
- if (metadata.projectId !== undefined && metadata.projectId !== cached.projectId) return true;
797
- if (metadata.branch !== undefined && metadata.branch !== cached.branch) return true;
798
- if (metadata.parentNamespace !== undefined && metadata.parentNamespace !== cached.parentNamespace) {
799
- return true;
800
- }
801
778
  // A changed storageDir repoints routing/containment for this namespace —
802
779
  // must be observable at once, not held stale by the coalesce window (#1903, Cursor).
803
780
  if (metadata.storageDir !== undefined && metadata.storageDir !== cached.storageDir) return true;
@@ -1399,15 +1376,10 @@ export class NamespaceCatalog {
1399
1376
  (kind === "register" ? "config" : kind === "maintenance" ? "scan" : kind),
1400
1377
  };
1401
1378
 
1402
- // Update mutable fields. storageDir, kind, and the principal/project hints
1403
- // may legitimately change over a namespace's lifetime, so they upsert.
1379
+ // Update mutable fields. storageDir and kind may change over a
1380
+ // namespace's lifetime, so they upsert.
1404
1381
  record.storageDir = storageDir;
1405
1382
  if (metadata?.kind) record.kind = metadata.kind;
1406
- if (metadata?.principal !== undefined) record.principal = metadata.principal;
1407
- if (metadata?.projectId !== undefined) record.projectId = metadata.projectId;
1408
- if (metadata?.branch !== undefined) record.branch = metadata.branch;
1409
- if (metadata?.parentNamespace !== undefined)
1410
- record.parentNamespace = metadata.parentNamespace;
1411
1383
  // PROVENANCE (creation-only, with one upgrade — round 6, codex P2 NBPmT):
1412
1384
  // `discoveredBy` is otherwise preserved for existing records (a routine
1413
1385
  // read/register/resolve never relabels it). The single exception is a real
@@ -2109,10 +2081,6 @@ export class NamespaceCatalog {
2109
2081
  if (prior.lastReadAt) merged.lastReadAt = prior.lastReadAt;
2110
2082
  if (prior.lastWriteAt) merged.lastWriteAt = prior.lastWriteAt;
2111
2083
  if (prior.lastMaintenanceAt) merged.lastMaintenanceAt = { ...prior.lastMaintenanceAt };
2112
- if (prior.principal !== undefined) merged.principal = prior.principal;
2113
- if (prior.projectId !== undefined) merged.projectId = prior.projectId;
2114
- if (prior.branch !== undefined) merged.branch = prior.branch;
2115
- if (prior.parentNamespace !== undefined) merged.parentNamespace = prior.parentNamespace;
2116
2084
  return merged;
2117
2085
  }
2118
2086
  // ── Persistence ──────────────────────────────────────────────────────────
@@ -0,0 +1,167 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { SmartBuffer } from "../buffer.js";
4
+ import { parseConfig } from "../config.js";
5
+ import type { BufferState, BufferTurn } from "../types.js";
6
+ import {
7
+ TurnIngestionCoordinator,
8
+ type TurnIngestionDeps,
9
+ } from "./turn-ingestion.js";
10
+
11
+ class FakeStorage {
12
+ constructor(private readonly initial: BufferState) {}
13
+ async loadBuffer(): Promise<BufferState> {
14
+ return structuredClone(this.initial);
15
+ }
16
+ async saveBuffer(_state: BufferState): Promise<void> {}
17
+ }
18
+
19
+ function makeTurn(content: string): BufferTurn {
20
+ return {
21
+ role: "user",
22
+ content,
23
+ timestamp: "2026-08-17T00:00:00.000Z",
24
+ sessionKey: "k",
25
+ };
26
+ }
27
+
28
+ test("#2468 extract trigger returns the mutation snapshot", async () => {
29
+ const buffer = new SmartBuffer(
30
+ parseConfig({ triggerMode: "every_n", bufferMaxTurns: 1 }),
31
+ new FakeStorage({ turns: [], lastExtractionAt: null, extractionCount: 0 }) as never,
32
+ );
33
+ const outcome = await buffer.addTurnWithOutcome("k", makeTurn("only"));
34
+ assert.equal(outcome.decision, "extract_batch");
35
+ assert.equal(outcome.extractionTurns?.length, 1);
36
+ assert.equal(outcome.extractionTurns?.[0]?.content, "only");
37
+ });
38
+
39
+ test("#2468 built-in trigger includes retained turns in the snapshot", async () => {
40
+ const buffer = new SmartBuffer(
41
+ parseConfig({ triggerMode: "every_n", bufferMaxTurns: 1 }),
42
+ new FakeStorage({ turns: [], lastExtractionAt: null, extractionCount: 0 }) as never,
43
+ );
44
+ await buffer.retainDeferredTurns("k", [makeTurn("retained")], 10);
45
+ const outcome = await buffer.addTurnWithOutcome("k", makeTurn("live"));
46
+ assert.equal(outcome.decision, "extract_batch");
47
+ assert.deepEqual(
48
+ outcome.extractionTurns?.map((turn) => turn.content),
49
+ ["retained", "live"],
50
+ );
51
+ });
52
+
53
+ test("#2468 processTurn does not adopt a later getTurns() batch", async () => {
54
+ const snapshot = [makeTurn("first")];
55
+ const later = [makeTurn("first"), makeTurn("later")];
56
+ const queued: BufferTurn[][] = [];
57
+ const buffer = {
58
+ addTurnWithOutcome: async () => ({
59
+ decision: "extract_now" as const,
60
+ extractionTurns: snapshot,
61
+ }),
62
+ getTurns: () => later,
63
+ };
64
+ const deps = {
65
+ buffer,
66
+ config: parseConfig({}),
67
+ getStorage: async () => {
68
+ throw new Error("unexpected getStorage");
69
+ },
70
+ bulkImportWriteNamespace: () => "default",
71
+ get extractionQueueCoordinator(): never {
72
+ throw new Error("unexpected extractionQueueCoordinator");
73
+ },
74
+ heartbeatObserverChains: new Map(),
75
+ lcmEngine: null,
76
+ passiveCorrectionDedup: new Set(),
77
+ passiveCorrectionService: () => {
78
+ throw new Error("unexpected passiveCorrectionService");
79
+ },
80
+ passiveCorrectionTelemetry: {
81
+ detected: 0,
82
+ queued: 0,
83
+ autoApplied: 0,
84
+ suppressedReasonCounts: {},
85
+ },
86
+ queueBufferedExtraction: async (turns: BufferTurn[]) => {
87
+ queued.push(turns);
88
+ },
89
+ resolveMemoryIdOrHandle: () => {
90
+ throw new Error("unexpected resolveMemoryIdOrHandle");
91
+ },
92
+ runExtraction: async () => {
93
+ throw new Error("unexpected runExtraction");
94
+ },
95
+ get sessionObserver(): never {
96
+ throw new Error("unexpected sessionObserver");
97
+ },
98
+ shouldQueueExtraction: () => {
99
+ throw new Error("unexpected shouldQueueExtraction");
100
+ },
101
+ get transcript(): never {
102
+ throw new Error("unexpected transcript");
103
+ },
104
+ } as unknown as TurnIngestionDeps;
105
+ await new TurnIngestionCoordinator(deps).processTurn("user", "first", "k");
106
+ assert.equal(queued.length, 1);
107
+ assert.equal(queued[0]?.length, 1);
108
+ assert.equal(queued[0]?.[0]?.content, "first");
109
+ });
110
+ test("#2468 concurrent processTurn first trigger keeps its mutation snapshot", async () => {
111
+ const queued: BufferTurn[][] = [];
112
+ const buffer = new SmartBuffer(
113
+ parseConfig({ triggerMode: "every_n", bufferMaxTurns: 1, bufferSaveDebounceMs: 0 }),
114
+ new FakeStorage({ turns: [], lastExtractionAt: null, extractionCount: 0 }) as never,
115
+ );
116
+ const deps = {
117
+ buffer,
118
+ config: parseConfig({ triggerMode: "every_n", bufferMaxTurns: 1 }),
119
+ getStorage: async () => {
120
+ throw new Error("unexpected getStorage");
121
+ },
122
+ bulkImportWriteNamespace: () => "default",
123
+ get extractionQueueCoordinator(): never {
124
+ throw new Error("unexpected extractionQueueCoordinator");
125
+ },
126
+ heartbeatObserverChains: new Map(),
127
+ lcmEngine: null,
128
+ passiveCorrectionDedup: new Set(),
129
+ passiveCorrectionService: () => {
130
+ throw new Error("unexpected passiveCorrectionService");
131
+ },
132
+ passiveCorrectionTelemetry: {
133
+ detected: 0,
134
+ queued: 0,
135
+ autoApplied: 0,
136
+ suppressedReasonCounts: {},
137
+ },
138
+ queueBufferedExtraction: async (turns: BufferTurn[]) => {
139
+ queued.push(turns);
140
+ },
141
+ resolveMemoryIdOrHandle: () => {
142
+ throw new Error("unexpected resolveMemoryIdOrHandle");
143
+ },
144
+ runExtraction: async () => {
145
+ throw new Error("unexpected runExtraction");
146
+ },
147
+ get sessionObserver(): never {
148
+ throw new Error("unexpected sessionObserver");
149
+ },
150
+ shouldQueueExtraction: () => {
151
+ throw new Error("unexpected shouldQueueExtraction");
152
+ },
153
+ get transcript(): never {
154
+ throw new Error("unexpected transcript");
155
+ },
156
+ } as unknown as TurnIngestionDeps;
157
+ const coordinator = new TurnIngestionCoordinator(deps);
158
+ await Promise.all([
159
+ coordinator.processTurn("user", "alpha", "shared-key"),
160
+ coordinator.processTurn("user", "beta", "shared-key"),
161
+ ]);
162
+ assert.equal(queued.length, 2);
163
+ assert.ok(
164
+ queued.some((batch) => batch.length === 1),
165
+ "the first trigger must extract only its mutation snapshot",
166
+ );
167
+ });
@@ -201,8 +201,9 @@ export class TurnIngestionCoordinator {
201
201
  : { decision: await this.deps.buffer.addTurn(bufferKey, turn) };
202
202
 
203
203
  if (outcome.decision === "keep_buffering") return;
204
+ if (!outcome.extractionTurns) return;
204
205
  await this.deps.queueBufferedExtraction(
205
- outcome.extractionTurns ?? this.deps.buffer.getTurns(bufferKey),
206
+ outcome.extractionTurns,
206
207
  "trigger_mode",
207
208
  { bufferKey },
208
209
  );