@elvisw/dsh-magic-context 0.0.0-stage → 0.14.0
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/LICENSE +23 -0
- package/NOTICE +93 -0
- package/README.md +501 -2
- package/README.zh-CN.md +398 -0
- package/cordis.patch.yml +20 -0
- package/lib/client.js +2753 -0
- package/lib/core/compartments.js +424 -0
- package/lib/core/corpus-store.js +176 -0
- package/lib/core/directive-detect.js +147 -0
- package/lib/core/domain-reconcile.js +394 -0
- package/lib/core/domains.js +127 -0
- package/lib/core/embedding-local.js +247 -0
- package/lib/core/global-autostage.js +223 -0
- package/lib/core/global-domains.js +302 -0
- package/lib/core/journal.js +248 -0
- package/lib/core/llm-deadline.js +53 -0
- package/lib/core/machine-domain.js +106 -0
- package/lib/core/maintenance.js +380 -0
- package/lib/core/memory-bridge.js +818 -0
- package/lib/core/memory-categories.js +84 -0
- package/lib/core/memory-scoring.js +65 -0
- package/lib/core/memory-search.js +414 -0
- package/lib/core/memory-write.js +1176 -0
- package/lib/core/policy.js +111 -0
- package/lib/core/profile-confirmation.js +55 -0
- package/lib/core/profile-review.js +375 -0
- package/lib/core/profile-write.js +283 -0
- package/lib/core/reference-seeds.js +63 -0
- package/lib/core/route-health.js +106 -0
- package/lib/core/stores.js +1021 -0
- package/lib/core/tagger.js +7 -0
- package/lib/core/types.js +15 -0
- package/lib/core/vector-backfill.js +140 -0
- package/lib/core/vector-lane.js +184 -0
- package/lib/dsh/command-surface.js +376 -0
- package/lib/dsh/compaction-engine.js +231 -0
- package/lib/dsh/config-switches.js +56 -0
- package/lib/dsh/config.js +13 -0
- package/lib/dsh/context-pressure.js +36 -0
- package/lib/dsh/drop-landing.js +42 -0
- package/lib/dsh/emergency.js +89 -0
- package/lib/dsh/flush-command.js +148 -0
- package/lib/dsh/global-review.js +412 -0
- package/lib/dsh/historian.js +907 -0
- package/lib/dsh/live-journal.js +81 -0
- package/lib/dsh/maintenance-wiring.js +229 -0
- package/lib/dsh/management-pass.js +564 -0
- package/lib/dsh/memory-lane.js +535 -0
- package/lib/dsh/pre-step.js +675 -0
- package/lib/dsh/project-drift.js +116 -0
- package/lib/dsh/recomp.js +192 -0
- package/lib/dsh/reduce-guidance.js +188 -0
- package/lib/dsh/seams.js +588 -0
- package/lib/dsh/search-hint.js +186 -0
- package/lib/dsh/session-notice.js +100 -0
- package/lib/dsh/status-command.js +75 -0
- package/lib/dsh/telemetry.js +349 -0
- package/lib/dsh/tool-bridge.js +1009 -0
- package/lib/dsh/volatile.js +89 -0
- package/lib/index.js +931 -0
- package/package.json +108 -4
- package/tools/migrate-profile-package-name.mjs +317 -0
- package/vendor/onnx/LICENSE +27 -0
- package/vendor/onnx/ort-wasm-simd-threaded.mjs +59 -0
- package/vendor/onnx/ort-wasm-simd-threaded.wasm +0 -0
- package/vendor/onnx/ort.node.min.mjs +8 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live journal wrapper — why it exists (measured on this adapter):
|
|
3
|
+
*
|
|
4
|
+
* `journal.*` leaves are declared `.volatile()` so the settings form can edit
|
|
5
|
+
* them, but a volatile-only commit NEVER remounts the plugin — and the journal
|
|
6
|
+
* used to be constructed once inside `apply()`, so every form edit saved
|
|
7
|
+
* cleanly and then did nothing until the next app start (the "dead option"
|
|
8
|
+
* audit, 2026-10-10). The wrapper moves the open/close and the knob reads to
|
|
9
|
+
* use time:
|
|
10
|
+
*
|
|
11
|
+
* - `enabled`: false stops recording immediately; true (re)opens the journal
|
|
12
|
+
* lazily on the next write.
|
|
13
|
+
* - `level` / `mirror`: handed to `openJournal` as GETTERS, so an edit
|
|
14
|
+
* applies to the very next entry (no reopen).
|
|
15
|
+
* - `retentionDays`: the prune that used to run only at open re-runs the
|
|
16
|
+
* first time a write observes the configured value changed (floored at 1
|
|
17
|
+
* day so a stray manual-YAML zero cannot prune the whole table).
|
|
18
|
+
*
|
|
19
|
+
* One asymmetry is deliberate: when the journal is disabled at BOOT the
|
|
20
|
+
* adapter still wires `journal: undefined` (see index.ts) — the maintenance
|
|
21
|
+
* tick treats a missing journal as "do not run", and a boot-time opt-out must
|
|
22
|
+
* keep meaning exactly that. This wrapper only serves compositions that
|
|
23
|
+
* booted with the journal on.
|
|
24
|
+
*/
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { openJournal } from "../core/journal.js";
|
|
27
|
+
import { liveBoolean, liveNumber, liveString } from "./volatile.js";
|
|
28
|
+
import { errText } from "../core/types.js";
|
|
29
|
+
export function liveJournal(options) {
|
|
30
|
+
const { config, stores, storeDir, logger } = options;
|
|
31
|
+
const desiredRetention = () => Math.max(1, liveNumber(config.journal?.retentionDays, 30));
|
|
32
|
+
let inner;
|
|
33
|
+
let lastRetention;
|
|
34
|
+
/** (Re)open the journal lazily; re-prune when the retention value changed. */
|
|
35
|
+
const ensure = () => {
|
|
36
|
+
if (inner === undefined) {
|
|
37
|
+
inner = openJournal({
|
|
38
|
+
stores,
|
|
39
|
+
level: () => liveString(config.journal?.level, 'info'),
|
|
40
|
+
mirrorPath: () => liveBoolean(config.journal?.mirror, true)
|
|
41
|
+
? join(storeDir, 'activity.log')
|
|
42
|
+
: undefined,
|
|
43
|
+
retentionDays: desiredRetention(),
|
|
44
|
+
logger,
|
|
45
|
+
});
|
|
46
|
+
lastRetention = desiredRetention();
|
|
47
|
+
return inner;
|
|
48
|
+
}
|
|
49
|
+
const retention = desiredRetention();
|
|
50
|
+
if (retention !== lastRetention) {
|
|
51
|
+
lastRetention = retention;
|
|
52
|
+
try {
|
|
53
|
+
const pruned = stores.pruneJournal(Date.now() - retention * 24 * 60 * 60_000);
|
|
54
|
+
if (pruned > 0) {
|
|
55
|
+
logger?.info(`magic-context journal: pruned ${String(pruned)} entries past ${String(retention)}d retention (retention edited at runtime)`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
logger?.warn(`magic-context journal: retention re-prune failed: ${errText(error)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return inner;
|
|
63
|
+
};
|
|
64
|
+
const record = (level, kind, summary, data, sessionId) => {
|
|
65
|
+
if (liveBoolean(config.journal?.enabled, true) === false)
|
|
66
|
+
return;
|
|
67
|
+
ensure().record(level, kind, summary, data, sessionId);
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
get enabled() { return liveBoolean(config.journal?.enabled, true) !== false; },
|
|
71
|
+
record,
|
|
72
|
+
info: (kind, summary, data, sessionId) => record('info', kind, summary, data, sessionId),
|
|
73
|
+
warn: (kind, summary, data, sessionId) => record('warn', kind, summary, data, sessionId),
|
|
74
|
+
error: (kind, summary, data, sessionId) => record('error', kind, summary, data, sessionId),
|
|
75
|
+
tail: (limit = 50, sessionId) => inner?.tail(limit, sessionId) ?? [],
|
|
76
|
+
byKind: since => inner?.byKind(since) ?? [],
|
|
77
|
+
get mirrorPath() {
|
|
78
|
+
return liveBoolean(config.journal?.mirror, true) ? join(storeDir, 'activity.log') : undefined;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { memorySample, runMaintenanceTick } from "../core/maintenance.js";
|
|
2
|
+
import { PROFILE_REVIEW_META_KEY, runProfileReview } from "../core/profile-review.js";
|
|
3
|
+
import { quotaSkipSince, resolveMaintenanceRoute, ROUTE_QUOTA_HOLD_MS, RouteHealth } from "../core/route-health.js";
|
|
4
|
+
/** WP11 default batch: how many vectors one maintenance tick may add. */
|
|
5
|
+
export const VECTOR_BACKFILL_DEFAULT_LIMIT = 24;
|
|
6
|
+
/**
|
|
7
|
+
* WP11 gate. Default TRUE since 2026-10-12: the memory write path never embeds,
|
|
8
|
+
* so with the lane off every NEW memory stayed vector-less forever and Chinese
|
|
9
|
+
* semantic recall (the only channel with real recall on this corpus — FTS with
|
|
10
|
+
* unicode61 hits ~18%) silently decayed as rows accumulated. The vectors land
|
|
11
|
+
* in the machine's own single-writer store, bounded by
|
|
12
|
+
* `vectorBackfillLimit` per tick and gated by the shared-space parity check
|
|
13
|
+
* (a closed gate aborts the whole batch). Set the key to `false` to keep the
|
|
14
|
+
* shared store free of writes.
|
|
15
|
+
*/
|
|
16
|
+
export function vectorBackfillEnabled(memory) {
|
|
17
|
+
return liveBoolean(memory?.vectorBackfill, true);
|
|
18
|
+
}
|
|
19
|
+
/** Live maintenance tickers keyed by adapter DB path — a same-DB remount replaces its predecessor (see wireMaintenance). */
|
|
20
|
+
const maintenanceTickers = new Map();
|
|
21
|
+
import { errText } from "../core/types.js";
|
|
22
|
+
import { autoStageGlobalCandidates } from "../core/global-autostage.js";
|
|
23
|
+
import { liveBoolean, liveNumber, liveString } from "./volatile.js";
|
|
24
|
+
export function wireMaintenance(input) {
|
|
25
|
+
const { ctx, config, stores, journal, llm, writer, profileWriter, vectorBackfill, routeHealth, lastSessionRoute, projects, disposers, isDisposed } = input;
|
|
26
|
+
const { seenIdentities } = projects;
|
|
27
|
+
let maintenanceLast;
|
|
28
|
+
let profileReviewLast;
|
|
29
|
+
let vectorBackfillLast;
|
|
30
|
+
let maintenanceBusy = false;
|
|
31
|
+
let maintenanceBusySince;
|
|
32
|
+
const runMaintenance = async () => {
|
|
33
|
+
if (maintenanceBusy || isDisposed() || stores.closed || journal === undefined)
|
|
34
|
+
return;
|
|
35
|
+
if (liveBoolean(config.maintenance?.enabled, true) === false)
|
|
36
|
+
return;
|
|
37
|
+
maintenanceBusy = true;
|
|
38
|
+
maintenanceBusySince = Date.now();
|
|
39
|
+
try {
|
|
40
|
+
// Process memory, once per tick (2026-10-15). Upstream MC 0.49.1 (#681)
|
|
41
|
+
// leaked in exactly this shape — each completed pass kept the previous pass
|
|
42
|
+
// and its full message history; our static audit found no such retention,
|
|
43
|
+
// so this series is what would make a future regression visible across days
|
|
44
|
+
// (no profiler, no LLM call, one journal row per tick).
|
|
45
|
+
const memoryNow = memorySample(process.memoryUsage());
|
|
46
|
+
journal.info('memory-sample', `rss ${String(memoryNow.rssMb)} MB · heap ${String(memoryNow.heapUsedMb)}/${String(memoryNow.heapTotalMb)} MB`, memoryNow);
|
|
47
|
+
// The route every LLM step of this tick uses, resolved ONCE: the explicit
|
|
48
|
+
// pin wins (rollback lever), otherwise the main conversation's latest
|
|
49
|
+
// route. A quota observation skips the LLM steps ONLY when the tick is
|
|
50
|
+
// about to ride the main conversation's own exact route — the event
|
|
51
|
+
// carries a provider but no model, and quotas are route-scoped, so a
|
|
52
|
+
// provider-keyed skip would stall a live sibling model (2026-10-09:
|
|
53
|
+
// model-a exhausted while model-b verified normally).
|
|
54
|
+
// One info line, zero requests; the observation is volatile and owned by
|
|
55
|
+
// real traffic, so an externally reset quota re-opens the lane within one
|
|
56
|
+
// hold at the cost of one cheap probe.
|
|
57
|
+
const pinnedProvider = liveString(config.maintenance?.verifyProvider, '');
|
|
58
|
+
const pinnedModel = liveString(config.maintenance?.verifyModel, '');
|
|
59
|
+
const tickRoute = resolveMaintenanceRoute(pinnedProvider.length > 0 && pinnedModel.length > 0 ? { provider: pinnedProvider, model: pinnedModel } : undefined, lastSessionRoute());
|
|
60
|
+
const tickQuotaAge = quotaSkipSince(routeHealth, tickRoute, lastSessionRoute(), Date.now());
|
|
61
|
+
if (tickRoute !== undefined && tickQuotaAge !== undefined) {
|
|
62
|
+
journal.info('maintenance', `LLM steps skipped: ${tickRoute.provider}/${tickRoute.model} observed quota-exhausted ${String(Math.round(tickQuotaAge / 60_000))}m ago (hold ${String(Math.round(ROUTE_QUOTA_HOLD_MS / 60_000))}m)`, { provider: tickRoute.provider, model: tickRoute.model, quotaAgeMs: tickQuotaAge, route: `${tickRoute.provider}/${tickRoute.model}` });
|
|
63
|
+
}
|
|
64
|
+
const tickLlmReady = llm !== undefined && tickRoute !== undefined && tickQuotaAge === undefined;
|
|
65
|
+
const identities = new Set();
|
|
66
|
+
if (config.memory?.project !== undefined)
|
|
67
|
+
identities.add(config.memory.project);
|
|
68
|
+
for (const identity of seenIdentities)
|
|
69
|
+
identities.add(identity);
|
|
70
|
+
for (const identity of identities) {
|
|
71
|
+
const outcome = await runMaintenanceTick({
|
|
72
|
+
writer,
|
|
73
|
+
identity,
|
|
74
|
+
sessionId: 'mc-maintenance',
|
|
75
|
+
journal,
|
|
76
|
+
...(tickLlmReady && tickRoute !== undefined && llm !== undefined
|
|
77
|
+
? { llm: llm, route: tickRoute }
|
|
78
|
+
: {}),
|
|
79
|
+
});
|
|
80
|
+
// A writer-level problem applies to every identity — stop the sweep.
|
|
81
|
+
if (outcome.reason === 'writer-unavailable' || outcome.reason === 'read-only')
|
|
82
|
+
break;
|
|
83
|
+
maintenanceLast = {
|
|
84
|
+
at: Date.now(),
|
|
85
|
+
expired: outcome.expired,
|
|
86
|
+
verified: outcome.verified,
|
|
87
|
+
updated: outcome.updated,
|
|
88
|
+
archived: outcome.archived,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// L1① v10: classify-then-route global auto-stage. The classifier needs
|
|
92
|
+
// the SAME LLM seam as the verify lane (route, model, session id), so it
|
|
93
|
+
// rides the tick's route. Fail-closed: no route (or the pin route on
|
|
94
|
+
// quota-hold) means NOTHING is staged — the memories stay at project
|
|
95
|
+
// level, never a guessed fallback domain.
|
|
96
|
+
if (liveBoolean(config.memory?.global?.machine?.autoStage, true)
|
|
97
|
+
&& tickLlmReady && tickRoute !== undefined && llm !== undefined && writer !== undefined) {
|
|
98
|
+
const outcome = await autoStageGlobalCandidates({
|
|
99
|
+
store: stores,
|
|
100
|
+
corpusDbPath: writer.dbPath,
|
|
101
|
+
now: Date.now(),
|
|
102
|
+
llm: llm,
|
|
103
|
+
route: tickRoute,
|
|
104
|
+
});
|
|
105
|
+
if (outcome.staged.length > 0) {
|
|
106
|
+
journal.info('memory-promote', `auto-staged ${String(outcome.staged.length)} global candidate(s): ${outcome.staged
|
|
107
|
+
.map(entry => `#${String(entry.candidateId)}(memory ${String(entry.memoryId)} → ${entry.domainId})`)
|
|
108
|
+
.join(', ')}`, { phase: 'auto-stage', staged: outcome.staged, skipped: outcome.skipped });
|
|
109
|
+
}
|
|
110
|
+
if (outcome.reason !== undefined) {
|
|
111
|
+
journal.info('memory-promote', `auto-stage paused: ${outcome.reason} (memories stay at project level)`, { phase: 'auto-stage', reason: outcome.reason });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// WP7: the `user` domain's review. It is GLOBAL — one candidate pool, one
|
|
115
|
+
// profile — so it runs once per tick rather than per identity, and it is
|
|
116
|
+
// deliberately outside the loop's `break` (the memory writer failing says
|
|
117
|
+
// nothing about the profile tables, which live in the same file but are
|
|
118
|
+
// reached through their own connection).
|
|
119
|
+
//
|
|
120
|
+
// The throttle is a stored timestamp because DSH has no daily scheduler to
|
|
121
|
+
// hang MC's `review-user-memories` cadence on. It is written when the pass
|
|
122
|
+
// reaches a DECIDED outcome — a run, or a nothing-to-do skip — which turns
|
|
123
|
+
// those skips into a once-per-day heartbeat row instead of per-tick noise
|
|
124
|
+
// (or per-tick silence). A FAILED call is never stamped, so it is retried
|
|
125
|
+
// on the next tick instead of being silenced for a day by its own failure.
|
|
126
|
+
if (liveBoolean(config.memory?.global?.user?.review, true) && profileWriter !== undefined) {
|
|
127
|
+
const lastReviewRaw = Number(stores.getMeta(PROFILE_REVIEW_META_KEY) ?? '');
|
|
128
|
+
const review = await runProfileReview({
|
|
129
|
+
writer: profileWriter,
|
|
130
|
+
sessionId: 'mc-maintenance',
|
|
131
|
+
journal,
|
|
132
|
+
...(tickLlmReady && tickRoute !== undefined && llm !== undefined
|
|
133
|
+
? { llm: llm, route: tickRoute }
|
|
134
|
+
: {}),
|
|
135
|
+
...(Number.isFinite(lastReviewRaw) && lastReviewRaw > 0 ? { lastReviewAt: lastReviewRaw } : {}),
|
|
136
|
+
});
|
|
137
|
+
if (review.attempted === true)
|
|
138
|
+
stores.setMeta(PROFILE_REVIEW_META_KEY, String(Date.now()));
|
|
139
|
+
profileReviewLast = review;
|
|
140
|
+
}
|
|
141
|
+
// WP11: fill the vector lane. Also global, also once per tick, and on by
|
|
142
|
+
// default since 2026-10-12 (see {@link vectorBackfillEnabled} for the
|
|
143
|
+
// reasoning). `vectorBackfill` returns undefined when the lane is
|
|
144
|
+
// unusable (no reader/embedder/writer), which is reported as "not run"
|
|
145
|
+
// rather than silently looking like "nothing missing".
|
|
146
|
+
if (vectorBackfillEnabled(config.memory) && vectorBackfill !== undefined) {
|
|
147
|
+
const limit = Math.max(1, Math.floor(config.memory?.vectorBackfillLimit ?? VECTOR_BACKFILL_DEFAULT_LIMIT));
|
|
148
|
+
const outcome = await vectorBackfill(limit, journal);
|
|
149
|
+
if (outcome === undefined) {
|
|
150
|
+
journal.info('vector-lane', 'backfill lane unavailable (no embedder or no writable store)', {
|
|
151
|
+
outcome: 'backfill-unavailable',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
vectorBackfillLast = outcome;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
journal.error('maintenance', `maintenance tick failed: ${errText(error)}`);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
maintenanceBusy = false;
|
|
164
|
+
maintenanceBusySince = undefined;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
// One live ticker per adapter store, process-wide. A remount that arrives
|
|
168
|
+
// without its predecessor's disposal (the GUI-reconnect reload path) used to
|
|
169
|
+
// STACK a second 60s first-tick and a second interval on the same DB —
|
|
170
|
+
// measured at 6 ticks/75 minutes. The registry, keyed by `dbPath`, replaces
|
|
171
|
+
// the live ticker instead: a same-DB remount takes over, different-DB mounts
|
|
172
|
+
// (tests with separate data dirs) stay independent.
|
|
173
|
+
const predecessor = maintenanceTickers.get(stores.dbPath);
|
|
174
|
+
if (predecessor !== undefined)
|
|
175
|
+
predecessor();
|
|
176
|
+
let stopped = false;
|
|
177
|
+
let maintenanceTimer;
|
|
178
|
+
const stopTicker = () => {
|
|
179
|
+
if (stopped)
|
|
180
|
+
return;
|
|
181
|
+
stopped = true;
|
|
182
|
+
if (maintenanceTickers.get(stores.dbPath) === stopTicker)
|
|
183
|
+
maintenanceTickers.delete(stores.dbPath);
|
|
184
|
+
if (maintenanceTimer !== undefined)
|
|
185
|
+
clearInterval(maintenanceTimer);
|
|
186
|
+
clearTimeout(maintenanceFirst);
|
|
187
|
+
};
|
|
188
|
+
// The schedule is installed by its OWN timer, not from the first tick's
|
|
189
|
+
// `finally` (2026-11-11). The old shape — `runMaintenance().finally(() =>
|
|
190
|
+
// setInterval)` — meant that one tick which never resolved also never
|
|
191
|
+
// installed the schedule, so TTL expiry, verification, profile review and the
|
|
192
|
+
// vector lane all stopped for the life of the process, silently (the panel
|
|
193
|
+
// only ever showed "never ran"). The tick's LLM calls now carry wall-clock
|
|
194
|
+
// budgets (core/llm-deadline.ts), and the schedule itself no longer depends on
|
|
195
|
+
// any single tick returning. Cadence is unchanged: first tick at 60s, then one
|
|
196
|
+
// per `intervalMs`. A tick that still outlives its own interval is a bug, and
|
|
197
|
+
// is journaled below instead of hiding.
|
|
198
|
+
const intervalMs = Math.max(60_000, liveNumber(config.maintenance?.intervalMs, 1_800_000));
|
|
199
|
+
let stallWarned = false;
|
|
200
|
+
const installInterval = () => {
|
|
201
|
+
if (stopped || maintenanceTimer !== undefined)
|
|
202
|
+
return;
|
|
203
|
+
maintenanceTimer = setInterval(() => {
|
|
204
|
+
if (maintenanceBusy) {
|
|
205
|
+
if (!stallWarned && maintenanceBusySince !== undefined && Date.now() - maintenanceBusySince > intervalMs) {
|
|
206
|
+
stallWarned = true;
|
|
207
|
+
journal?.warn('maintenance', `maintenance tick has been running for over ${String(Math.round((Date.now() - maintenanceBusySince) / 60_000))}m — later ticks hold off until it returns`, { stalledMs: Date.now() - maintenanceBusySince });
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
stallWarned = false;
|
|
212
|
+
void runMaintenance();
|
|
213
|
+
}, intervalMs);
|
|
214
|
+
maintenanceTimer.unref?.();
|
|
215
|
+
};
|
|
216
|
+
const maintenanceFirst = setTimeout(() => {
|
|
217
|
+
// Install first: the schedule must survive a tick that never settles.
|
|
218
|
+
installInterval();
|
|
219
|
+
void runMaintenance();
|
|
220
|
+
}, 60_000);
|
|
221
|
+
maintenanceFirst.unref?.();
|
|
222
|
+
maintenanceTickers.set(stores.dbPath, stopTicker);
|
|
223
|
+
disposers.push(stopTicker);
|
|
224
|
+
return {
|
|
225
|
+
maintenanceLast: () => maintenanceLast,
|
|
226
|
+
profileReviewLast: () => profileReviewLast,
|
|
227
|
+
vectorBackfillLast: () => vectorBackfillLast,
|
|
228
|
+
};
|
|
229
|
+
}
|