@remnic/core 9.3.727 → 9.3.729

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 (95) hide show
  1. package/dist/access-boundary.d.ts +2 -2
  2. package/dist/access-boundary.js +6 -6
  3. package/dist/access-cli.js +11 -11
  4. package/dist/access-http.d.ts +2 -2
  5. package/dist/access-http.js +10 -10
  6. package/dist/access-mcp.d.ts +2 -2
  7. package/dist/access-mcp.js +9 -9
  8. package/dist/access-operations-batch.js +7 -7
  9. package/dist/access-operations.d.ts +8 -8
  10. package/dist/access-operations.js +8 -8
  11. package/dist/access-schema.d.ts +46 -46
  12. package/dist/{access-service-Bgpjxo4f.d.ts → access-service-BXMJqkTA.d.ts} +1 -1
  13. package/dist/access-service.d.ts +2 -2
  14. package/dist/access-service.js +5 -5
  15. package/dist/access-surface-catalog.d.ts +2 -2
  16. package/dist/bootstrap.d.ts +1 -1
  17. package/dist/capabilities.d.ts +43 -1
  18. package/dist/capabilities.js +5 -3
  19. package/dist/{chunk-DVRHLN2O.js → chunk-3RCBIOOJ.js} +18 -2
  20. package/dist/{chunk-DVRHLN2O.js.map → chunk-3RCBIOOJ.js.map} +1 -1
  21. package/dist/{chunk-R7IYU5DZ.js → chunk-4ESAR77N.js} +2 -2
  22. package/dist/{chunk-SNKPUJHR.js → chunk-76NHR6ZL.js} +3 -3
  23. package/dist/{chunk-6PY5YJAV.js → chunk-BQCHEV7O.js} +8 -3
  24. package/dist/{chunk-6PY5YJAV.js.map → chunk-BQCHEV7O.js.map} +1 -1
  25. package/dist/{chunk-ZA2S2VLL.js → chunk-C2PG7IEH.js} +5 -2
  26. package/dist/chunk-C2PG7IEH.js.map +1 -0
  27. package/dist/{chunk-MJIHJ7OG.js → chunk-H5BWGCRZ.js} +11 -10
  28. package/dist/chunk-H5BWGCRZ.js.map +1 -0
  29. package/dist/{chunk-XPHVAOBT.js → chunk-HEDZUSCS.js} +180 -101
  30. package/dist/chunk-HEDZUSCS.js.map +1 -0
  31. package/dist/{chunk-N3VNUM4Z.js → chunk-HGNOCPZX.js} +4 -4
  32. package/dist/{chunk-CSRXQ6NB.js → chunk-I5REUWLH.js} +10 -4
  33. package/dist/chunk-I5REUWLH.js.map +1 -0
  34. package/dist/{chunk-NSA5ZRUL.js → chunk-M4AYVEWS.js} +3 -3
  35. package/dist/{chunk-OEEW4KJA.js → chunk-O6YH5F47.js} +3 -3
  36. package/dist/{chunk-27QQC7ZX.js → chunk-UM5AWJNQ.js} +6 -5
  37. package/dist/chunk-UM5AWJNQ.js.map +1 -0
  38. package/dist/{chunk-2KAYTPPT.js → chunk-UTHQPSRC.js} +2 -2
  39. package/dist/{chunk-CTOQEZSN.js → chunk-V4GNSDFH.js} +2 -2
  40. package/dist/{chunk-EHISUJFN.js → chunk-WAS3NV46.js} +6 -3
  41. package/dist/chunk-WAS3NV46.js.map +1 -0
  42. package/dist/{chunk-DKJCRGWM.js → chunk-YGIQORJ3.js} +2 -2
  43. package/dist/{cli-BLME420h.d.ts → cli-DmAiDd8A.d.ts} +2 -2
  44. package/dist/cli.d.ts +3 -3
  45. package/dist/cli.js +13 -13
  46. package/dist/embedding-fallback.js +2 -1
  47. package/dist/explicit-capture.d.ts +1 -1
  48. package/dist/extraction.js +2 -1
  49. package/dist/{first-start-migration-PG5HBC3K.js → first-start-migration-22LPFHD5.js} +6 -2
  50. package/dist/first-start-migration-22LPFHD5.js.map +1 -0
  51. package/dist/index.d.ts +4 -4
  52. package/dist/index.js +16 -16
  53. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  54. package/dist/namespaces/migrate.js +5 -4
  55. package/dist/namespaces/search.js +4 -3
  56. package/dist/operator-toolkit.js +6 -6
  57. package/dist/{orchestrator-DqPAz8ot.d.ts → orchestrator-BR0sGsME.d.ts} +104 -26
  58. package/dist/orchestrator.d.ts +1 -1
  59. package/dist/orchestrator.js +7 -7
  60. package/dist/schemas.d.ts +74 -74
  61. package/dist/search/embed-helper.js +2 -1
  62. package/dist/search/factory.js +3 -2
  63. package/dist/search/index.js +3 -2
  64. package/dist/shared-context/manager.d.ts +2 -2
  65. package/dist/transfer/types.d.ts +12 -12
  66. package/package.json +2 -2
  67. package/src/access-service.ts +2 -1
  68. package/src/capabilities.test.ts +155 -0
  69. package/src/capabilities.ts +91 -0
  70. package/src/chat/chat-engine.test.ts +39 -1
  71. package/src/chat/chat-session.ts +25 -2
  72. package/src/cli.ts +3 -2
  73. package/src/embedding-fallback.ts +2 -1
  74. package/src/extraction.ts +7 -3
  75. package/src/maintenance/first-start-migration.ts +3 -1
  76. package/src/orchestration/extraction-queue-coordinator.ts +175 -0
  77. package/src/orchestrator-extraction-queue.test.ts +26 -38
  78. package/src/orchestrator-flush.test.ts +10 -15
  79. package/src/orchestrator.ts +52 -104
  80. package/src/search/embed-helper.ts +3 -2
  81. package/dist/chunk-27QQC7ZX.js.map +0 -1
  82. package/dist/chunk-CSRXQ6NB.js.map +0 -1
  83. package/dist/chunk-EHISUJFN.js.map +0 -1
  84. package/dist/chunk-MJIHJ7OG.js.map +0 -1
  85. package/dist/chunk-XPHVAOBT.js.map +0 -1
  86. package/dist/chunk-ZA2S2VLL.js.map +0 -1
  87. package/dist/first-start-migration-PG5HBC3K.js.map +0 -1
  88. /package/dist/{chunk-R7IYU5DZ.js.map → chunk-4ESAR77N.js.map} +0 -0
  89. /package/dist/{chunk-SNKPUJHR.js.map → chunk-76NHR6ZL.js.map} +0 -0
  90. /package/dist/{chunk-N3VNUM4Z.js.map → chunk-HGNOCPZX.js.map} +0 -0
  91. /package/dist/{chunk-NSA5ZRUL.js.map → chunk-M4AYVEWS.js.map} +0 -0
  92. /package/dist/{chunk-OEEW4KJA.js.map → chunk-O6YH5F47.js.map} +0 -0
  93. /package/dist/{chunk-2KAYTPPT.js.map → chunk-UTHQPSRC.js.map} +0 -0
  94. /package/dist/{chunk-CTOQEZSN.js.map → chunk-V4GNSDFH.js.map} +0 -0
  95. /package/dist/{chunk-DKJCRGWM.js.map → chunk-YGIQORJ3.js.map} +0 -0
