@remnic/core 9.3.691 → 9.3.693

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 (113) hide show
  1. package/dist/access-boundary.d.ts +6 -5
  2. package/dist/access-boundary.js +12 -12
  3. package/dist/access-cli.js +71 -18
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +5 -4
  6. package/dist/access-http.js +16 -16
  7. package/dist/access-mcp.d.ts +7 -4
  8. package/dist/access-mcp.js +15 -15
  9. package/dist/access-operations.d.ts +12 -6
  10. package/dist/access-operations.js +16 -14
  11. package/dist/access-schema.js +3 -3
  12. package/dist/{access-service-CKnFdxH-.d.ts → access-service-DxCpgyVW.d.ts} +96 -2
  13. package/dist/access-service.d.ts +5 -4
  14. package/dist/access-service.js +11 -11
  15. package/dist/access-surface-catalog.d.ts +5 -4
  16. package/dist/access-surface-catalog.js +2 -0
  17. package/dist/access-surface-catalog.js.map +1 -1
  18. package/dist/bootstrap.d.ts +4 -3
  19. package/dist/{chunk-QNG35H27.js → chunk-4IQNAOP3.js} +5 -141
  20. package/dist/chunk-4IQNAOP3.js.map +1 -0
  21. package/dist/{chunk-VH2DHBHG.js → chunk-5HGPDRQZ.js} +2 -2
  22. package/dist/chunk-5HGPDRQZ.js.map +1 -0
  23. package/dist/chunk-6C2BMUZA.js +444 -0
  24. package/dist/chunk-6C2BMUZA.js.map +1 -0
  25. package/dist/{chunk-UUMYB3FR.js → chunk-6ZNIUGXB.js} +10 -10
  26. package/dist/{chunk-IPJD7WJH.js → chunk-E6BOD3UJ.js} +2 -2
  27. package/dist/{chunk-TJ5IIQIR.js → chunk-F7BVJ3IE.js} +40 -369
  28. package/dist/chunk-F7BVJ3IE.js.map +1 -0
  29. package/dist/{chunk-D3DNGQNQ.js → chunk-FM7KLRKN.js} +2 -2
  30. package/dist/{chunk-7MOTEVAA.js → chunk-HRUULBBV.js} +3 -1
  31. package/dist/chunk-HRUULBBV.js.map +1 -0
  32. package/dist/chunk-HXNMDPHN.js +149 -0
  33. package/dist/chunk-HXNMDPHN.js.map +1 -0
  34. package/dist/{chunk-7UVDIWLL.js → chunk-JNKH2XWQ.js} +36 -6
  35. package/dist/chunk-JNKH2XWQ.js.map +1 -0
  36. package/dist/{chunk-IPLYGWQF.js → chunk-KQAFEZQX.js} +5 -5
  37. package/dist/{chunk-OOO7ZKOD.js → chunk-KYH53EFD.js} +3 -3
  38. package/dist/{chunk-LQ6JI4VH.js → chunk-LD53WPMU.js} +4 -4
  39. package/dist/{chunk-KO6OL4I7.js → chunk-MQTPUPLP.js} +297 -17
  40. package/dist/chunk-MQTPUPLP.js.map +1 -0
  41. package/dist/{chunk-TSGREOYY.js → chunk-RHWFMBO5.js} +33 -6
  42. package/dist/chunk-RHWFMBO5.js.map +1 -0
  43. package/dist/{chunk-5CEJH5ZN.js → chunk-TAPSNIMA.js} +4 -4
  44. package/dist/{chunk-K7I24ZO2.js → chunk-THKNO6OX.js} +22 -21
  45. package/dist/{chunk-K7I24ZO2.js.map → chunk-THKNO6OX.js.map} +1 -1
  46. package/dist/{chunk-QLRYXOAD.js → chunk-UDJLF3BO.js} +2 -2
  47. package/dist/{chunk-NCRDCTEJ.js → chunk-WYB44HHV.js} +21 -7
  48. package/dist/chunk-WYB44HHV.js.map +1 -0
  49. package/dist/{chunk-WRFKZEO6.js → chunk-YQGHW5ML.js} +4 -4
  50. package/dist/{cli-CrSW_KRq.d.ts → cli-DoQsv4ut.d.ts} +2 -2
  51. package/dist/cli.d.ts +6 -5
  52. package/dist/cli.js +26 -25
  53. package/dist/contradiction/index.js +4 -4
  54. package/dist/explicit-capture.d.ts +4 -3
  55. package/dist/index.d.ts +6 -5
  56. package/dist/index.js +33 -31
  57. package/dist/index.js.map +1 -1
  58. package/dist/mcp-memory-inspector-app.d.ts +5 -4
  59. package/dist/namespaces/migrate.js +8 -8
  60. package/dist/namespaces/search.js +7 -7
  61. package/dist/operator-toolkit.js +11 -10
  62. package/dist/orchestration/maintenance.d.ts +112 -0
  63. package/dist/orchestration/maintenance.js +39 -0
  64. package/dist/orchestration/maintenance.js.map +1 -0
  65. package/dist/{orchestrator-DvswGK76.d.ts → orchestrator-CNJCYjZ1.d.ts} +9 -33
  66. package/dist/orchestrator.d.ts +2 -1
  67. package/dist/orchestrator.js +13 -11
  68. package/dist/schemas.d.ts +28 -28
  69. package/dist/search/factory.js +6 -6
  70. package/dist/search/index.js +11 -11
  71. package/dist/search/lancedb-backend.js +2 -2
  72. package/dist/search/meilisearch-backend.js +2 -2
  73. package/dist/search/orama-backend.js +2 -2
  74. package/dist/transfer/autodetect.js +1 -1
  75. package/dist/transfer/backup.js +1 -1
  76. package/dist/transfer/capsule-export.js +2 -2
  77. package/dist/transfer/types.d.ts +12 -12
  78. package/package.json +12 -2
  79. package/src/access-boundary.ts +2 -1
  80. package/src/access-cli.ts +65 -2
  81. package/src/access-http.ts +20 -0
  82. package/src/access-mcp.ts +29 -0
  83. package/src/access-operations.ts +46 -0
  84. package/src/access-service.ts +55 -0
  85. package/src/access-surface-catalog.test.ts +1 -1
  86. package/src/access-surface-catalog.ts +2 -0
  87. package/src/cli.ts +1 -0
  88. package/src/coding/coding-namespace.ts +1 -1
  89. package/src/coding/session-delta-surfaces.test.ts +383 -0
  90. package/src/coding/session-delta-surfaces.ts +325 -0
  91. package/src/coding/session-delta.test.ts +411 -0
  92. package/src/coding/session-delta.ts +442 -0
  93. package/src/orchestration/maintenance.test.ts +738 -0
  94. package/src/orchestration/maintenance.ts +557 -0
  95. package/src/orchestrator-flush.test.ts +0 -599
  96. package/src/orchestrator.ts +33 -419
  97. package/dist/chunk-7MOTEVAA.js.map +0 -1
  98. package/dist/chunk-7UVDIWLL.js.map +0 -1
  99. package/dist/chunk-KO6OL4I7.js.map +0 -1
  100. package/dist/chunk-NCRDCTEJ.js.map +0 -1
  101. package/dist/chunk-QNG35H27.js.map +0 -1
  102. package/dist/chunk-TJ5IIQIR.js.map +0 -1
  103. package/dist/chunk-TSGREOYY.js.map +0 -1
  104. package/dist/chunk-VH2DHBHG.js.map +0 -1
  105. /package/dist/{chunk-UUMYB3FR.js.map → chunk-6ZNIUGXB.js.map} +0 -0
  106. /package/dist/{chunk-IPJD7WJH.js.map → chunk-E6BOD3UJ.js.map} +0 -0
  107. /package/dist/{chunk-D3DNGQNQ.js.map → chunk-FM7KLRKN.js.map} +0 -0
  108. /package/dist/{chunk-IPLYGWQF.js.map → chunk-KQAFEZQX.js.map} +0 -0
  109. /package/dist/{chunk-OOO7ZKOD.js.map → chunk-KYH53EFD.js.map} +0 -0
  110. /package/dist/{chunk-LQ6JI4VH.js.map → chunk-LD53WPMU.js.map} +0 -0
  111. /package/dist/{chunk-5CEJH5ZN.js.map → chunk-TAPSNIMA.js.map} +0 -0
  112. /package/dist/{chunk-QLRYXOAD.js.map → chunk-UDJLF3BO.js.map} +0 -0
  113. /package/dist/{chunk-WRFKZEO6.js.map → chunk-YQGHW5ML.js.map} +0 -0
