@elvisw/dsh-magic-context 0.0.0-stage → 0.13.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.
Files changed (66) hide show
  1. package/LICENSE +23 -0
  2. package/NOTICE +93 -0
  3. package/README.md +452 -2
  4. package/README.zh-CN.md +368 -0
  5. package/cordis.patch.yml +20 -0
  6. package/lib/client.js +2270 -0
  7. package/lib/core/compartments.js +424 -0
  8. package/lib/core/corpus-store.js +176 -0
  9. package/lib/core/directive-detect.js +147 -0
  10. package/lib/core/domain-reconcile.js +394 -0
  11. package/lib/core/domains.js +127 -0
  12. package/lib/core/embedding-local.js +247 -0
  13. package/lib/core/global-autostage.js +223 -0
  14. package/lib/core/global-domains.js +302 -0
  15. package/lib/core/journal.js +248 -0
  16. package/lib/core/llm-deadline.js +53 -0
  17. package/lib/core/machine-domain.js +106 -0
  18. package/lib/core/maintenance.js +380 -0
  19. package/lib/core/memory-bridge.js +818 -0
  20. package/lib/core/memory-categories.js +84 -0
  21. package/lib/core/memory-scoring.js +65 -0
  22. package/lib/core/memory-search.js +414 -0
  23. package/lib/core/memory-write.js +1176 -0
  24. package/lib/core/policy.js +111 -0
  25. package/lib/core/profile-confirmation.js +55 -0
  26. package/lib/core/profile-review.js +375 -0
  27. package/lib/core/profile-write.js +283 -0
  28. package/lib/core/reference-seeds.js +63 -0
  29. package/lib/core/route-health.js +106 -0
  30. package/lib/core/stores.js +1021 -0
  31. package/lib/core/tagger.js +7 -0
  32. package/lib/core/types.js +15 -0
  33. package/lib/core/vector-backfill.js +140 -0
  34. package/lib/core/vector-lane.js +184 -0
  35. package/lib/dsh/command-surface.js +376 -0
  36. package/lib/dsh/compaction-engine.js +231 -0
  37. package/lib/dsh/config-switches.js +56 -0
  38. package/lib/dsh/config.js +13 -0
  39. package/lib/dsh/context-pressure.js +36 -0
  40. package/lib/dsh/drop-landing.js +42 -0
  41. package/lib/dsh/emergency.js +89 -0
  42. package/lib/dsh/flush-command.js +148 -0
  43. package/lib/dsh/global-review.js +412 -0
  44. package/lib/dsh/historian.js +907 -0
  45. package/lib/dsh/live-journal.js +81 -0
  46. package/lib/dsh/maintenance-wiring.js +229 -0
  47. package/lib/dsh/management-pass.js +564 -0
  48. package/lib/dsh/memory-lane.js +535 -0
  49. package/lib/dsh/pre-step.js +675 -0
  50. package/lib/dsh/project-drift.js +116 -0
  51. package/lib/dsh/recomp.js +192 -0
  52. package/lib/dsh/reduce-guidance.js +188 -0
  53. package/lib/dsh/seams.js +588 -0
  54. package/lib/dsh/search-hint.js +186 -0
  55. package/lib/dsh/session-notice.js +100 -0
  56. package/lib/dsh/status-command.js +75 -0
  57. package/lib/dsh/telemetry.js +349 -0
  58. package/lib/dsh/tool-bridge.js +1009 -0
  59. package/lib/dsh/volatile.js +89 -0
  60. package/lib/index.js +931 -0
  61. package/package.json +108 -4
  62. package/tools/migrate-profile-package-name.mjs +317 -0
  63. package/vendor/onnx/LICENSE +27 -0
  64. package/vendor/onnx/ort-wasm-simd-threaded.mjs +59 -0
  65. package/vendor/onnx/ort-wasm-simd-threaded.wasm +0 -0
  66. 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
+ }