@@ -313,6 +313,7 @@ declare const CapsuleBlockSchema: z.ZodObject<{
313
313
  peerProfiles: boolean;
314
314
  }>;
315
315
  }, "strip", z.ZodTypeAny, {
316
+ id: string;
316
317
  includes: {
317
318
  procedural: boolean;
318
319
  taxonomy: boolean;
@@ -320,7 +321,6 @@ declare const CapsuleBlockSchema: z.ZodObject<{
320
321
  peerProfiles: boolean;
321
322
  };
322
323
  schemaVersion: string;
323
- id: string;
324
324
  version: string;
325
325
  description: string;
326
326
  parentCapsule: string | null;
@@ -334,6 +334,7 @@ declare const CapsuleBlockSchema: z.ZodObject<{
334
334
  directAnswerEnabled: boolean;
335
335
  };
336
336
  }, {
337
+ id: string;
337
338
  includes: {
338
339
  procedural: boolean;
339
340
  taxonomy: boolean;
@@ -341,7 +342,6 @@ declare const CapsuleBlockSchema: z.ZodObject<{
341
342
  peerProfiles: boolean;
342
343
  };
343
344
  schemaVersion: string;
344
- id: string;
345
345
  version: string;
346
346
  description: string;
347
347
  parentCapsule: string | null;
@@ -464,6 +464,7 @@ declare const ExportManifestV2Schema: z.ZodObject<{
464
464
  peerProfiles: boolean;
465
465
  }>;
466
466
  }, "strip", z.ZodTypeAny, {
467
+ id: string;
467
468
  includes: {
468
469
  procedural: boolean;
469
470
  taxonomy: boolean;
@@ -471,7 +472,6 @@ declare const ExportManifestV2Schema: z.ZodObject<{
471
472
  peerProfiles: boolean;
472
473
  };
473
474
  schemaVersion: string;
474
- id: string;
475
475
  version: string;
476
476
  description: string;
477
477
  parentCapsule: string | null;
@@ -485,6 +485,7 @@ declare const ExportManifestV2Schema: z.ZodObject<{
485
485
  directAnswerEnabled: boolean;
486
486
  };
487
487
  }, {
488
+ id: string;
488
489
  includes: {
489
490
  procedural: boolean;
490
491
  taxonomy: boolean;
@@ -492,7 +493,6 @@ declare const ExportManifestV2Schema: z.ZodObject<{
492
493
  peerProfiles: boolean;
493
494
  };
494
495
  schemaVersion: string;
495
- id: string;
496
496
  version: string;
497
497
  description: string;
498
498
  parentCapsule: string | null;
@@ -518,6 +518,7 @@ declare const ExportManifestV2Schema: z.ZodObject<{
518
518
  pluginVersion: string;
519
519
  includesTranscripts: boolean;
520
520
  capsule: {
521
+ id: string;
521
522
  includes: {
522
523
  procedural: boolean;
523
524
  taxonomy: boolean;
@@ -525,7 +526,6 @@ declare const ExportManifestV2Schema: z.ZodObject<{
525
526
  peerProfiles: boolean;
526
527
  };
527
528
  schemaVersion: string;
528
- id: string;
529
529
  version: string;
530
530
  description: string;
531
531
  parentCapsule: string | null;
@@ -551,6 +551,7 @@ declare const ExportManifestV2Schema: z.ZodObject<{
551
551
  pluginVersion: string;
552
552
  includesTranscripts: boolean;
553
553
  capsule: {
554
+ id: string;
554
555
  includes: {
555
556
  procedural: boolean;
556
557
  taxonomy: boolean;
@@ -558,7 +559,6 @@ declare const ExportManifestV2Schema: z.ZodObject<{
558
559
  peerProfiles: boolean;
559
560
  };
560
561
  schemaVersion: string;
561
- id: string;
562
562
  version: string;
563
563
  description: string;
564
564
  parentCapsule: string | null;
@@ -683,6 +683,7 @@ declare const ExportBundleV2Schema: z.ZodObject<{
683
683
  peerProfiles: boolean;
684
684
  }>;
685
685
  }, "strip", z.ZodTypeAny, {
686
+ id: string;
686
687
  includes: {
687
688
  procedural: boolean;
688
689
  taxonomy: boolean;
@@ -690,7 +691,6 @@ declare const ExportBundleV2Schema: z.ZodObject<{
690
691
  peerProfiles: boolean;
691
692
  };
692
693
  schemaVersion: string;
693
- id: string;
694
694
  version: string;
695
695
  description: string;
696
696
  parentCapsule: string | null;
@@ -704,6 +704,7 @@ declare const ExportBundleV2Schema: z.ZodObject<{
704
704
  directAnswerEnabled: boolean;
705
705
  };
706
706
  }, {
707
+ id: string;
707
708
  includes: {
708
709
  procedural: boolean;
709
710
  taxonomy: boolean;
@@ -711,7 +712,6 @@ declare const ExportBundleV2Schema: z.ZodObject<{
711
712
  peerProfiles: boolean;
712
713
  };
713
714
  schemaVersion: string;
714
- id: string;
715
715
  version: string;
716
716
  description: string;
717
717
  parentCapsule: string | null;
@@ -737,6 +737,7 @@ declare const ExportBundleV2Schema: z.ZodObject<{
737
737
  pluginVersion: string;
738
738
  includesTranscripts: boolean;
739
739
  capsule: {
740
+ id: string;
740
741
  includes: {
741
742
  procedural: boolean;
742
743
  taxonomy: boolean;
@@ -744,7 +745,6 @@ declare const ExportBundleV2Schema: z.ZodObject<{
744
745
  peerProfiles: boolean;
745
746
  };
746
747
  schemaVersion: string;
747
- id: string;
748
748
  version: string;
749
749
  description: string;
750
750
  parentCapsule: string | null;
@@ -770,6 +770,7 @@ declare const ExportBundleV2Schema: z.ZodObject<{
770
770
  pluginVersion: string;
771
771
  includesTranscripts: boolean;
772
772
  capsule: {
773
+ id: string;
773
774
  includes: {
774
775
  procedural: boolean;
775
776
  taxonomy: boolean;
@@ -777,7 +778,6 @@ declare const ExportBundleV2Schema: z.ZodObject<{
777
778
  peerProfiles: boolean;
778
779
  };
779
780
  schemaVersion: string;
780
- id: string;
781
781
  version: string;
782
782
  description: string;
783
783
  parentCapsule: string | null;
@@ -819,6 +819,7 @@ declare const ExportBundleV2Schema: z.ZodObject<{
819
819
  pluginVersion: string;
820
820
  includesTranscripts: boolean;
821
821
  capsule: {
822
+ id: string;
822
823
  includes: {
823
824
  procedural: boolean;
824
825
  taxonomy: boolean;
@@ -826,7 +827,6 @@ declare const ExportBundleV2Schema: z.ZodObject<{
826
827
  peerProfiles: boolean;
827
828
  };
828
829
  schemaVersion: string;
829
- id: string;
830
830
  version: string;
831
831
  description: string;
832
832
  parentCapsule: string | null;
@@ -858,6 +858,7 @@ declare const ExportBundleV2Schema: z.ZodObject<{
858
858
  pluginVersion: string;
859
859
  includesTranscripts: boolean;
860
860
  capsule: {
861
+ id: string;
861
862
  includes: {
862
863
  procedural: boolean;
863
864
  taxonomy: boolean;
@@ -865,7 +866,6 @@ declare const ExportBundleV2Schema: z.ZodObject<{
865
866
  peerProfiles: boolean;
866
867
  };
867
868
  schemaVersion: string;
868
- id: string;
869
869
  version: string;
870
870
  description: string;
871
871
  parentCapsule: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.3.727",
3
+ "version": "9.3.729",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -2990,7 +2990,7 @@
2990
2990
  "core"
2991
2991
  ],
2992
2992
  "peerDependencies": {
2993
- "@remnic/coding-graph": "^9.3.727"
2993
+ "@remnic/coding-graph": "^9.3.729"
2994
2994
  },
2995
2995
  "peerDependenciesMeta": {
2996
2996
  "@remnic/coding-graph": {
@@ -5,6 +5,7 @@ import { readdirSync, unlinkSync } from "node:fs";
5
5
  import { createHash } from "node:crypto";
6
6
  import { ZodError } from "zod";
7
7
  import { AccessIdempotencyStore, hashAccessIdempotencyPayload } from "./access-idempotency.js";
8
+ import { resolveMemoryLifecycleCapabilities } from "./capabilities.js";
8
9
  import { AccessAuditAdapter, type AccessAuditConfig, type AccessAuditResult } from "./access-audit.js";
9
10
  import type { AnomalyDetectorResult } from "./recall-audit-anomaly.js";
10
11
  import { resolveGitContext } from "./coding/git-context.js";
@@ -8187,7 +8188,7 @@ export class EngramAccessService {
8187
8188
 
8188
8189
  get embeddingLookupFactoryRef(): (storage: import("./storage.js").StorageManager) => SemanticDedupLookup | undefined {
8189
8190
  return (storage) => {
8190
- if (!this.orchestrator.config.embeddingFallbackEnabled) return undefined;
8191
+ if (!resolveMemoryLifecycleCapabilities(this.orchestrator.config).embeddingFallback) return undefined;
8191
8192
  return async (content: string, limit: number) => {
8192
8193
  try {
8193
8194
  return await this.orchestrator.semanticDedupLookup(content, limit, storage);
@@ -153,6 +153,12 @@ import {
153
153
  type GraphConstructionCapabilitySet,
154
154
  } from "./capabilities.js";
155
155
 
156
+ import {
157
+ resolveMemoryLifecycleCapabilities,
158
+ type MemoryLifecycleCapabilitySet,
159
+ } from "./capabilities.js";
160
+
161
+
156
162
  const GRAPH_FIELD_TO_FLAG: Record<keyof GraphConstructionCapabilitySet, string> = {
157
163
  entityGraph: "entityGraphEnabled",
158
164
  timeGraph: "timeGraphEnabled",
@@ -306,3 +312,152 @@ test("resolveGraphConstructionCapabilities matches pre-migration config reads (p
306
312
  );
307
313
  }
308
314
  });
315
+
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // MemoryLifecycleCapabilitySet — gate-parity tests (issue #1523 batch 3).
319
+ //
320
+ // Same invariant as the sets above: every caps field must project from its
321
+ // `<field>Enabled` config flag, so a future edit that maps a field to the wrong
322
+ // flag fails loudly here. All ten flags are required booleans on PluginConfig
323
+ // (defaults resolved at the parse boundary), so the projection is a pure
324
+ // passthrough — no optional-default variants needed.
325
+ //
326
+ // Parity contract: a caps-resolved run and a config-derived run MUST produce
327
+ // identical boolean values for every gate — on AND off.
328
+ // ---------------------------------------------------------------------------
329
+
330
+ const LIFECYCLE_FIELD_TO_FLAG: Record<keyof MemoryLifecycleCapabilitySet, string> = {
331
+ temporalSupersession: "temporalSupersessionEnabled",
332
+ temporalMemoryTree: "temporalMemoryTreeEnabled",
333
+ lifecyclePolicy: "lifecyclePolicyEnabled",
334
+ lifecycleFilterStale: "lifecycleFilterStaleEnabled",
335
+ lifecycleMetrics: "lifecycleMetricsEnabled",
336
+ extractionScopeClassification: "extractionScopeClassificationEnabled",
337
+ extractionJudge: "extractionJudgeEnabled",
338
+ extractionDedupe: "extractionDedupeEnabled",
339
+ extractionTelemetryPrefilter: "extractionTelemetryPrefilterEnabled",
340
+ extractionJudgeTelemetry: "extractionJudgeTelemetryEnabled",
341
+ embeddingFallback: "embeddingFallbackEnabled",
342
+ };
343
+
344
+ const LIFECYCLE_FIELDS = Object.keys(LIFECYCLE_FIELD_TO_FLAG) as Array<
345
+ keyof MemoryLifecycleCapabilitySet
346
+ >;
347
+
348
+ test("resolveMemoryLifecycleCapabilities projects every field from its flag (true variant)", () => {
349
+ const overrides: Record<string, boolean> = {};
350
+ for (const flag of Object.values(LIFECYCLE_FIELD_TO_FLAG)) overrides[flag] = true;
351
+ const config = parseConfig(overrides);
352
+ const caps = resolveMemoryLifecycleCapabilities(config);
353
+
354
+ for (const field of LIFECYCLE_FIELDS) {
355
+ const flag = LIFECYCLE_FIELD_TO_FLAG[field];
356
+ assert.equal(
357
+ caps[field],
358
+ (config as unknown as Record<string, boolean>)[flag],
359
+ `caps.${field} must equal config.${flag} (true variant)`,
360
+ );
361
+ assert.equal(caps[field], true, `caps.${field} should be true here`);
362
+ }
363
+ });
364
+
365
+ test("resolveMemoryLifecycleCapabilities projects every field from its flag (false variant)", () => {
366
+ const overrides: Record<string, boolean> = {};
367
+ for (const flag of Object.values(LIFECYCLE_FIELD_TO_FLAG)) overrides[flag] = false;
368
+ const config = parseConfig(overrides);
369
+ const caps = resolveMemoryLifecycleCapabilities(config);
370
+
371
+ for (const field of LIFECYCLE_FIELDS) {
372
+ const flag = LIFECYCLE_FIELD_TO_FLAG[field];
373
+ assert.equal(
374
+ caps[field],
375
+ (config as unknown as Record<string, boolean>)[flag],
376
+ `caps.${field} must equal config.${flag} (false variant)`,
377
+ );
378
+ assert.equal(caps[field], false, `caps.${field} should be false here`);
379
+ }
380
+ });
381
+
382
+ test("resolveMemoryLifecycleCapabilities returns a frozen object", () => {
383
+ const config = parseConfig({});
384
+ const caps = resolveMemoryLifecycleCapabilities(config);
385
+ assert.equal(Object.isFrozen(caps), true);
386
+ });
387
+
388
+ test("resolveMemoryLifecycleCapabilities matches pre-migration config reads (parity contract)", () => {
389
+ // For representative on/off combinations, the caps-resolved values MUST be
390
+ // identical to what the old scattered `this.config.<flag>Enabled` reads
391
+ // produced. All ten flags used bare reads (no `!== false`/`=== true` at the
392
+ // call site that would change the value — the two sites that wrote
393
+ // `=== true` were redundant coercion on an already-boolean config field),
394
+ // so parity is a direct equality against the resolved config boolean.
395
+ const cases: Record<string, Record<string, boolean>> = {
396
+ "all-off": {
397
+ temporalSupersessionEnabled: false,
398
+ temporalMemoryTreeEnabled: false,
399
+ lifecyclePolicyEnabled: false,
400
+ lifecycleFilterStaleEnabled: false,
401
+ lifecycleMetricsEnabled: false,
402
+ extractionScopeClassificationEnabled: false,
403
+ extractionJudgeEnabled: false,
404
+ extractionDedupeEnabled: false,
405
+ extractionTelemetryPrefilterEnabled: false,
406
+ extractionJudgeTelemetryEnabled: false,
407
+ embeddingFallbackEnabled: false,
408
+ },
409
+ "all-on": {
410
+ temporalSupersessionEnabled: true,
411
+ temporalMemoryTreeEnabled: true,
412
+ lifecyclePolicyEnabled: true,
413
+ lifecycleFilterStaleEnabled: true,
414
+ lifecycleMetricsEnabled: true,
415
+ extractionScopeClassificationEnabled: true,
416
+ extractionJudgeEnabled: true,
417
+ extractionDedupeEnabled: true,
418
+ extractionTelemetryPrefilterEnabled: true,
419
+ extractionJudgeTelemetryEnabled: true,
420
+ embeddingFallbackEnabled: true,
421
+ },
422
+ "temporal-on-rest-off": {
423
+ temporalSupersessionEnabled: true,
424
+ temporalMemoryTreeEnabled: true,
425
+ lifecyclePolicyEnabled: false,
426
+ lifecycleFilterStaleEnabled: false,
427
+ lifecycleMetricsEnabled: false,
428
+ extractionScopeClassificationEnabled: false,
429
+ extractionJudgeEnabled: false,
430
+ extractionDedupeEnabled: false,
431
+ extractionTelemetryPrefilterEnabled: false,
432
+ extractionJudgeTelemetryEnabled: false,
433
+ embeddingFallbackEnabled: false,
434
+ },
435
+ "extraction-on-rest-off": {
436
+ temporalSupersessionEnabled: false,
437
+ temporalMemoryTreeEnabled: false,
438
+ lifecyclePolicyEnabled: false,
439
+ lifecycleFilterStaleEnabled: false,
440
+ lifecycleMetricsEnabled: false,
441
+ extractionScopeClassificationEnabled: true,
442
+ extractionJudgeEnabled: true,
443
+ extractionDedupeEnabled: true,
444
+ extractionTelemetryPrefilterEnabled: true,
445
+ extractionJudgeTelemetryEnabled: true,
446
+ embeddingFallbackEnabled: true,
447
+ },
448
+ };
449
+
450
+ for (const [label, overrides] of Object.entries(cases)) {
451
+ const config = parseConfig(overrides);
452
+ const caps = resolveMemoryLifecycleCapabilities(config);
453
+
454
+ for (const field of LIFECYCLE_FIELDS) {
455
+ const flag = LIFECYCLE_FIELD_TO_FLAG[field];
456
+ assert.equal(
457
+ caps[field],
458
+ (config as unknown as Record<string, boolean>)[flag],
459
+ `[${label}] caps.${field} must equal config.${flag}`,
460
+ );
461
+ }
462
+ }
463
+ });
@@ -203,3 +203,94 @@ export function resolveAccessSetupCapabilities(
203
203
  recallAuditAnomalyDetection: config.recallAuditAnomalyDetectionEnabled === true,
204
204
  });
205
205
  }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Memory-lifecycle capability set (issue #1523 batch 3).
209
+ //
210
+ // The flags below gate memory WRITE/EXTRACTION (extraction judge, scope
211
+ // classification, dedupe, telemetry prefilter), temporal supersession of
212
+ // stored facts, and the lifecycle policy pass (promotion / decay / stale
213
+ // filtering). They are read on the persistExtraction write path, the recall
214
+ // candidate-filter path, and the maintenance / lifecycle-pass path — so, like
215
+ // the graph-construction set above, they get their own projection resolved
216
+ // ONCE at each operation entry that reads them (never re-derived from raw
217
+ // config mid-operation — the gate-divergence defect class #1523 targets).
218
+ //
219
+ // Every field projects from an already-resolved PluginConfig boolean (defaults
220
+ // are applied at the config-parse boundary, rule 36), so the resolver is a
221
+ // pure projection — no `!== false` / `=== true` re-coercion here.
222
+ // ---------------------------------------------------------------------------
223
+
224
+ /**
225
+ * Frozen projection of memory-lifecycle feature gates (issue #1523 batch 3).
226
+ *
227
+ * Resolved once per write / recall-filter / maintenance operation and threaded
228
+ * down. Composition lives ONLY in {@link resolveMemoryLifecycleCapabilities}.
229
+ */
230
+ export interface MemoryLifecycleCapabilitySet {
231
+ /** `temporalSupersessionEnabled` — supersede stale structured facts on write, filter on recall. */
232
+ readonly temporalSupersession: boolean;
233
+ /** `temporalMemoryTreeEnabled` — temporal-memory-tree build + recall tier. */
234
+ readonly temporalMemoryTree: boolean;
235
+ /** `lifecyclePolicyEnabled` — lifecycle promotion / decay metadata pass. */
236
+ readonly lifecyclePolicy: boolean;
237
+ /** `lifecycleFilterStaleEnabled` — filter stale-lifecycle memories from recall. */
238
+ readonly lifecycleFilterStale: boolean;
239
+ /** `lifecycleMetricsEnabled` — emit lifecycle-pass metrics. */
240
+ readonly lifecycleMetrics: boolean;
241
+ /** `extractionScopeClassificationEnabled` — LLM classifies each fact's scope. */
242
+ readonly extractionScopeClassification: boolean;
243
+ /** `extractionJudgeEnabled` — LLM-as-judge fact-worthiness gate. */
244
+ readonly extractionJudge: boolean;
245
+ /** `extractionDedupeEnabled` — dedupe against recent extractions. */
246
+ readonly extractionDedupe: boolean;
247
+ /** `extractionTelemetryPrefilterEnabled` — skip mechanical-telemetry transcripts. */
248
+ readonly extractionTelemetryPrefilter: boolean;
249
+ /** `extractionJudgeTelemetryEnabled` — collect judge verdict telemetry. */
250
+ readonly extractionJudgeTelemetry: boolean;
251
+ /** `embeddingFallbackEnabled` — semantic-dedup / archive-search embedding fallback. */
252
+ readonly embeddingFallback: boolean;
253
+ }
254
+
255
+ /**
256
+ * Config projection consumed by {@link resolveMemoryLifecycleCapabilities}.
257
+ */
258
+ export type MemoryLifecycleConfigProjection = Pick<
259
+ PluginConfig,
260
+ | "temporalSupersessionEnabled"
261
+ | "temporalMemoryTreeEnabled"
262
+ | "lifecyclePolicyEnabled"
263
+ | "lifecycleFilterStaleEnabled"
264
+ | "lifecycleMetricsEnabled"
265
+ | "extractionScopeClassificationEnabled"
266
+ | "extractionJudgeEnabled"
267
+ | "extractionDedupeEnabled"
268
+ | "extractionTelemetryPrefilterEnabled"
269
+ | "extractionJudgeTelemetryEnabled"
270
+ | "embeddingFallbackEnabled"
271
+ >;
272
+
273
+ /**
274
+ * Resolve the {@link MemoryLifecycleCapabilitySet} from parsed config.
275
+ *
276
+ * Call this ONCE per write / recall-filter / maintenance operation entry and
277
+ * thread the result down — exactly the pattern {@link resolveCapabilities}
278
+ * established for recall.
279
+ */
280
+ export function resolveMemoryLifecycleCapabilities(
281
+ config: MemoryLifecycleConfigProjection,
282
+ ): MemoryLifecycleCapabilitySet {
283
+ return Object.freeze({
284
+ temporalSupersession: config.temporalSupersessionEnabled,
285
+ temporalMemoryTree: config.temporalMemoryTreeEnabled,
286
+ lifecyclePolicy: config.lifecyclePolicyEnabled,
287
+ lifecycleFilterStale: config.lifecycleFilterStaleEnabled,
288
+ lifecycleMetrics: config.lifecycleMetricsEnabled,
289
+ extractionScopeClassification: config.extractionScopeClassificationEnabled,
290
+ extractionJudge: config.extractionJudgeEnabled,
291
+ extractionDedupe: config.extractionDedupeEnabled,
292
+ extractionTelemetryPrefilter: config.extractionTelemetryPrefilterEnabled,
293
+ extractionJudgeTelemetry: config.extractionJudgeTelemetryEnabled,
294
+ embeddingFallback: config.embeddingFallbackEnabled,
295
+ });
296
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { strict as assert } from "node:assert";
10
- import { mkdir, rm, readFile, utimes, unlink } from "node:fs/promises";
10
+ import { mkdir, rm, readFile, utimes, unlink, appendFile } from "node:fs/promises";
11
11
  import { join } from "node:path";
12
12
  import { tmpdir } from "node:os";
13
13
  import { test } from "node:test";
@@ -22,6 +22,7 @@ import {
22
22
  sessionBelongsToPrincipal,
23
23
  chatSessionFile,
24
24
  cleanupExpiredChatSessions,
25
+ __resetTranscriptSeqForTest,
25
26
  } from "./chat-session.js";
26
27
  import type { ChatSessionState } from "./chat-types.js";
27
28
  import type { ChatToolExecutor } from "./chat-engine.js";
@@ -335,3 +336,40 @@ test("appendTranscriptEntry refuses to recreate a swept session (chat_session_ex
335
336
  /chat_session_expired/,
336
337
  );
337
338
  });
339
+
340
+
341
+ test("loadChatSession raises the seq counter above the on-disk max on backward clock skew (issue #1718)", async () => {
342
+ const dir = await makeTempDir();
343
+ const session = await createChatSession(dir, { principal: "skew" });
344
+ // Simulate a previous process that ran with a forward-skewed clock: append a
345
+ // transcript line whose seq is far in the future relative to the current
346
+ // Date.now(). A later restart with a normal (or backward-stepped) clock must
347
+ // still issue seqs strictly greater than this persisted line.
348
+ const futureSeq = Date.now() + 10_000_000;
349
+ await appendFile(
350
+ chatSessionFile(dir, session.id),
351
+ JSON.stringify({
352
+ seq: futureSeq,
353
+ ts: new Date().toISOString(),
354
+ role: "user",
355
+ content: "future-dated line from a forward-skewed prior process",
356
+ }) + "\n",
357
+ "utf8",
358
+ );
359
+ // Simulate a process restart whose module-level counter starts below the
360
+ // persisted on-disk seq (the backward-clock-skew window).
361
+ __resetTranscriptSeqForTest();
362
+ // Resume the session: loadChatSession must raise the counter above the
363
+ // on-disk max so the next append is strictly greater, regardless of the
364
+ // current Date.now().
365
+ const loaded = await loadChatSession(dir, session.id);
366
+ assert.ok(loaded, "session must load");
367
+ const next = await appendTranscriptEntry(dir, session.id, {
368
+ role: "assistant",
369
+ content: "resumed after backward clock skew",
370
+ });
371
+ assert.ok(
372
+ next.seq > futureSeq,
373
+ `seq ${next.seq} must exceed the on-disk max ${futureSeq} after a backward clock skew (issue #1718)`,
374
+ );
375
+ });
@@ -50,8 +50,10 @@ export function chatSessionFile(memoryDir: string, chatSessionId: string): strin
50
50
  * incremented when two appends land in the same millisecond, so `seq` is
51
51
  * strictly increasing within a process (review thread: `Date.now()` alone is
52
52
  * not monotonic — same-ms entries would share a seq and break the SSE
53
- * reconnect dedup-by-seq contract). Across process restarts `Date.now()` is
54
- * already forward-moving, so no collision occurs in practice.
53
+ * reconnect dedup-by-seq contract). Across process restarts the counter is
54
+ * raised above the on-disk max by `loadChatSession` (issue #1718), so a
55
+ * backward clock step (NTP, VM migration, manual change) cannot re-seed it
56
+ * below lines already persisted for a resumed session.
55
57
  */
56
58
  let lastTranscriptSeq = 0;
57
59
 
@@ -61,6 +63,14 @@ function nextTranscriptSeq(): number {
61
63
  return lastTranscriptSeq;
62
64
  }
63
65
 
66
+ /**
67
+ * Test-only: reset the module-level transcript seq counter so a regression
68
+ * test can deterministically simulate a fresh process restart (issue #1718).
69
+ */
70
+ export function __resetTranscriptSeqForTest(): void {
71
+ lastTranscriptSeq = 0;
72
+ }
73
+
64
74
  /**
65
75
  * Create a new chat session. The session binds the caller's principal and
66
76
  * sessionKey at creation (rule 42); every tool call flows through
@@ -175,6 +185,19 @@ export async function loadChatSession(
175
185
  // tolerated, never crash the engine).
176
186
  }
177
187
  }
188
+ // Issue #1718 — backward-clock-skew robustness: raise the module-level seq
189
+ // counter so the next append is strictly greater than any line already on
190
+ // disk for this session, regardless of clock direction across restarts
191
+ // (NTP step, VM migration, manual change). Without this, a backward clock
192
+ // skew would re-seed the counter below the on-disk max and the SSE
193
+ // reconnect dedup-by-seq contract could drop freshly-appended entries as
194
+ // duplicates. The transcript is already fully parsed above, so this is a
195
+ // single pass over the in-memory array.
196
+ for (const e of transcript) {
197
+ if (typeof e.seq === "number" && e.seq + 1 > lastTranscriptSeq) {
198
+ lastTranscriptSeq = e.seq + 1;
199
+ }
200
+ }
178
201
  return {
179
202
  id: chatSessionId,
180
203
  principal,
package/src/cli.ts CHANGED
@@ -4,6 +4,7 @@ import { access, lstat, readFile, readdir, realpath, unlink } from "node:fs/prom
4
4
  import { createHash } from "node:crypto";
5
5
  import type { Readable, Writable } from "node:stream";
6
6
  import type { Orchestrator } from "./orchestrator.js";
7
+ import { resolveMemoryLifecycleCapabilities } from "./capabilities.js";
7
8
  import { ThreadingManager } from "./threading.js";
8
9
  import { utcDayRange } from "./transcript.js";
9
10
  import { runWearablesCliCommand } from "./wearables/cli.js";
@@ -3728,7 +3729,7 @@ export function registerCli(
3728
3729
  console.log("=== Extraction Judge Verdict Stats ===\n");
3729
3730
  console.log(`Total verdicts: ${stats.total}`);
3730
3731
  if (stats.total === 0) {
3731
- if (!orchestrator.config.extractionJudgeTelemetryEnabled) {
3732
+ if (!resolveMemoryLifecycleCapabilities(orchestrator.config).extractionJudgeTelemetry) {
3732
3733
  console.log(
3733
3734
  "\nNote: extractionJudgeTelemetryEnabled is OFF. Enable it in plugin config to collect verdict telemetry.",
3734
3735
  );
@@ -8541,7 +8542,7 @@ export function registerCli(
8541
8542
  config: orchestrator.config,
8542
8543
  memoryDir: orchestrator.config.memoryDir,
8543
8544
  embeddingLookupFactory: (storage) => {
8544
- if (!orchestrator.config.embeddingFallbackEnabled) return undefined;
8545
+ if (!resolveMemoryLifecycleCapabilities(orchestrator.config).embeddingFallback) return undefined;
8545
8546
  return async (content: string, limit: number) => {
8546
8547
  try {
8547
8548
  return await orchestrator.semanticDedupLookup(content, limit, storage);
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { log } from "./logger.js";
4
+ import { resolveMemoryLifecycleCapabilities } from "./capabilities.js";
4
5
  import { readEnvVar } from "./runtime/env.js";
5
6
  import type { PluginConfig } from "./types.js";
6
7
  import {
@@ -397,7 +398,7 @@ export class EmbeddingFallback {
397
398
  private async resolveProvider(
398
399
  options: { includeHost?: boolean } = {},
399
400
  ): Promise<ProviderConfig | null> {
400
- if (!this.config.embeddingFallbackEnabled) return null;
401
+ if (!resolveMemoryLifecycleCapabilities(this.config).embeddingFallback) return null;
401
402
 
402
403
  if (
403
404
  options.includeHost !== false &&