@@ -82,15 +82,7 @@ import {
82
82
  fallbackLlmRuntimeContextFromConfig,
83
83
  gatewayTaskChainOptions,
84
84
  } from "./fallback-llm.js";
85
- import {
86
- ensureDaySummaryCron,
87
- ensureNightlyGovernanceCron,
88
- ensureProceduralMiningCron,
89
- ensureContradictionScanCron,
90
- ensurePatternReinforcementCron,
91
- ensureGraphEdgeDecayCron,
92
- graphEdgeDecayCadenceToCronExpr,
93
- } from "./maintenance/memory-governance-cron.js";
85
+ import { MaintenanceScheduler } from "./orchestration/maintenance.js";
94
86
  import {
95
87
  runLiveConnectorsOnce,
96
88
  type LiveConnectorsRunSummary,
@@ -315,9 +307,6 @@ import {
315
307
  } from "./namespaces/catalog.js";
316
308
  import {
317
309
  planNamespaceMaintenance,
318
- runNamespaceMaintenanceBatchPlan,
319
- type NamespaceMaintenancePlan,
320
- type NamespaceMaintenanceSkipReason,
321
310
  type NamespaceMaintenanceSummary,
322
311
  } from "./maintenance/namespace-planner.js";
323
312
  import {
@@ -1804,13 +1793,6 @@ export function resolvePersistedMemoryRelativePath(options: {
1804
1793
  return path.join(subtree, `${options.memoryId}.md`);
1805
1794
  }
1806
1795
 
1807
- function qmdMaintenanceSkipReasonForError(error: unknown): NamespaceMaintenanceSkipReason | null {
1808
- const message = error instanceof Error ? error.message : String(error);
1809
- return /^QMD (?:update|embed) skipped by .*min-interval gate$/.test(message)
1810
- ? "throttled"
1811
- : null;
1812
- }
1813
-
1814
1796
  export class Orchestrator {
1815
1797
  readonly storage: StorageManager;
1816
1798
  private readonly storageRouter: NamespaceStorageRouter;
@@ -1820,6 +1802,12 @@ export class Orchestrator {
1820
1802
  private namespaceStorageDirHintsLoaded = false;
1821
1803
  private readonly namespaceSearchRouter: NamespaceSearchRouter;
1822
1804
  qmd: SearchBackend;
1805
+ /**
1806
+ * Maintenance scheduler (issue #1526 PR1). Owns cron auto-registration,
1807
+ * debounced QMD index maintenance, and consolidation scheduling. Job
1808
+ * runners (consolidation/pattern-reinforcement/governance) stay here.
1809
+ */
1810
+ readonly maintenanceScheduler: MaintenanceScheduler;
1823
1811
  private readonly conversationQmd?: ConversationQmdRuntime;
1824
1812
  private readonly conversationFaiss?: ReturnType<
1825
1813
  typeof createConversationIndexRuntime
@@ -1947,19 +1935,11 @@ export class Orchestrator {
1947
1935
  private queueProcessing = false;
1948
1936
  private heartbeatObserverChains = new Map<string, Promise<void>>();
1949
1937
  private recentExtractionFingerprints = new Map<string, number>();
1950
- private nonZeroExtractionsSinceConsolidation = 0;
1951
- private lastConsolidationRunAtMs = 0;
1952
- private consolidationInFlight = false;
1953
1938
  private readonly consolidationObservers = new Set<
1954
1939
  (observation: ConsolidationObservation) => Promise<void> | void
1955
1940
  >();
1956
- private qmdMaintenanceTimer: NodeJS.Timeout | null = null;
1957
1941
  private wearablesServiceInstance: WearablesService | null = null;
1958
1942
  private wearablesAutoSyncHandle: { stop(): Promise<void> } | null = null;
1959
- private qmdMaintenancePending = false;
1960
- private qmdMaintenanceInFlight = false;
1961
- private lastQmdEmbedAtMs = 0;
1962
- private lastQmdEmbedAtMsByNamespace = new Map<string, number>();
1963
1943
  private lastQmdReprobeAtMs = 0;
1964
1944
  private tierMigrationInFlight = false;
1965
1945
  private lastTierMigrationRunAtMs = 0;
@@ -2044,11 +2024,7 @@ export class Orchestrator {
2044
2024
  await this.wearablesAutoSyncHandle.stop();
2045
2025
  this.wearablesAutoSyncHandle = null;
2046
2026
  }
2047
- if (this.qmdMaintenanceTimer) {
2048
- clearTimeout(this.qmdMaintenanceTimer);
2049
- this.qmdMaintenanceTimer = null;
2050
- }
2051
- this.qmdMaintenancePending = false;
2027
+ this.maintenanceScheduler.dispose();
2052
2028
  await this.namespaceSearchRouter.dispose();
2053
2029
  await this.disposeSearchBackendIfNeeded();
2054
2030
  if (this.conversationQmd && this.conversationQmd !== this.qmd) {
@@ -2462,22 +2438,6 @@ export class Orchestrator {
2462
2438
  }
2463
2439
  }
2464
2440
 
2465
- /**
2466
- * Shared namespace maintenance planner (issue #1500). This extends the
2467
- * #1499 catalog-union QMD helper into a reusable contract: configured
2468
- * namespaces are always considered, dynamic catalog namespaces are admitted
2469
- * only when their live router root still matches real memory data, and branch
2470
- * namespaces are opt-in. Recurring jobs use the per-cycle budget; startup and
2471
- * recovery discovery paths use the same safety filters without that cycle
2472
- * budget so every live namespace is ensured/synced.
2473
- */
2474
- private async namespaceMaintenancePlan(jobName: string): Promise<NamespaceMaintenancePlan> {
2475
- return planNamespaceMaintenance(this.config, {
2476
- jobName,
2477
- catalog: this.namespaceCatalog,
2478
- });
2479
- }
2480
-
2481
2441
  private async maintenanceNamespaces(
2482
2442
  jobName = "qmd",
2483
2443
  budgetMode: "cycle" | "unbounded" = "unbounded",
@@ -2760,6 +2720,16 @@ export class Orchestrator {
2760
2720
  // and writes will throw SecureStoreLockedError via resolveWriteKey().
2761
2721
  }
2762
2722
  this.qmd = createSearchBackend(config);
2723
+ this.maintenanceScheduler = new MaintenanceScheduler({
2724
+ config,
2725
+ // Live accessor: the orchestrator reassigns this.qmd to NoopSearchBackend
2726
+ // after construction when the collection is missing (initialize /
2727
+ // startupSearchSync); the scheduler must read the current backend so
2728
+ // debounced maintenance never runs against a stale/disposed one.
2729
+ getQmd: () => this.qmd,
2730
+ namespaceSearchRouter: this.namespaceSearchRouter,
2731
+ namespaceCatalog: this.namespaceCatalog,
2732
+ });
2763
2733
  const conversationIndexRuntime = createConversationIndexRuntime(config, {
2764
2734
  getQmd: () => this.conversationQmd,
2765
2735
  getFaiss: () => this.conversationFaiss,
@@ -3540,56 +3510,10 @@ export class Orchestrator {
3540
3510
  // rely on cron jobs being registered when it resolves. Without this, the
3541
3511
  // fire-and-forget pattern lets deferredReady settle while cron writes are
3542
3512
  // still in flight. Errors are non-fatal — catch individually.
3543
- if (this.config.daySummaryEnabled) {
3544
- try {
3545
- await this.autoRegisterDaySummaryCron();
3546
- } catch (err) {
3547
- log.debug(`day-summary cron auto-register failed (non-fatal): ${err}`);
3548
- }
3549
- }
3550
- if (this.config.nightlyGovernanceCronAutoRegister) {
3551
- try {
3552
- await this.autoRegisterNightlyGovernanceCron();
3553
- } catch (err) {
3554
- log.debug(`nightly governance cron auto-register failed (non-fatal): ${err}`);
3555
- }
3556
- }
3557
- if (this.config.procedural?.proceduralMiningCronAutoRegister) {
3558
- try {
3559
- await this.autoRegisterProceduralMiningCron();
3560
- } catch (err) {
3561
- log.debug(`procedural mining cron auto-register failed (non-fatal): ${err}`);
3562
- }
3563
- }
3564
-
3565
- // Auto-register contradiction scan cron (gated by config)
3566
- if (this.config.contradictionScan?.enabled) {
3567
- try {
3568
- await this.autoRegisterContradictionScanCron();
3569
- } catch (err) {
3570
- log.debug(`contradiction scan cron auto-register failed (non-fatal): ${err}`);
3571
- }
3572
- }
3573
-
3574
- // Auto-register pattern-reinforcement cron (issue #687 PR 2/4).
3575
- // Gated on the feature flag so memory-only users without the
3576
- // cron daemon installed never see a stray jobs.json mutation.
3577
- if (this.config.patternReinforcementEnabled) {
3578
- try {
3579
- await this.autoRegisterPatternReinforcementCron();
3580
- } catch (err) {
3581
- log.debug(`pattern reinforcement cron auto-register failed (non-fatal): ${err}`);
3582
- }
3583
- }
3584
-
3585
- // Auto-register graph-edge decay cron (gated by config — issue #681 PR 2/3).
3586
- if (this.config.graphEdgeDecayEnabled) {
3587
- try {
3588
- await this.autoRegisterGraphEdgeDecayCron();
3589
- } catch (err) {
3590
- log.debug(`graph edge decay cron auto-register failed (non-fatal): ${err}`);
3591
- }
3592
- }
3513
+ // Auto-register every cron job in one pass. Each registration is gated
3514
+ // by its config flag inside the scheduler and individually non-fatal
3515
+ // (issue #1526 PR1 — moved to MaintenanceScheduler).
3516
+ await this.maintenanceScheduler.autoRegisterCrons(signal);
3593
3517
 
3594
3518
  // First-start lifecycle migration (issue #686 retention-completion).
3595
3519
  // When lifecyclePolicyEnabled is true and the memoryDir has never been
@@ -3819,168 +3743,6 @@ export class Orchestrator {
3819
3743
  return true;
3820
3744
  }
3821
3745
 
3822
- /**
3823
- * Auto-register the engram-day-summary cron job in OpenClaw.
3824
- * Reconciles model and timezone on every startup so config changes propagate.
3825
- * Fire-and-forget — never blocks init or crashes on failure.
3826
- * Issues #1474, #1475.
3827
- */
3828
- private async autoRegisterDaySummaryCron(): Promise<void> {
3829
- const home = resolveHomeDir();
3830
- const jobsPath = path.join(home, ".openclaw", "cron", "jobs.json");
3831
-
3832
- try {
3833
- if (!existsSync(jobsPath)) {
3834
- log.debug(
3835
- "day-summary cron: jobs.json not found, skipping auto-register",
3836
- );
3837
- return;
3838
- }
3839
-
3840
- // Resolve an OpenClaw cron-routing model only in gateway mode. In plugin
3841
- // mode, summaryModel is a direct-client model id for Remnic's own LLM
3842
- // calls and may be unroutable as an OpenClaw agentTurn model.
3843
- const rawSummaryModel = this.config.summaryModel;
3844
- const taskPrimary = this.config.taskModelChain?.primary;
3845
- const isGateway = this.config.modelSource === "gateway";
3846
- const model = isGateway ? (rawSummaryModel || taskPrimary || undefined) : undefined;
3847
- // Attach task-chain fallbacks only when the model matches the task-chain
3848
- // primary. If summaryModel is a distinct override, its fallbacks would
3849
- // be unrelated to the task chain. Also append gateway default models as
3850
- // tail fallbacks (de-duped) so a task-chain outage doesn't stop the cron
3851
- // before reaching the gateway default chain. Mirrors hourly cron pattern.
3852
- const fallbacks: string[] = [];
3853
- if (model && taskPrimary && model === taskPrimary) {
3854
- const seen = new Set<string>(model ? [model] : []);
3855
- const addUnique = (value: string | undefined) => {
3856
- if (typeof value !== "string") return;
3857
- const trimmed = value.trim();
3858
- if (trimmed.length > 0 && !seen.has(trimmed)) {
3859
- seen.add(trimmed);
3860
- fallbacks.push(trimmed);
3861
- }
3862
- };
3863
- for (const fb of this.config.taskModelChain?.fallbacks ?? []) addUnique(fb);
3864
- const gwDefaults = this.config.gatewayConfig?.agents?.defaults?.model;
3865
- addUnique(gwDefaults?.primary);
3866
- if (Array.isArray(gwDefaults?.fallbacks)) {
3867
- for (const fb of gwDefaults.fallbacks) addUnique(fb);
3868
- }
3869
- }
3870
-
3871
- // Resolve timezone: configurable override, then server default
3872
- const timezone = this.config.daySummaryTimezone
3873
- || Intl.DateTimeFormat().resolvedOptions().timeZone;
3874
-
3875
- const result = await ensureDaySummaryCron(jobsPath, {
3876
- timezone,
3877
- ...(model ? { model } : {}),
3878
- ...(fallbacks.length > 0 ? { fallbacks } : {}),
3879
- });
3880
- if (result.created) {
3881
- log.info(
3882
- `day-summary cron auto-registered (${result.jobId}, 23:47 ${timezone}${model ? `, model: ${model}` : ""})`,
3883
- );
3884
- } else if (result.updated) {
3885
- log.info(
3886
- `day-summary cron reconciled (${result.jobId}, timezone: ${timezone}${model ? `, model: ${model}` : ""})`,
3887
- );
3888
- } else {
3889
- log.debug("day-summary cron already up to date");
3890
- }
3891
- } catch (err) {
3892
- log.debug(`day-summary cron auto-register error: ${err}`);
3893
- }
3894
- }
3895
-
3896
- private async autoRegisterNightlyGovernanceCron(): Promise<void> {
3897
- const home = resolveHomeDir();
3898
- const jobsPath = path.join(home, ".openclaw", "cron", "jobs.json");
3899
-
3900
- try {
3901
- if (!existsSync(jobsPath)) {
3902
- log.debug("nightly governance cron: jobs.json not found, skipping auto-register");
3903
- return;
3904
- }
3905
-
3906
- const created = await ensureNightlyGovernanceCron(jobsPath, {
3907
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
3908
- });
3909
- if (created.created) {
3910
- log.info(
3911
- `nightly governance cron auto-registered (${created.jobId}, 02:23 ${Intl.DateTimeFormat().resolvedOptions().timeZone})`,
3912
- );
3913
- } else {
3914
- log.debug("nightly governance cron already exists, skipping auto-register");
3915
- }
3916
- } catch (err) {
3917
- log.debug(`nightly governance cron auto-register error: ${err}`);
3918
- }
3919
- }
3920
-
3921
- private async autoRegisterProceduralMiningCron(): Promise<void> {
3922
- const home = resolveHomeDir();
3923
- const jobsPath = path.join(home, ".openclaw", "cron", "jobs.json");
3924
- try {
3925
- if (!existsSync(jobsPath)) {
3926
- log.debug("procedural mining cron: jobs.json not found, skipping auto-register");
3927
- return;
3928
- }
3929
- const created = await ensureProceduralMiningCron(jobsPath, {
3930
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
3931
- });
3932
- if (created.created) {
3933
- log.info(`procedural mining cron auto-registered (${created.jobId})`);
3934
- } else {
3935
- log.debug("procedural mining cron already exists, skipping auto-register");
3936
- }
3937
- } catch (err) {
3938
- log.debug(`procedural mining cron auto-register error: ${err}`);
3939
- }
3940
- }
3941
-
3942
- private async autoRegisterContradictionScanCron(): Promise<void> {
3943
- const home = resolveHomeDir();
3944
- const jobsPath = path.join(home, ".openclaw", "cron", "jobs.json");
3945
- try {
3946
- if (!existsSync(jobsPath)) {
3947
- log.debug("contradiction scan cron: jobs.json not found, skipping auto-register");
3948
- return;
3949
- }
3950
- const created = await ensureContradictionScanCron(jobsPath, {
3951
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
3952
- });
3953
- if (created.created) {
3954
- log.info(`contradiction scan cron auto-registered (${created.jobId})`);
3955
- } else {
3956
- log.debug("contradiction scan cron already exists, skipping auto-register");
3957
- }
3958
- } catch (err) {
3959
- log.debug(`contradiction scan cron auto-register error: ${err}`);
3960
- }
3961
- }
3962
-
3963
- private async autoRegisterPatternReinforcementCron(): Promise<void> {
3964
- const home = resolveHomeDir();
3965
- const jobsPath = path.join(home, ".openclaw", "cron", "jobs.json");
3966
- try {
3967
- if (!existsSync(jobsPath)) {
3968
- log.debug("pattern reinforcement cron: jobs.json not found, skipping auto-register");
3969
- return;
3970
- }
3971
- const created = await ensurePatternReinforcementCron(jobsPath, {
3972
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
3973
- });
3974
- if (created.created) {
3975
- log.info(`pattern reinforcement cron auto-registered (${created.jobId})`);
3976
- } else {
3977
- log.debug("pattern reinforcement cron already exists, skipping auto-register");
3978
- }
3979
- } catch (err) {
3980
- log.debug(`pattern reinforcement cron auto-register error: ${err}`);
3981
- }
3982
- }
3983
-
3984
3746
  /**
3985
3747
  * Run the pattern-reinforcement maintenance job (issue #687 PR 2/4).
3986
3748
  *
@@ -4142,29 +3904,6 @@ export class Orchestrator {
4142
3904
  );
4143
3905
  }
4144
3906
 
4145
- private async autoRegisterGraphEdgeDecayCron(): Promise<void> {
4146
- const home = resolveHomeDir();
4147
- const jobsPath = path.join(home, ".openclaw", "cron", "jobs.json");
4148
- try {
4149
- if (!existsSync(jobsPath)) {
4150
- log.debug("graph edge decay cron: jobs.json not found, skipping auto-register");
4151
- return;
4152
- }
4153
- const scheduleExpr = graphEdgeDecayCadenceToCronExpr(this.config.graphEdgeDecayCadenceMs);
4154
- const created = await ensureGraphEdgeDecayCron(jobsPath, {
4155
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
4156
- scheduleExpr,
4157
- });
4158
- if (created.created) {
4159
- log.info(`graph edge decay cron auto-registered (${created.jobId}, ${scheduleExpr})`);
4160
- } else {
4161
- log.debug("graph edge decay cron already exists, skipping auto-register");
4162
- }
4163
- } catch (err) {
4164
- log.debug(`graph edge decay cron auto-register error: ${err}`);
4165
- }
4166
- }
4167
-
4168
3907
  async runLiveConnectors(options: {
4169
3908
  force?: boolean;
4170
3909
  abortSignal?: AbortSignal;
@@ -4718,7 +4457,7 @@ export class Orchestrator {
4718
4457
 
4719
4458
  async waitForConsolidationIdle(timeoutMs: number = 60_000): Promise<boolean> {
4720
4459
  const started = Date.now();
4721
- while (this.consolidationInFlight) {
4460
+ while (this.maintenanceScheduler.isConsolidationInFlight()) {
4722
4461
  if (Date.now() - started > timeoutMs) {
4723
4462
  log.warn(`waitForConsolidationIdle timed out after ${timeoutMs}ms`);
4724
4463
  return false;
@@ -13657,7 +13396,8 @@ export class Orchestrator {
13657
13396
  // Check if consolidation is needed (debounced + non-zero gated).
13658
13397
  const nonZeroExtraction = durableOutputCount > 0;
13659
13398
  try {
13660
- if (nonZeroExtraction) this.nonZeroExtractionsSinceConsolidation += 1;
13399
+ // The increment of nonZeroExtractionsSinceConsolidation moved into
13400
+ // the scheduler with the rest of the cadence state (issue #1526 PR1).
13661
13401
  this.maybeScheduleConsolidation(nonZeroExtraction);
13662
13402
  } catch (err) {
13663
13403
  log.warn(
@@ -13928,148 +13668,22 @@ export class Orchestrator {
13928
13668
  }
13929
13669
 
13930
13670
  private maybeScheduleConsolidation(nonZeroExtraction: boolean): void {
13931
- if (this.config.consolidationRequireNonZeroExtraction && !nonZeroExtraction)
13932
- return;
13933
- if (
13934
- this.nonZeroExtractionsSinceConsolidation < this.config.consolidateEveryN
13935
- )
13936
- return;
13937
-
13938
- const now = Date.now();
13939
- if (
13940
- now - this.lastConsolidationRunAtMs <
13941
- this.config.consolidationMinIntervalMs
13942
- )
13943
- return;
13944
- if (this.consolidationInFlight) return;
13945
-
13946
- this.consolidationInFlight = true;
13947
- this.lastConsolidationRunAtMs = now;
13948
- this.nonZeroExtractionsSinceConsolidation = 0;
13949
- this.runConsolidation()
13950
- .catch((err) => log.error("background consolidation failed", err))
13951
- .finally(() => {
13952
- this.consolidationInFlight = false;
13953
- });
13671
+ this.maintenanceScheduler.maybeScheduleConsolidation(
13672
+ nonZeroExtraction,
13673
+ () => this.runConsolidation(),
13674
+ );
13954
13675
  }
13955
13676
 
13956
- private requestQmdMaintenance(): void {
13957
- if (!this.qmd.isAvailable()) return;
13958
- if (!this.config.qmdMaintenanceEnabled) return;
13959
-
13960
- this.qmdMaintenancePending = true;
13961
- if (this.qmdMaintenanceTimer) return;
13962
-
13963
- this.qmdMaintenanceTimer = setTimeout(() => {
13964
- this.qmdMaintenanceTimer = null;
13965
- this.runQmdMaintenance().catch((err) =>
13966
- log.debug(`background qmd maintenance failed: ${err}`),
13967
- );
13968
- }, this.config.qmdMaintenanceDebounceMs);
13677
+ requestQmdMaintenance(): void {
13678
+ this.maintenanceScheduler.requestQmdMaintenanceForTool("__internal__");
13969
13679
  }
13970
13680
 
13971
13681
  /**
13972
13682
  * Public entrypoint for tool-driven QMD maintenance requests.
13973
- * Routes through existing debounced/singleflight maintenance controls.
13683
+ * Delegates to the maintenance scheduler (issue #1526 PR1).
13974
13684
  */
13975
13685
  requestQmdMaintenanceForTool(reason: string): void {
13976
- try {
13977
- this.requestQmdMaintenance();
13978
- } catch (err) {
13979
- const msg = err instanceof Error ? err.message : String(err);
13980
- log.warn(`qmd maintenance request failed (${reason}): ${msg}`);
13981
- }
13982
- }
13983
-
13984
- private async runQmdMaintenance(): Promise<void> {
13985
- if (this.qmdMaintenanceInFlight) return;
13986
- if (!this.qmdMaintenancePending) return;
13987
- this.qmdMaintenanceInFlight = true;
13988
- this.qmdMaintenancePending = false;
13989
-
13990
- try {
13991
- if (this.config.namespacesEnabled) {
13992
- // Include cataloged dynamic namespaces, not just the configured set
13993
- // (NGnei), but run through the namespace-aware maintenance planner so
13994
- // each namespace is budgeted, lock-protected, and status-recorded
13995
- // independently (issue #1500).
13996
- const plan = await this.namespaceMaintenancePlan("qmd");
13997
- const now = Date.now();
13998
- const lastEmbedAtByNamespace =
13999
- this.lastQmdEmbedAtMsByNamespace ?? (this.lastQmdEmbedAtMsByNamespace = new Map());
14000
- const dueEmbedNamespaces = (namespaces: string[]): string[] => {
14001
- if (!this.config.qmdAutoEmbedEnabled) return [];
14002
- return namespaces.filter(
14003
- (namespace) =>
14004
- now - (lastEmbedAtByNamespace.get(namespace) ?? 0) >= this.config.qmdEmbedMinIntervalMs,
14005
- );
14006
- };
14007
- const markEmbedded = (namespaces: string[]): void => {
14008
- if (namespaces.length === 0) return;
14009
- for (const namespace of namespaces) {
14010
- lastEmbedAtByNamespace.set(namespace, now);
14011
- }
14012
- this.lastQmdEmbedAtMs = now;
14013
- };
14014
- await runNamespaceMaintenanceBatchPlan(
14015
- this.config,
14016
- plan,
14017
- async (candidates) => {
14018
- const namespaces = candidates.map((candidate) => candidate.namespace);
14019
- const embedNamespaces = dueEmbedNamespaces(namespaces);
14020
- let result: Awaited<ReturnType<NamespaceSearchRouter["updateNamespacesDetailed"]>>;
14021
- try {
14022
- result = await this.namespaceSearchRouter.updateNamespacesDetailed(
14023
- namespaces,
14024
- undefined,
14025
- { strict: true },
14026
- );
14027
- } catch (error) {
14028
- if (
14029
- embedNamespaces.length > 0 &&
14030
- qmdMaintenanceSkipReasonForError(error) === "throttled"
14031
- ) {
14032
- await this.namespaceSearchRouter.embedNamespaces(embedNamespaces, { strict: true });
14033
- markEmbedded(embedNamespaces);
14034
- }
14035
- throw error;
14036
- }
14037
- if (result.backendCount <= 0) {
14038
- throw new Error("no eligible QMD backend for selected namespaces");
14039
- }
14040
- if (result.eligibleNamespaces.length !== namespaces.length) {
14041
- const eligible = new Set(result.eligibleNamespaces);
14042
- const missing = namespaces.filter((namespace) => !eligible.has(namespace));
14043
- throw new Error(`QMD backend ineligible for selected namespaces (${missing.length})`);
14044
- }
14045
- if (embedNamespaces.length > 0) {
14046
- await this.namespaceSearchRouter.embedNamespaces(embedNamespaces, { strict: true });
14047
- markEmbedded(embedNamespaces);
14048
- }
14049
- return { itemCount: result.backendCount };
14050
- },
14051
- this.namespaceCatalog,
14052
- {
14053
- skipReasonForError: qmdMaintenanceSkipReasonForError,
14054
- },
14055
- );
14056
- } else {
14057
- await this.qmd.update();
14058
- const now = Date.now();
14059
- if (
14060
- this.config.qmdAutoEmbedEnabled &&
14061
- now - this.lastQmdEmbedAtMs >= this.config.qmdEmbedMinIntervalMs
14062
- ) {
14063
- await this.qmd.embed();
14064
- this.lastQmdEmbedAtMs = now;
14065
- }
14066
- }
14067
- } finally {
14068
- this.qmdMaintenanceInFlight = false;
14069
- if (this.qmdMaintenancePending) {
14070
- this.requestQmdMaintenance();
14071
- }
14072
- }
13686
+ this.maintenanceScheduler.requestQmdMaintenanceForTool(reason);
14073
13687
  }
14074
13688
 
14075
13689
  private async persistExtraction(