@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.
- package/dist/access-admin-ops-surface.d.ts +4 -4
- package/dist/access-admin-ops-surface.js +1 -1
- package/dist/access-authorization-probe.d.ts +4 -4
- package/dist/access-authorization-probe.js +2 -2
- package/dist/access-boundary.d.ts +4 -4
- package/dist/access-boundary.js +1 -1
- package/dist/access-cli.js +19 -18
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.d.ts +4 -4
- package/dist/access-extraction-force-flush.js +1 -1
- package/dist/access-http-lcm-compaction.d.ts +4 -4
- package/dist/access-http-lifecycle-flush.d.ts +4 -4
- package/dist/access-http-offline-stream.d.ts +4 -4
- package/dist/access-http-query.d.ts +13 -1
- package/dist/access-http-query.js +5 -2
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +12 -11
- package/dist/access-identity-continuity-surface.d.ts +3 -3
- package/dist/access-identity-continuity-surface.js +1 -1
- package/dist/access-lcm-surface.d.ts +4 -4
- package/dist/access-lcm-surface.js +1 -1
- package/dist/access-mcp.d.ts +4 -4
- package/dist/access-mcp.js +6 -6
- package/dist/access-namespace-preflight.js +1 -1
- package/dist/access-observe-write-surface.d.ts +4 -4
- package/dist/access-observe-write-surface.js +1 -1
- package/dist/access-offline-manifest.d.ts +3 -3
- package/dist/access-operations-batch.js +2 -2
- package/dist/access-operations.d.ts +7 -7
- package/dist/access-operations.js +5 -5
- package/dist/access-recall-concurrency.d.ts +4 -4
- package/dist/access-recall-concurrency.js +1 -1
- package/dist/access-recall-response.d.ts +4 -4
- package/dist/access-recall-response.js +1 -1
- package/dist/access-recall-surface.d.ts +4 -4
- package/dist/access-recall-surface.js +1 -1
- package/dist/access-schema.d.ts +88 -88
- package/dist/{access-service-M4LgO141.d.ts → access-service-D-bi3r01.d.ts} +66 -72
- package/dist/access-service-helpers.d.ts +4 -4
- package/dist/access-service.d.ts +4 -4
- package/dist/access-service.js +1 -1
- package/dist/access-surface-catalog.d.ts +4 -4
- package/dist/access-surface-catalog.js +3 -3
- package/dist/bootstrap.d.ts +3 -3
- package/dist/buffer.js +1 -1
- package/dist/{catalog-CyYTf_ok.d.ts → catalog-BYIHQw1l.d.ts} +2 -10
- package/dist/{chunk-JJ7JW62R.js → chunk-2JL7WPXL.js} +1 -11
- package/dist/chunk-2JL7WPXL.js.map +1 -0
- package/dist/{chunk-AJSZ2EK6.js → chunk-3USQWITL.js} +2 -2
- package/dist/{chunk-RYGJGXL6.js → chunk-4EDAHS6D.js} +2 -2
- package/dist/{chunk-5W5N7XJZ.js → chunk-A4HXWNST.js} +19 -752
- package/dist/chunk-A4HXWNST.js.map +1 -0
- package/dist/{chunk-KWN6NOHQ.js → chunk-BXOZALJ6.js} +2 -2
- package/dist/{chunk-6F2UMP4H.js → chunk-CKWOMJDY.js} +2 -2
- package/dist/{chunk-MR5HWQHS.js → chunk-DFEYRCG2.js} +2 -2
- package/dist/{chunk-6UZR7KEK.js → chunk-DQV3LUQL.js} +7 -1
- package/dist/chunk-DQV3LUQL.js.map +1 -0
- package/dist/{chunk-EJQXDKYK.js → chunk-EJCN5ATI.js} +3 -3
- package/dist/{chunk-TRPX6F2G.js → chunk-GNKY5JEP.js} +10 -13
- package/dist/chunk-GNKY5JEP.js.map +1 -0
- package/dist/{chunk-XZPSBGHA.js → chunk-ISFUFLFM.js} +2 -2
- package/dist/{chunk-4NMVX5U7.js → chunk-JSH2TV34.js} +2 -2
- package/dist/{chunk-R74NCKEY.js → chunk-LNFYISZ6.js} +2 -2
- package/dist/{chunk-6ZYQ36IB.js → chunk-MRJ43KSG.js} +15 -2
- package/dist/chunk-MRJ43KSG.js.map +1 -0
- package/dist/{chunk-IM56VPKO.js → chunk-OR7KEGLR.js} +2 -2
- package/dist/{chunk-A73QHOZP.js → chunk-QOMUE4FS.js} +2 -2
- package/dist/chunk-RSLRGRBJ.js +727 -0
- package/dist/chunk-RSLRGRBJ.js.map +1 -0
- package/dist/{chunk-44KHFMCM.js → chunk-VG6DC2GO.js} +32 -17
- package/dist/{chunk-44KHFMCM.js.map → chunk-VG6DC2GO.js.map} +1 -1
- package/dist/{chunk-5UFQCU6M.js → chunk-ZUZD2DRV.js} +5 -5
- package/dist/{cli-Bw9z-KqX.d.ts → cli-DEoABKXH.d.ts} +2 -2
- package/dist/cli.d.ts +5 -5
- package/dist/cli.js +15 -14
- package/dist/explicit-capture.d.ts +3 -3
- package/dist/external-wiki-access.d.ts +4 -4
- package/dist/external-wiki-access.js +2 -2
- package/dist/external-wiki-mcp-tools.d.ts +4 -4
- package/dist/index.d.ts +684 -684
- package/dist/index.js +21 -20
- package/dist/{maintenance-uJb74j64.d.ts → maintenance-N5ur13Al.d.ts} +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +4 -4
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/operator-toolkit.js +3 -2
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +3 -2
- package/dist/{orchestrator-CFi-5JSR.d.ts → orchestrator-ByLrpav4.d.ts} +2 -2
- package/dist/orchestrator.d.ts +3 -3
- package/dist/orchestrator.js +19 -18
- package/dist/{public-http-DK-QwaBu.d.ts → public-http-DusietP2.d.ts} +1 -1
- package/dist/schemas.d.ts +108 -108
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/support-passport/index.d.ts +7 -7
- package/dist/support-passport/index.js +4 -4
- package/dist/transfer/types.d.ts +66 -66
- package/package.json +2 -2
- package/src/access-http-query.ts +12 -0
- package/src/access-http.test.ts +32 -0
- package/src/access-http.ts +3 -7
- package/src/admin/admin-surfaces.test.ts +5 -10
- package/src/admin/admin-surfaces.ts +0 -16
- package/src/binary-lifecycle/pipeline.test.ts +1 -1
- package/src/binary-lifecycle/pipeline.ts +23 -9
- package/src/buffer.ts +6 -0
- package/src/maintenance/namespace-planner.ts +0 -2
- package/src/namespaces/catalog.test.ts +53 -8
- package/src/namespaces/catalog.ts +5 -37
- package/src/orchestration/turn-ingestion-snapshot.test.ts +167 -0
- package/src/orchestration/turn-ingestion.ts +2 -1
- package/dist/chunk-5W5N7XJZ.js.map +0 -1
- package/dist/chunk-6UZR7KEK.js.map +0 -1
- package/dist/chunk-6ZYQ36IB.js.map +0 -1
- package/dist/chunk-JJ7JW62R.js.map +0 -1
- package/dist/chunk-TRPX6F2G.js.map +0 -1
- /package/dist/{chunk-AJSZ2EK6.js.map → chunk-3USQWITL.js.map} +0 -0
- /package/dist/{chunk-RYGJGXL6.js.map → chunk-4EDAHS6D.js.map} +0 -0
- /package/dist/{chunk-KWN6NOHQ.js.map → chunk-BXOZALJ6.js.map} +0 -0
- /package/dist/{chunk-6F2UMP4H.js.map → chunk-CKWOMJDY.js.map} +0 -0
- /package/dist/{chunk-MR5HWQHS.js.map → chunk-DFEYRCG2.js.map} +0 -0
- /package/dist/{chunk-EJQXDKYK.js.map → chunk-EJCN5ATI.js.map} +0 -0
- /package/dist/{chunk-XZPSBGHA.js.map → chunk-ISFUFLFM.js.map} +0 -0
- /package/dist/{chunk-4NMVX5U7.js.map → chunk-JSH2TV34.js.map} +0 -0
- /package/dist/{chunk-R74NCKEY.js.map → chunk-LNFYISZ6.js.map} +0 -0
- /package/dist/{chunk-IM56VPKO.js.map → chunk-OR7KEGLR.js.map} +0 -0
- /package/dist/{chunk-A73QHOZP.js.map → chunk-QOMUE4FS.js.map} +0 -0
- /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
|
|
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: {
|
|
216
|
-
assert.ok(result.entries.
|
|
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"), "");
|
|
589
589
|
assert.equal(manifest.assets[0]?.status, "error");
|
|
590
|
-
assert.equal(
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
603
|
+
["bad-kind", "valid"],
|
|
604
|
+
"unknown kinds coerce to explicit; other malformed records stay quarantined",
|
|
605
605
|
);
|
|
606
|
-
assert.equal(list
|
|
607
|
-
|
|
608
|
-
assert.equal(
|
|
609
|
-
assert.equal(
|
|
610
|
-
assert.equal(
|
|
611
|
-
assert.
|
|
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 =
|
|
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
|
|
1403
|
-
//
|
|
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
|
|
206
|
+
outcome.extractionTurns,
|
|
206
207
|
"trigger_mode",
|
|
207
208
|
{ bufferKey },
|
|
208
209
|
);
|