@remnic/core 10.0.53 → 10.0.54

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/index.js CHANGED
@@ -181,7 +181,7 @@ import {
181
181
  writeManifest,
182
182
  writeMeetingEpisodeMemory,
183
183
  writeTimelineState
184
- } from "./chunk-CA6X37OB.js";
184
+ } from "./chunk-2GU6XS4L.js";
185
185
  import {
186
186
  WEARABLE_SOURCE_PREFIX,
187
187
  buildExtractionTurns,
@@ -247,7 +247,7 @@ import {
247
247
  timelineAnalysisClientsFromConfig,
248
248
  timelineCorrectionDatabasePath,
249
249
  timelineDayPath
250
- } from "./chunk-H4DWSOBQ.js";
250
+ } from "./chunk-PVG7TCUG.js";
251
251
  import "./chunk-I74SUMNI.js";
252
252
  import {
253
253
  PEERS_DIR_NAME,
@@ -130,7 +130,7 @@ export declare class MaintenanceScheduler {
130
130
  * unavailable or maintenance off), the pass failed, or dispose() abandoned it,
131
131
  * so a caller that must not lose the re-index keeps owing it.
132
132
  */
133
- requestQmdMaintenanceCompletion(): Promise<boolean>;
133
+ requestQmdMaintenanceCompletion(namespace?: string): Promise<boolean>;
134
134
  /**
135
135
  * Public entrypoint for tool-driven QMD maintenance requests.
136
136
  * Routes through existing debounced/singleflight maintenance controls.
@@ -1 +1 @@
1
- {"version":3,"file":"maintenance.d.ts","sourceRoot":"","sources":["../../src/orchestration/maintenance.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"maintenance.d.ts","sourceRoot":"","sources":["../../src/orchestration/maintenance.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MaintenanceScheduler
3
- } from "../chunk-H4DWSOBQ.js";
3
+ } from "../chunk-PVG7TCUG.js";
4
4
  import "../chunk-666A3MOW.js";
5
5
  import "../chunk-B43P3DTC.js";
6
6
  import "../chunk-6CDNOREO.js";
@@ -7,13 +7,18 @@
7
7
  */
8
8
  export declare class QmdMaintenanceCompletion {
9
9
  private waiters;
10
- /** Resolves true when a pass that started after this call completed successfully. */
11
- wait(): Promise<boolean>;
10
+ /**
11
+ * Resolves true when a pass that started after this call completed successfully. With a
12
+ * `namespace` the request is served as soon as a pass ran that namespace, whatever else it skipped.
13
+ */
14
+ wait(namespace?: string): Promise<boolean>;
12
15
  /**
13
16
  * A pass is starting: take every waiter it will serve and return their settle function.
14
- * `defer` keeps them waiting for a later pass (a pass that covered only part of the selection).
17
+ * `ran` lists the namespaces a namespace-aware pass ran (absent when namespaces are disabled:
18
+ * one corpus is maintained and the pass outcome applies to every waiter); `defer` keeps the
19
+ * waiters it did not serve waiting for a later pass (a pass that covered only part of the selection).
15
20
  */
16
- claim(): (served: boolean, defer?: boolean) => void;
21
+ claim(): (served: boolean, defer?: boolean, ran?: ReadonlySet<string>) => void;
17
22
  /** The scheduler stopped: nothing will serve the waiters. */
18
23
  abandon(): void;
19
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"qmd-maintenance-completion.d.ts","sourceRoot":"","sources":["../../src/orchestration/qmd-maintenance-completion.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,qBAAa,wBAAwB;IACnC,OAAO,CAAC,OAAO,CAAwC;IAEvD,qFAAqF;IACrF,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC;IAIxB;;;OAGG;IACH,KAAK,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI;IAQnD,6DAA6D;IAC7D,OAAO,IAAI,IAAI;CAGhB"}
1
+ {"version":3,"file":"qmd-maintenance-completion.d.ts","sourceRoot":"","sources":["../../src/orchestration/qmd-maintenance-completion.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,qBAAa,wBAAwB;IACnC,OAAO,CAAC,OAAO,CAAyE;IAExF;;;OAGG;IACH,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI1C;;;;;OAKG;IACH,KAAK,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,KAAK,IAAI;IAe9E,6DAA6D;IAC7D,OAAO,IAAI,IAAI;CAGhB"}
@@ -4,13 +4,13 @@ import {
4
4
  graphPathRelativeToStorage,
5
5
  mergeGraphExpandedResults,
6
6
  qmdStartupCollectionChecksWithTimeout
7
- } from "./chunk-CA6X37OB.js";
7
+ } from "./chunk-2GU6XS4L.js";
8
8
  import "./chunk-YDIOMDBW.js";
9
9
  import "./chunk-HM3ZTDAX.js";
10
10
  import {
11
11
  formatCompressionGuidelinesForRecall
12
12
  } from "./chunk-5PHOU5QI.js";
13
- import "./chunk-H4DWSOBQ.js";
13
+ import "./chunk-PVG7TCUG.js";
14
14
  import "./chunk-I74SUMNI.js";
15
15
  import "./chunk-ILNPU5W5.js";
16
16
  import "./chunk-VZAKYDMJ.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "10.0.53",
3
+ "version": "10.0.54",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3346,7 +3346,7 @@
3346
3346
  "core"
3347
3347
  ],
3348
3348
  "peerDependencies": {
3349
- "@remnic/coding-graph": "^10.0.53"
3349
+ "@remnic/coding-graph": "^10.0.54"
3350
3350
  },
3351
3351
  "peerDependenciesMeta": {
3352
3352
  "@remnic/coding-graph": {
@@ -3077,6 +3077,66 @@ test("requestQmdMaintenanceCompletion resolves false when dispose() clears the a
3077
3077
  assert.equal(await served, false, "a destroy inside the debounce window must leave the re-index owed");
3078
3078
  });
3079
3079
 
3080
+ test("requestQmdMaintenanceCompletion over namespaces: a scoped request is served only when its own namespace ran, an unscoped one by the pass outcome", async () => {
3081
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-completion-permanent-"));
3082
+ const config = fixtureConfig({
3083
+ memoryDir,
3084
+ namespacesEnabled: true,
3085
+ defaultNamespace: "default",
3086
+ sharedNamespace: "shared",
3087
+ namespacePolicies: [],
3088
+ maintenanceNamespaceLockStaleMs: 100,
3089
+ qmdAutoEmbedEnabled: false,
3090
+ });
3091
+ const catalog: NamespaceCatalog = Object.assign(Object.create(null), {
3092
+ enabled: true,
3093
+ async listNamespaces() {
3094
+ return [{ namespace: "feature-x", kind: "branch" }];
3095
+ },
3096
+ async markMaintenance() {},
3097
+ });
3098
+ const router: NamespaceSearchRouter = Object.assign(Object.create(null), {
3099
+ async updateNamespacesDetailed(ns: string[]): Promise<NamespaceUpdateResult> {
3100
+ return { backendCount: ns.length, eligibleNamespaces: ns };
3101
+ },
3102
+ async embedNamespaces(): Promise<void> {},
3103
+ });
3104
+ const scheduler = buildScheduler({ config, catalog, router });
3105
+ try {
3106
+ // feature-x is skipped as branch_disabled (never maintained): the pass is served for the
3107
+ // namespaces that ran, but nothing re-indexed feature-x.
3108
+ const unscoped = scheduler.requestQmdMaintenanceCompletion();
3109
+ const forBranch = scheduler.requestQmdMaintenanceCompletion("feature-x");
3110
+ const forDefault = scheduler.requestQmdMaintenanceCompletion("default");
3111
+ await scheduler.runQmdMaintenance();
3112
+ assert.equal(await unscoped, true);
3113
+ assert.equal(await forDefault, true, "default ran");
3114
+ assert.equal(await forBranch, false, "feature-x never ran, so its re-index is still owed");
3115
+ } finally {
3116
+ await scheduler.dispose();
3117
+ await rm(memoryDir, { recursive: true, force: true });
3118
+ }
3119
+ });
3120
+
3121
+ test("requestQmdMaintenanceCompletion serves a scoped request when namespaces are disabled and the update ran", async () => {
3122
+ // Without namespaces there is one corpus; the pass has no per-namespace statuses to match.
3123
+ const scheduler = completionScheduler({});
3124
+ try {
3125
+ const served = scheduler.requestQmdMaintenanceCompletion("default");
3126
+ await scheduler.runQmdMaintenance();
3127
+ assert.equal(await served, true);
3128
+ } finally {
3129
+ await scheduler.dispose();
3130
+ }
3131
+ });
3132
+
3133
+ test("requestQmdMaintenanceCompletion resolves false for a namespace-scoped request when dispose() abandons it", async () => {
3134
+ const scheduler = completionScheduler({});
3135
+ const served = scheduler.requestQmdMaintenanceCompletion("default");
3136
+ await scheduler.dispose();
3137
+ assert.equal(await served, false, "no pass ran the namespace, so the re-index stays owed");
3138
+ });
3139
+
3080
3140
  test("requestQmdMaintenanceCompletion resolves false without arming anything when QMD is unavailable or disposed", async () => {
3081
3141
  const unavailable = completionScheduler({ available: false });
3082
3142
  try {
@@ -3300,6 +3360,43 @@ test("requestQmdMaintenanceCompletion over namespaces: true only when a namespac
3300
3360
  }
3301
3361
  });
3302
3362
 
3363
+ test("requestQmdMaintenanceCompletion over namespaces: a failed catalog read leaves the request owed (#3289)", async () => {
3364
+ // The configured namespaces still run, but cataloged (dynamic) namespaces were never discovered,
3365
+ // so the re-index is incomplete and the migration must keep its marker.
3366
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-completion-catalog-"));
3367
+ const config = fixtureConfig({
3368
+ memoryDir,
3369
+ namespacesEnabled: true,
3370
+ defaultNamespace: "default",
3371
+ sharedNamespace: "shared",
3372
+ namespacePolicies: [],
3373
+ maintenanceNamespaceLockStaleMs: 100,
3374
+ qmdAutoEmbedEnabled: false,
3375
+ });
3376
+ const catalog: NamespaceCatalog = Object.assign(Object.create(null), {
3377
+ enabled: true,
3378
+ async listNamespaces(): Promise<never> {
3379
+ throw new Error("catalog unavailable");
3380
+ },
3381
+ async markMaintenance() {},
3382
+ });
3383
+ const router: NamespaceSearchRouter = Object.assign(Object.create(null), {
3384
+ async updateNamespacesDetailed(ns: string[]): Promise<NamespaceUpdateResult> {
3385
+ return { backendCount: ns.length, eligibleNamespaces: ns };
3386
+ },
3387
+ async embedNamespaces(): Promise<void> {},
3388
+ });
3389
+ const scheduler = buildScheduler({ config, catalog, router });
3390
+ try {
3391
+ const served = scheduler.requestQmdMaintenanceCompletion();
3392
+ await scheduler.runQmdMaintenance();
3393
+ assert.equal(await served, false);
3394
+ } finally {
3395
+ await scheduler.dispose();
3396
+ await rm(memoryDir, { recursive: true, force: true });
3397
+ }
3398
+ });
3399
+
3303
3400
  test("requestQmdMaintenanceCompletion over namespaces: a permanent skip (branch_disabled) does not block serving", async () => {
3304
3401
  // A cataloged branch namespace is never maintained by default; that is a standing configuration,
3305
3402
  // not an unfinished pass, so the ran namespace must still serve the request.
@@ -3424,3 +3521,45 @@ test("requestQmdMaintenanceCompletion over namespaces: a pass that adds no cover
3424
3521
  await rm(memoryDir, { recursive: true, force: true });
3425
3522
  }
3426
3523
  });
3524
+
3525
+ test("requestQmdMaintenanceCompletion over namespaces: a request scoped to a namespace is served once that namespace ran, however the budget ranks the rest", async () => {
3526
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-completion-budget-"));
3527
+ const config = fixtureConfig({
3528
+ memoryDir,
3529
+ namespacesEnabled: true,
3530
+ defaultNamespace: "default",
3531
+ sharedNamespace: "shared",
3532
+ namespacePolicies: [],
3533
+ maintenanceNamespaceLockStaleMs: 100,
3534
+ qmdAutoEmbedEnabled: false,
3535
+ maintenanceMaxNamespacesPerCycle: 1,
3536
+ });
3537
+ const catalog: NamespaceCatalog = Object.assign(Object.create(null), {
3538
+ enabled: true,
3539
+ async listNamespaces() {
3540
+ return [{ namespace: "default" }];
3541
+ },
3542
+ async markMaintenance() {},
3543
+ });
3544
+ const router: NamespaceSearchRouter = Object.assign(Object.create(null), {
3545
+ async updateNamespacesDetailed(ns: string[]): Promise<NamespaceUpdateResult> {
3546
+ return { backendCount: ns.length, eligibleNamespaces: ns };
3547
+ },
3548
+ async embedNamespaces(): Promise<void> {},
3549
+ });
3550
+ const scheduler = buildScheduler({ config, catalog, router });
3551
+ try {
3552
+ // Budget 1 with default and shared: default always ranks first and shared is never selected.
3553
+ const forDefault = scheduler.requestQmdMaintenanceCompletion("default");
3554
+ const forShared = scheduler.requestQmdMaintenanceCompletion("shared");
3555
+ await scheduler.runQmdMaintenance();
3556
+ assert.equal(await forDefault, true, "the namespace the migration rewrote ran this pass");
3557
+ // shared starved: the pass added coverage once, then a pass adds none and the request ends unserved.
3558
+ await scheduler.runQmdMaintenance();
3559
+ assert.equal(await forShared, false);
3560
+ assert.equal(scheduler.isQmdMaintenancePending(), false);
3561
+ } finally {
3562
+ await scheduler.dispose();
3563
+ await rm(memoryDir, { recursive: true, force: true });
3564
+ }
3565
+ });
@@ -499,9 +499,9 @@ export class MaintenanceScheduler {
499
499
  * unavailable or maintenance off), the pass failed, or dispose() abandoned it,
500
500
  * so a caller that must not lose the re-index keeps owing it.
501
501
  */
502
- requestQmdMaintenanceCompletion(): Promise<boolean> {
502
+ requestQmdMaintenanceCompletion(namespace?: string): Promise<boolean> {
503
503
  if (this.disposed || !this.requestQmdMaintenance()) return Promise.resolve(false);
504
- return this.qmdCompletion.wait();
504
+ return this.qmdCompletion.wait(namespace);
505
505
  }
506
506
 
507
507
  /**
@@ -546,6 +546,8 @@ export class MaintenanceScheduler {
546
546
  const settle = this.qmdCompletion.claim();
547
547
  let served = false;
548
548
  let deferred = false;
549
+ // Set only by the namespace-aware branch; absent means the pass outcome applies to every waiter.
550
+ let ranNamespaces: Set<string> | undefined;
549
551
 
550
552
  try {
551
553
  if (resolveNamespaceCapabilities(this.deps.config).namespaces) {
@@ -626,9 +628,13 @@ export class MaintenanceScheduler {
626
628
  // request, so a deployment with more namespaces than the cycle budget still converges.
627
629
  // Permanent skip reasons describe namespaces this deployment never re-indexes.
628
630
  const covered = this.qmdCoveredNamespaces;
631
+ const ran = new Set<string>();
632
+ ranNamespaces = ran;
629
633
  const before = covered.size;
630
634
  for (const status of summary.statuses) {
631
- if (status.state === "ran") covered.add(status.namespace);
635
+ if (status.state !== "ran") continue;
636
+ covered.add(status.namespace);
637
+ ran.add(status.namespace);
632
638
  }
633
639
  const uncovered = summary.statuses.some(
634
640
  (status) =>
@@ -636,10 +642,13 @@ export class MaintenanceScheduler {
636
642
  (status.reason === "budget_exhausted" || status.reason === "lock_held") &&
637
643
  !covered.has(status.namespace),
638
644
  );
639
- served = summary.ran > 0 && summary.failed === 0 && !uncovered;
640
- // Partial progress keeps the waiters for another pass; no progress (or a failure) ends the
641
- // request unserved so a stuck namespace cannot re-arm passes forever.
642
- deferred = !served && summary.failed === 0 && covered.size > before && !this.disposed;
645
+ // The batch runner drops namespace "*" skips from the statuses, so a failed catalog read
646
+ // is only visible on the plan: cataloged namespaces were never discovered this pass.
647
+ const catalogReadFailed = plan.skipped.some((skip) => skip.reason === "catalog_read_failed");
648
+ served = summary.ran > 0 && summary.failed === 0 && !uncovered && !catalogReadFailed;
649
+ // Partial progress keeps the waiters for another pass; no progress, a failure or a failed
650
+ // catalog read ends the request unserved so the next start asks again.
651
+ deferred = !served && summary.failed === 0 && !catalogReadFailed && covered.size > before && !this.disposed;
643
652
  } else {
644
653
  const qmd = this.deps.getQmd();
645
654
  // Plain update() is fail-open on every backend: it resolves when the min-interval gate,
@@ -678,7 +687,7 @@ export class MaintenanceScheduler {
678
687
  // backoff) does not advance lastUpdateRanAtMs, so it leaves caches warm
679
688
  // (#1904, Codex/Cursor).
680
689
  } finally {
681
- settle(served, deferred);
690
+ settle(served, deferred, ranNamespaces);
682
691
  if (!deferred) this.qmdCoveredNamespaces.clear();
683
692
  this.qmdMaintenanceInFlight = false;
684
693
  if (deferred) this.qmdMaintenancePending = true;
@@ -6,22 +6,34 @@
6
6
  * settles them with the pass outcome, and abandons them on dispose().
7
7
  */
8
8
  export class QmdMaintenanceCompletion {
9
- private waiters: Array<(served: boolean) => void> = [];
9
+ private waiters: Array<{ resolve: (served: boolean) => void; namespace?: string }> = [];
10
10
 
11
- /** Resolves true when a pass that started after this call completed successfully. */
12
- wait(): Promise<boolean> {
13
- return new Promise((resolve) => this.waiters.push(resolve));
11
+ /**
12
+ * Resolves true when a pass that started after this call completed successfully. With a
13
+ * `namespace` the request is served as soon as a pass ran that namespace, whatever else it skipped.
14
+ */
15
+ wait(namespace?: string): Promise<boolean> {
16
+ return new Promise((resolve) => this.waiters.push({ resolve, namespace }));
14
17
  }
15
18
 
16
19
  /**
17
20
  * A pass is starting: take every waiter it will serve and return their settle function.
18
- * `defer` keeps them waiting for a later pass (a pass that covered only part of the selection).
21
+ * `ran` lists the namespaces a namespace-aware pass ran (absent when namespaces are disabled:
22
+ * one corpus is maintained and the pass outcome applies to every waiter); `defer` keeps the
23
+ * waiters it did not serve waiting for a later pass (a pass that covered only part of the selection).
19
24
  */
20
- claim(): (served: boolean, defer?: boolean) => void {
25
+ claim(): (served: boolean, defer?: boolean, ran?: ReadonlySet<string>) => void {
21
26
  const claimed = this.waiters.splice(0);
22
- return (served, defer = false) => {
23
- if (defer) this.waiters.unshift(...claimed);
24
- else claimed.forEach((resolve) => resolve(served));
27
+ return (served, defer = false, ran) => {
28
+ const requeue: typeof claimed = [];
29
+ for (const waiter of claimed) {
30
+ // A scoped waiter needs its own namespace to have run; the pass outcome alone is not enough.
31
+ const own = waiter.namespace !== undefined && ran !== undefined ? ran.has(waiter.namespace) : served;
32
+ if (own) waiter.resolve(true);
33
+ else if (defer) requeue.push(waiter);
34
+ else waiter.resolve(false);
35
+ }
36
+ this.waiters.unshift(...requeue);
25
37
  };
26
38
  }
27
39
 
@@ -1515,7 +1515,7 @@ export class Orchestrator {
1515
1515
  storage.onEntityMigrationComplete = () => {
1516
1516
  if (!this.acceptingEntityMigrationMaintenance) return false;
1517
1517
  // Settles once a QMD pass covering this request ran; the migration keeps its pending marker until then.
1518
- const served = this.maintenanceScheduler.requestQmdMaintenanceCompletion();
1518
+ const served = this.maintenanceScheduler.requestQmdMaintenanceCompletion(namespace);
1519
1519
  // Same side effects as before: projection rebuild and ledger-compaction checks, independent of QMD.
1520
1520
  this.requestQmdMaintenance();
1521
1521
  return served;