@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
package/lib/index.js ADDED
@@ -0,0 +1,931 @@
1
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, DSH_EVENTS, enumerateSurfaceToolResults, probeDshSeams, readUsageFields, requestContextOf, tagToolResultContent, } from "./dsh/seams.js";
2
+ import { reclaimForEmergency } from "./dsh/emergency.js";
3
+ import { historianInFlight } from "./dsh/historian.js";
4
+ import { publishReadyCompartments, surfaceTokens } from "./dsh/management-pass.js";
5
+ import { defineCtxExpandTool, defineCtxReduceTool, defineCtxMemoryTool } from "./dsh/tool-bridge.js";
6
+ import { ReduceNudgeTracker } from "./dsh/reduce-guidance.js";
7
+ import { registerCtxStatusCommand } from "./dsh/status-command.js";
8
+ import { registerCtxFlushCommand } from "./dsh/flush-command.js";
9
+ import { registerCtxRecompCommand } from "./dsh/recomp.js";
10
+ import { registerCtxGlobalCommand, registerCtxProfileCommand } from "./dsh/command-surface.js";
11
+ import { reviewGlobalCandidates } from "./dsh/global-review.js";
12
+ import { promoteMemoryToCandidate } from "./core/global-domains.js";
13
+ import { SearchHintTracker } from "./dsh/search-hint.js";
14
+ import { registerMagicCompactionEngine } from "./dsh/compaction-engine.js";
15
+ import { liveJournal } from "./dsh/live-journal.js";
16
+ import { createTelemetryService } from "./dsh/telemetry.js";
17
+ import { openMemoryLane } from "./dsh/memory-lane.js";
18
+ import { injectReduceGuidance, preStepListener } from "./dsh/pre-step.js";
19
+ import { wireMaintenance } from "./dsh/maintenance-wiring.js";
20
+ import { SWITCH_SNAPSHOT_META_KEY, buildSwitchSnapshot, diffSwitches } from "./dsh/config-switches.js";
21
+ import { isQuotaExhaustedDetail, RouteHealth } from "./core/route-health.js";
22
+ import { legacyTagsRetentionWarning, } from "./dsh/config.js";
23
+ import { DEFAULT_POLICY } from "./core/policy.js";
24
+ import z from '@deepseek-ai/schemastery';
25
+ import { ensureTag, tagPrefix } from "./core/tagger.js";
26
+ import { recoverOrphanHistorianRuns } from "./core/journal.js";
27
+ import { defaultDataDir, openStores, SCHEMA_VERSION } from "./core/stores.js";
28
+ import { errText } from "./core/types.js";
29
+ import { liveBoolean, liveNumber, liveRoute, liveString, policyOf } from "./dsh/volatile.js";
30
+ import { pressureUsedTokens, shouldLogContextPressure, } from "./dsh/context-pressure.js";
31
+ export const name = 'magic-context-dsh-poc';
32
+ /**
33
+ * Required host services. Declaring the injection makes the loader wait for
34
+ * availability (a bare `ctx.get()` does not) and unlocks guarded property
35
+ * access inside apply. A host without a tools registry or LLM seam pends this
36
+ * plugin visibly instead of half-mounting it.
37
+ */
38
+ export const inject = ['tools', 'llm'];
39
+ /** One legacy-config note per process, not one per mount. */
40
+ let legacyTagsWarningEmitted = false;
41
+ export async function apply(ctx, config = {}) {
42
+ if (config.enabled === false) {
43
+ ctx.logger.info('magic-context dsh adapter: disabled by config');
44
+ return;
45
+ }
46
+ const probe = probeDshSeams(ctx);
47
+ if (!probe.ok) {
48
+ // Fail loud, not silent: an adapter that cannot see its seams must not
49
+ // pretend to manage context (Magic Context's fail-closed discipline).
50
+ throw new Error(`magic-context dsh adapter: DSH seam probe failed on this host build: ${probe.problems.join('; ')}`);
51
+ }
52
+ const policy = policyOf(config.policy);
53
+ /** Re-evaluated per pass so a settings-form edit applies immediately. */
54
+ const activePolicy = () => policyOf(config.policy);
55
+ /**
56
+ * The generation pin (historian + `/compact` summaries), re-read at every use
57
+ * point so a settings-form edit reaches the next run without a remount.
58
+ * `undefined` = follow the session's own route (the pre-step lane passes it
59
+ * as the fallback; the engine/command lanes pass no override).
60
+ */
61
+ const historianRouteOf = () => liveRoute(config.historian?.provider, config.historian?.model);
62
+ /**
63
+ * Effort is independent of the route pin: it must also apply when the
64
+ * historian follows the session route. Each lane merges it with the route it
65
+ * actually resolved (the pre-step has the live session route; the engine and
66
+ * /ctx-recomp merge it into their active/fallback route).
67
+ */
68
+ const historianEffortOf = () => {
69
+ const effort = liveString(config.historian?.reasoningEffort, '').trim();
70
+ return effort === '' ? undefined : effort;
71
+ };
72
+ /** Process start, for the telemetry uptime readout. */
73
+ const startedAt = Date.now();
74
+ const storeDir = config.dataDir ?? defaultDataDir();
75
+ const stores = openStores(storeDir);
76
+ ctx.logger.info(`magic-context dsh adapter: stores at ${stores.dbPath}`);
77
+ // §N§ tags are not deleted by age. The fork's mount-time 30-day prune is
78
+ // gone; message/session deletion and compaction are the original model's
79
+ // retention paths, but this adapter does not implement deletion here.
80
+ const legacyTagsWarning = legacyTagsRetentionWarning(config);
81
+ if (legacyTagsWarning !== undefined && !legacyTagsWarningEmitted) {
82
+ legacyTagsWarningEmitted = true;
83
+ ctx.logger.warn(legacyTagsWarning);
84
+ }
85
+ // Volatile config leaves arrive as live references (see `Config`), so every
86
+ // consumer reads through the `live*` helpers instead of comparing the raw
87
+ // value: `config.registerCompactionEngine !== false` is TRUE for a reference,
88
+ // which would mount the engine a user had explicitly disabled.
89
+ //
90
+ // `journal` is a LIVE wrapper when the composition boots with the journal
91
+ // on: enabled/level/mirror/retention are read at write time, so a
92
+ // settings-form edit applies to the next entry (a volatile commit never
93
+ // remounts, and the old mount-time construction made every journal edit a
94
+ // dead option until restart — the audit, 2026-10-10). Booting with
95
+ // `journal.enabled: false` still wires `undefined`: the maintenance tick
96
+ // reads a missing journal as "do not run", and the boot-time opt-out keeps
97
+ // that meaning (turning it back on then needs an app restart, which the
98
+ // form's hint says).
99
+ const journal = liveBoolean(config.journal?.enabled, true) === false
100
+ ? undefined
101
+ : liveJournal({ config, stores, storeDir, logger: ctx.logger });
102
+ /** Sessions already announced, so the journal gets exactly one first-seen row. */
103
+ const seenSessions = new Set();
104
+ // Journal repair: an hmr remount disposes stores mid-run and leaves a
105
+ // historian `start` without a terminal sibling. At mount time no run of the
106
+ // new composition is in flight yet, so every still-open start predates this
107
+ // process and gets one synthetic `adapter-remount` terminal (journal.ts).
108
+ const recoveredHistorianRuns = journal === undefined ? 0 : recoverOrphanHistorianRuns(stores, journal);
109
+ /** Resolved tool-constructor path, recorded at boot for the review. */
110
+ let toolMode = 'skipped';
111
+ /** The LLM seam (declared inject); the historian's auxiliary call uses it. */
112
+ const llm = ctx.llm;
113
+ /** Reduce-guidance state per session (Channel 1/2 sticky bands, turn counts). */
114
+ const reduceTracker = new ReduceNudgeTracker();
115
+ // ── The memory lane ────────────────────────────────────────────────────────
116
+ // `memory` is deliberately NOT declared in the exported Config: a schemastery
117
+ // key would be materialized into every profile. It arrives as a plain object,
118
+ // and since the distribution defaults (0.9.0) the gate is:
119
+ // absent config → on (defaults; corpus beside the adapter store)
120
+ // enabled: false → off (explicit opt-out; reported as disabled-by-config)
121
+ // anything else → on
122
+ // The bundle's presence in `dsh.profile.bundles` IS the consent: after
123
+ // `dsh plugin add` the features must work with no profile hand-editing.
124
+ // Read once, inside the lane, so a disabled adapter can never half-open.
125
+ const lane = openMemoryLane(ctx, config, stores);
126
+ const { memoryRecall, memoryWriter, profileWriter, memorySearch, describeMemoryWrite } = lane;
127
+ const { memorySettings } = lane;
128
+ const disposers = [];
129
+ const on = (name, listener) => {
130
+ disposers.push(ctx.on.call(ctx, name, listener));
131
+ };
132
+ // ── The compaction seam ────────────────────────────────────────────────────
133
+ // Occupying `ctx.compaction` is what makes this adapter authoritative over
134
+ // reclaim. The seam is FIRST-COME-FIRST-SERVED — measured on the real host
135
+ // stack (tests/compaction-conflict.spec.ts, 2026-10-14): when compaction-
136
+ // basic mounts first (the stock composition — the base bundle ships it),
137
+ // OUR engine's registration is rejected and the adapter stands down (warn
138
+ // log + `unavailable:` meta; compaction-basic keeps compaction; everything
139
+ // else about the adapter is unaffected). When the adapter mounts first,
140
+ // compaction-basic cannot mount at all. Takeover therefore needs a profile
141
+ // that does not mount compaction-basic — defaulting the switch on cannot
142
+ // win on stock and stays opt-in.
143
+ // `registerCompactionEngine` is a LIVE switch: the pre-step band calls
144
+ // `reconcileEngine` on every step, so a settings-form toggle mounts or
145
+ // unmounts the engine without a restart. The switch is volatile, and a
146
+ // volatile commit never remounts the plugin — the old mount-time branch
147
+ // made the form's toggle a dead option: it saved, and nothing changed
148
+ // until the app restarted (the audit, 2026-10-10). The state sits here,
149
+ // before any `await` in apply, so a pre-step firing mid-apply (during the
150
+ // tools section's dynamic import) cannot hit a temporal-dead-zone read.
151
+ let adapterDisposed = false;
152
+ let engineHandle;
153
+ let engineBusy = false;
154
+ /** One mount attempt per ON transition: a failing mount must not retry on every step. */
155
+ let engineFailed = false;
156
+ const reconcileEngine = () => {
157
+ const wanted = liveBoolean(config.registerCompactionEngine, false);
158
+ if (engineBusy || adapterDisposed || wanted === (engineHandle !== undefined))
159
+ return;
160
+ if (wanted && engineFailed)
161
+ return;
162
+ engineBusy = true;
163
+ if (wanted) {
164
+ void (async () => {
165
+ try {
166
+ const base = config.compactionBase ?? (await import('@deepseek-ai/dsh-compaction')).CompactionEngine;
167
+ // Awaiting: a duplicate-mount rejection (`ctx.compaction` already
168
+ // owned) must land in the catch below, not become an unhandled
169
+ // rejection.
170
+ engineHandle = await registerMagicCompactionEngine(ctx, {
171
+ base,
172
+ stores,
173
+ policy,
174
+ activePolicy,
175
+ logger: ctx.logger,
176
+ ...llm === undefined ? {} : { llm },
177
+ ...journal === undefined ? {} : { journal },
178
+ activeRoute: historianRouteOf,
179
+ activeEffort: historianEffortOf,
180
+ ...memoryRecall === undefined ? {} : { recall: memoryRecall },
181
+ isDisposed: () => adapterDisposed,
182
+ });
183
+ ctx.logger.info('magic-context dsh adapter: CompactionEngine mounted on ctx.compaction');
184
+ stores.setMeta('compaction_engine', 'magic-context-dsh');
185
+ journal?.info('engine', 'CompactionEngine mounted on ctx.compaction', { action: 'mounted' });
186
+ }
187
+ catch (error) {
188
+ const detail = errText(error);
189
+ engineFailed = true;
190
+ stores.setMeta('compaction_engine', `unavailable: ${detail}`);
191
+ journal?.warn('engine', `CompactionEngine not mounted: ${detail}`, { action: 'unavailable', detail });
192
+ ctx.logger.warn(`magic-context dsh adapter: CompactionEngine not mounted `
193
+ + `(${detail}); the management loop is unaffected`);
194
+ }
195
+ finally {
196
+ engineBusy = false;
197
+ }
198
+ })();
199
+ return;
200
+ }
201
+ const handle = engineHandle;
202
+ if (handle === undefined) {
203
+ // Unreachable past the guard above; kept so the narrowing is explicit.
204
+ engineBusy = false;
205
+ return;
206
+ }
207
+ engineHandle = undefined;
208
+ engineFailed = false;
209
+ void handle.dispose()
210
+ .then(() => {
211
+ // Record the opt-out explicitly: a stale "mounted" value from an
212
+ // earlier composition would otherwise keep claiming the seam.
213
+ stores.setMeta('compaction_engine', 'disabled-by-config');
214
+ journal?.info('engine', 'CompactionEngine unmounted by configuration', { action: 'unmounted' });
215
+ ctx.logger.info('magic-context dsh adapter: CompactionEngine unmounted (disabled by configuration)');
216
+ })
217
+ .catch((error) => {
218
+ ctx.logger.warn(`magic-context dsh adapter: engine unmount failed: ${errText(error)}`);
219
+ })
220
+ .finally(() => { engineBusy = false; });
221
+ };
222
+ // Boot: mount eagerly, or record the opt-out explicitly (a stale "mounted"
223
+ // value from an earlier composition would otherwise keep claiming the seam).
224
+ if (liveBoolean(config.registerCompactionEngine, false)) {
225
+ reconcileEngine();
226
+ }
227
+ else {
228
+ stores.setMeta('compaction_engine', 'disabled-by-config');
229
+ journal?.info('engine', 'CompactionEngine disabled by configuration', { action: 'disabled-by-config' });
230
+ }
231
+ // Identities the preStep registers, the maintenance sweep serves and the
232
+ // panel scopes by — one registry, three consumers.
233
+ const projects = { seenIdentities: new Set(), sessionIdentity: new Map() };
234
+ // Auto-search hint state: one hint per user turn (preStep runs per model step).
235
+ const searchHints = new SearchHintTracker();
236
+ // The main conversation's latest route (last-seen-wins across sessions) and
237
+ // the volatile per-provider quota observations fed from `agent/request-error`.
238
+ // The maintenance lane follows the route when unpinned, and skips its LLM
239
+ // steps while the route's latest observation is an exhausted quota — the
240
+ // signal is owned by real traffic and never persisted, so an externally
241
+ // reset quota cannot be trapped by a stale guess.
242
+ let lastSessionRoute;
243
+ const routeHealth = new RouteHealth();
244
+ // ── §N§ tags + the pressure decision on the admitted user batch ───────────
245
+ on(DSH_EVENTS.preStep, preStepListener({
246
+ ctx,
247
+ config,
248
+ stores,
249
+ journal,
250
+ llm,
251
+ reduceTracker,
252
+ lane,
253
+ projects,
254
+ seenSessions,
255
+ activePolicy,
256
+ /** Engine mount/unmount rides the step band: a form toggle applies without a restart. */
257
+ reconcileEngine: () => reconcileEngine(),
258
+ searchHints,
259
+ noteSessionRoute: route => { lastSessionRoute = route; },
260
+ }));
261
+ // ── §N§ tags on tool results at landing (Phase 0) ─────────────────────────
262
+ on(DSH_EVENTS.postToolExecute, async (exec, result, next) => {
263
+ const decision = await next();
264
+ if (decision.kind !== 'accept' || 'value' in decision || result.isError)
265
+ return decision;
266
+ const sessionId = exec.agent?.session.id;
267
+ if (sessionId === undefined)
268
+ return decision;
269
+ const tag = ensureTag(stores, sessionId, { kind: 'tool-result', callId: exec.callId });
270
+ const base = decision.content ?? result.content;
271
+ // Nothing to tag (a result without content blocks); the decision is
272
+ // already the host's, so it passes through unchanged.
273
+ if (base === undefined)
274
+ return decision;
275
+ return { kind: 'accept', content: tagToolResultContent(base, tagPrefix, tag) };
276
+ });
277
+ // ── Reduce guidance through the SystemPrompt service (MC parity) ──────────
278
+ injectReduceGuidance({ ctx, config, stores, journal, llm, reduceTracker, lane, projects, seenSessions, activePolicy });
279
+ // ── Usage recording from durable assistant/message events (Phase 0) ───────
280
+ on(DSH_EVENTS.sessionEvent, (session, event) => {
281
+ const fields = readUsageFields(event);
282
+ if (fields === undefined)
283
+ return;
284
+ const record = { sessionId: session.id, recordedAt: Date.now(), ...fields };
285
+ try {
286
+ stores.recordUsage(record);
287
+ }
288
+ catch (error) {
289
+ const detail = errText(error);
290
+ journal?.warn('error', `usage recording failed: ${detail}`, { scope: 'usage', detail }, session.id);
291
+ // Best effort by design (mirrors MC per-turn fail-open): the next
292
+ // assistant/message anchors a fresh row.
293
+ ctx.logger.warn(`magic-context: usage recording failed for ${session.id}@${String(fields.seq)}: ${detail}`);
294
+ }
295
+ });
296
+ // ── Emergency reclaim + retry on provider-confirmed overflow ──────────────
297
+ on(DSH_EVENTS.requestError, async (payload, next) => {
298
+ // Route-health feed, best-effort by design: these reads sit BEFORE the
299
+ // band try/catch below, so a drifted payload shape would throw into the
300
+ // host's request-error waterfall. An exhausted-quota wording for a known
301
+ // provider arms the maintenance lane's skip; anything else is ignored.
302
+ try {
303
+ const provider = payload?.provider;
304
+ const failure = payload?.failure;
305
+ if (typeof provider === 'string' && provider.length > 0 && failure !== undefined) {
306
+ const detail = `${failure.code ?? ''} ${failure.message ?? ''}`;
307
+ if (isQuotaExhaustedDetail(detail))
308
+ routeHealth.observeQuotaFailure(provider, Date.now());
309
+ }
310
+ }
311
+ catch (feedError) {
312
+ ctx.logger.warn(`magic-context: route-health feed failed: ${errText(feedError)}`);
313
+ }
314
+ // Shape-tolerant guard: these reads sit BEFORE the band try/catch below, so
315
+ // a drifted payload shape would throw into the host's request-error
316
+ // waterfall. Degrade to pass-through instead — the emergency lane simply
317
+ // stays idle, and the doctor heartbeat is what surfaces the drift.
318
+ if (payload?.failure?.code !== CONTEXT_WINDOW_EXCEEDED_CODE || payload?.signal?.aborted)
319
+ return next();
320
+ const session = payload.agent?.session;
321
+ const sessionId = session?.id;
322
+ if (session === undefined || sessionId === undefined)
323
+ return next();
324
+ const before = session.surface.replaceGeneration;
325
+ try {
326
+ // C1: the token window scales with the route's context window.
327
+ const context = requestContextOf(session);
328
+ // ── Tier 0 (fusion, mon3stera's ready bank): land ready compartments
329
+ // BEFORE the drop lane. One replacement reclaims a whole digested
330
+ // span; an emergency drop only individual tool results — and the
331
+ // request has already failed once, so this bust cannot lose more than
332
+ // the failed retry did. This lane is a forced execute band in
333
+ // everything but name (the same exception family as the wire-repair
334
+ // iron rule: the provider rejected the wire as-is). Fail-open: a
335
+ // publish failure falls through to the drop lane, never skips it. No
336
+ // baseline refresh and no fact promotion here — both belong to the
337
+ // next execute pass (facts stay durable in the compartment row).
338
+ const publishedIds = [];
339
+ const publishOnce = () => {
340
+ const bank = publishReadyCompartments({
341
+ session,
342
+ sessionId,
343
+ stores,
344
+ policy,
345
+ ...journal === undefined ? {} : { journal },
346
+ });
347
+ publishedIds.push(...bank.ids);
348
+ };
349
+ publishOnce();
350
+ // ── Tier 0.5 (OFF by default): a digest in flight gets a bounded
351
+ // chance to finish before the drop lane fires (mon3stera's
352
+ // waitReadyTimeout race). `forceWaitReadyMs: 0` — the shipped default —
353
+ // skips the wait; flip it after the force-path observation window.
354
+ if (publishedIds.length === 0) {
355
+ const waitReadyMs = Math.max(0, policy.forceWaitReadyMs);
356
+ const inFlight = historianInFlight(sessionId);
357
+ if (waitReadyMs > 0 && inFlight !== undefined) {
358
+ await Promise.race([
359
+ inFlight.catch(() => undefined),
360
+ new Promise(resolve => {
361
+ const timer = setTimeout(resolve, waitReadyMs);
362
+ timer.unref?.();
363
+ }),
364
+ ]);
365
+ journal?.info('force-path', `overflow path waited on the in-flight historian (${String(waitReadyMs)}ms cap)`, {
366
+ tier: 'wait',
367
+ waitReadyMs,
368
+ }, sessionId);
369
+ publishOnce();
370
+ }
371
+ }
372
+ if (publishedIds.length > 0) {
373
+ journal?.warn('force-path', `provider rejected the request as too long; landed ${String(publishedIds.length)} ready compartment(s) before the drop lane`, {
374
+ tier: 'publish',
375
+ published: publishedIds,
376
+ reason: 'context-window-exceeded',
377
+ generationBefore: before,
378
+ generationAfter: session.surface.replaceGeneration,
379
+ }, sessionId);
380
+ }
381
+ // Re-enumerate AFTER any publish: it shadows surface nodes, and
382
+ // dropping against a stale seq would throw into the waterfall.
383
+ const nodes = enumerateSurfaceToolResults(session, callId => stores.getTag(sessionId, { kind: 'tool-result', callId }));
384
+ // #731: the whole-surface estimate (measured AFTER any Tier-0 publish)
385
+ // lets the drop lane refuse a reclaim that cannot fit the request.
386
+ const reclaim = reclaimForEmergency(session, nodes, policy, context.contextWindow, surfaceTokens(session));
387
+ journal?.warn('emergency-reclaim', `provider rejected the request as too long; dropped ${String(reclaim.dropped.length)} tool output(s)`, {
388
+ dropped: reclaim.dropped.length,
389
+ protected: reclaim.protectedCount,
390
+ reason: 'context-window-exceeded',
391
+ tier: reclaim.skipped === 'cannot-fit'
392
+ ? 'skipped-cannot-fit'
393
+ : reclaim.dropped.length > 0 ? 'drop' : publishedIds.length > 0 ? 'publish-only' : 'none',
394
+ // #731: dropping everything reclaimable still leaves residualTokens
395
+ // against the window — the results are preserved and no retry fires
396
+ // (no drop ⇒ no surface progress ⇒ `next()` passes the error through).
397
+ ...(reclaim.skipped === 'cannot-fit'
398
+ ? { skipped: reclaim.skipped, residualTokens: reclaim.residualTokens, contextWindow: context.contextWindow }
399
+ : {}),
400
+ publishedBeforeDrop: publishedIds,
401
+ generationBefore: before,
402
+ generationAfter: session.surface.replaceGeneration,
403
+ retried: session.surface.replaceGeneration > before,
404
+ }, sessionId);
405
+ ctx.logger.warn(`magic-context: overflow reclaim landed ${String(publishedIds.length)} compartment(s), dropped ${String(reclaim.dropped.length)} tool outputs `
406
+ + `(${String(reclaim.protectedCount)} protected) after a context-overflow rejection`);
407
+ }
408
+ catch (error) {
409
+ const detail = errText(error);
410
+ journal?.error('error', `emergency reclaim failed: ${detail}`, { scope: 'request-error', detail }, sessionId);
411
+ ctx.logger.warn(`magic-context: emergency reclaim failed: ${detail}`);
412
+ return next();
413
+ }
414
+ // Durable surface progress is the retry proof (compaction-basic pattern):
415
+ // a retry re-derives the request from the shrunken surface.
416
+ if (!payload?.signal?.aborted && session.surface.replaceGeneration > before) {
417
+ return { kind: 'retry' };
418
+ }
419
+ return next();
420
+ });
421
+ // ── Tools + command ────────────────────────────────────────────────────────
422
+ // Human approval for global-domain candidates (L1① second leg): the asker
423
+ // resolves the session's live root Agent and forwards the cards to the
424
+ // host's user-questions waterfall. Structural injection — the package is
425
+ // not a dependency; the asker stays undefined (fail-closed at call time)
426
+ // when the service is absent, e.g. headless.
427
+ let askGlobalReview;
428
+ ctx.inject(['userQuestions', 'agents'], (scope) => {
429
+ const { userQuestions, agents } = scope;
430
+ if (userQuestions === undefined || agents === undefined)
431
+ return;
432
+ askGlobalReview = async ({ questions, sessionId }) => {
433
+ const agent = sessionId === undefined ? undefined : agents.get(sessionId);
434
+ return userQuestions.ask({ questions, ...(agent === undefined ? {} : { agent }) });
435
+ };
436
+ });
437
+ if (config.registerTools !== false) {
438
+ // Constructor resolution: host-injected → dynamic import. The import is
439
+ // the normal path (`@deepseek-ai/dsh-tools` is a hard dependency); when
440
+ // neither resolves, the failure propagates and the mount aborts loudly —
441
+ // the ctx_* tools are the only entry for the agent's reduce marks, so a
442
+ // boot without them must not half-run. See dsh/tool-bridge.ts.
443
+ let defineTool = config.defineTool;
444
+ toolMode = 'host-injected';
445
+ if (defineTool === undefined) {
446
+ defineTool = (await import('@deepseek-ai/dsh-tools')).defineTool;
447
+ toolMode = 'dynamic-import';
448
+ }
449
+ try {
450
+ const registrar = ctx.tools;
451
+ // The host's `defineTool` takes a wider option type than the subset this
452
+ // package authors; `tool-bridge`'s seam names that subset so the three
453
+ // tool bodies keep their argument types (see `LocalDefineTool`).
454
+ const deps = { stores, defineTool: defineTool, ...journal === undefined ? {} : { journal } };
455
+ registrar.register(defineCtxReduceTool(deps));
456
+ registrar.register(defineCtxExpandTool(deps));
457
+ if (memoryRecall !== undefined) {
458
+ registrar.register(defineCtxMemoryTool(lane.ctxMemoryRegistration({
459
+ activePolicy,
460
+ defineTool,
461
+ journal,
462
+ // L1① second leg: the review closure reads the asker binding lazily —
463
+ // the inject above may land after this registration runs.
464
+ reviewGlobal: exec => {
465
+ const ask = askGlobalReview;
466
+ const pending = reviewGlobalCandidates({
467
+ store: stores,
468
+ ...(ask === undefined
469
+ ? {}
470
+ : { ask: ({ questions, sessionId }) => ask({ questions, sessionId }) }),
471
+ ...memoryWriter === undefined || memoryWriter.status.ok !== true ? {} : { publisher: memoryWriter },
472
+ ...journal === undefined ? {} : { journal },
473
+ // Default 50 = the auto-stage pool cap: one review_global call
474
+ // drains the whole pending queue through chained 5-question
475
+ // cards (2026-10-13). The old call-site pin of 5 sliced a batch
476
+ // into one card per invocation.
477
+ batchLimit: liveNumber(config.memory?.global?.review?.batchLimit, 50),
478
+ sessionId: exec?.agent?.session?.id,
479
+ now: () => Date.now(),
480
+ });
481
+ return pending.then(r => ({
482
+ outcome: `审批完成:批准 ${String(r.approved)} / 改判 ${String(r.reclassified)} / 拒绝 ${String(r.rejected)}`
483
+ + ` / 跳过 ${String(r.skipped)} / 失败 ${String(r.failed)};剩余待审 ${String(r.remaining)}`,
484
+ detail: r.lines.length === 0 ? undefined : r.lines.join('\n'),
485
+ }));
486
+ },
487
+ // L1① second leg, instant path: promote enqueues ONE project memory
488
+ // into a global domain and raises its card at once (onlyCandidateId)
489
+ // — the human-judged lane without the recurrence gate. Same asker
490
+ // binding as reviewGlobal: the lazy read survives the inject landing
491
+ // after this registration.
492
+ promoteGlobal: (input, exec) => {
493
+ if (memoryRecall === undefined) {
494
+ return Promise.resolve({ outcome: '共享记忆库未接线,无法提升。' });
495
+ }
496
+ const domainId = input.domain === undefined || input.domain === '' || input.domain === 'org'
497
+ ? 'dir:global:org'
498
+ : input.domain === 'machine' ? 'dir:global:machine' : input.domain;
499
+ const promoted = promoteMemoryToCandidate(stores, {
500
+ corpusDbPath: memoryRecall.dbPath,
501
+ memoryId: input.memoryId,
502
+ domainId,
503
+ now: Date.now(),
504
+ });
505
+ if (!promoted.ok) {
506
+ return Promise.resolve({ outcome: `提升失败:${promoted.error}` });
507
+ }
508
+ const ask = askGlobalReview;
509
+ const review = reviewGlobalCandidates({
510
+ store: stores,
511
+ ...(ask === undefined
512
+ ? {}
513
+ : { ask: ({ questions, sessionId }) => ask({ questions, sessionId }) }),
514
+ ...memoryWriter === undefined || memoryWriter.status.ok !== true ? {} : { publisher: memoryWriter },
515
+ ...journal === undefined ? {} : { journal },
516
+ sessionId: exec?.agent?.session?.id,
517
+ now: () => Date.now(),
518
+ onlyCandidateId: promoted.candidate.id,
519
+ });
520
+ return review.then(r => ({
521
+ outcome: `候选 #${String(promoted.candidate.id)} 已入池(${domainId}),审批卡已弹出:`
522
+ + `批准 ${String(r.approved)} / 改判 ${String(r.reclassified)} / 拒绝 ${String(r.rejected)}`
523
+ + ` / 跳过 ${String(r.skipped)} / 失败 ${String(r.failed)}`,
524
+ detail: r.lines.length === 0 ? undefined : r.lines.join('\n'),
525
+ }));
526
+ },
527
+ })));
528
+ }
529
+ ctx.logger.info(`magic-context dsh adapter: ctx_reduce / ctx_expand registered (${toolMode})`);
530
+ }
531
+ catch (error) {
532
+ // Fail closed: without its tools the adapter cannot accept the
533
+ // agent's reduce marks, so a mount without them must be loud, not
534
+ // silently half-alive. Use registerTools: false to opt out explicitly.
535
+ throw new Error(`magic-context dsh adapter: agent tools failed to register `
536
+ + `(${errText(error)})`);
537
+ }
538
+ }
539
+ if (config.registerStatusCommand !== false) {
540
+ registerCtxStatusCommand(ctx, { stores, policy: activePolicy });
541
+ // Flush shares the slash-command gate: it is a manual queue lever, not a
542
+ // second recovery mode. The command only writes the durable marker; the
543
+ // next preStep lands it through the same helper as the automatic pass.
544
+ registerCtxFlushCommand(ctx, { stores });
545
+ // Recomp rides the same gate: it needs the same seams as the historian, and
546
+ // its handler refuses with a readable message when the LLM is absent or the
547
+ // historian is switched off.
548
+ registerCtxRecompCommand(ctx, {
549
+ stores,
550
+ policy: activePolicy,
551
+ logger: ctx.logger,
552
+ // LIVE gate: the same volatile `historianEnabled` the pre-step band
553
+ // reads at decision time (the old mount-time boolean disagreed with
554
+ // the live gate after a settings-form toggle).
555
+ enabled: () => liveBoolean(config.historianEnabled, true) !== false,
556
+ ...journal === undefined ? {} : { journal },
557
+ ...llm === undefined ? {} : { llm },
558
+ activeRoute: historianRouteOf,
559
+ activeEffort: historianEffortOf,
560
+ });
561
+ ctx.logger.info('magic-context dsh adapter: /ctx-status, /ctx-flush and /ctx-recomp registered');
562
+ // The global-domain surface rides the same gate: with Magic Context's
563
+ // dashboard gone these commands ARE the operating surface for the profile
564
+ // queue and the domains.
565
+ registerCtxProfileCommand(ctx, {
566
+ stores,
567
+ ...memoryRecall === undefined ? {} : { corpusDbPath: memoryRecall.dbPath },
568
+ // WP12/DoD: retiring a profile row is the user domain's only write from
569
+ // the command surface. Without the writer the command says so instead of
570
+ // reporting a retirement that never happened.
571
+ ...profileWriter === undefined || profileWriter.status.ok !== true ? {} : { profileWriter },
572
+ });
573
+ registerCtxGlobalCommand(ctx, {
574
+ stores,
575
+ ...memoryRecall === undefined ? {} : { corpusDbPath: memoryRecall.dbPath },
576
+ // WP9: approving a candidate publishes it. Without the writer there is no
577
+ // way to make it visible, and the command says so rather than marking a
578
+ // candidate approved that nobody can see.
579
+ ...memoryWriter === undefined || memoryWriter.status.ok !== true ? {} : { publisher: memoryWriter },
580
+ });
581
+ ctx.logger.info('magic-context dsh adapter: /ctx-profile and /ctx-global registered');
582
+ }
583
+ stores.setMeta('adapter_version', 'phase2');
584
+ stores.setMeta('memory_bridge', memoryRecall === undefined
585
+ ? memorySettings === undefined ? 'off' : 'disabled-by-config'
586
+ : memoryRecall.status.ok
587
+ ? `ok schema v${String(memoryRecall.status.schemaVersion)}`
588
+ : `unavailable: ${String(memoryRecall.status.detail)}`);
589
+ stores.setMeta('memory_write', describeMemoryWrite());
590
+ // ── Telemetry for the browser half (validation-period observability) ──────
591
+ // The Remote event whitelist is a compile-time constant, so a third-party
592
+ // bundle cannot subscribe to plugin events; a web route we own is the
593
+ // supported seam (verified by docs/dsh-plugin-ui-telemetry-contract.md).
594
+ //
595
+ // `ctx.get('webServer')` is a SNAPSHOT read: the web server's provider fiber
596
+ // is normally not active yet when a bundle applies, so reading it eagerly
597
+ // returned undefined and the route never registered (measured as a 404 in an
598
+ // isolated profile while the desktop instance happened to win the race).
599
+ // `ctx.inject` waits for the service and re-runs on its lifecycle, which is
600
+ // how @deepseek-ai/dsh-client-modules attaches its own route.
601
+ let telemetry;
602
+ ctx.inject(['webServer'], (scope) => {
603
+ const webServer = scope.webServer;
604
+ telemetry = createTelemetryService({
605
+ stores,
606
+ ...journal === undefined ? {} : { journal },
607
+ meta: () => ({
608
+ engine: stores.getMeta('compaction_engine'),
609
+ memory: stores.getMeta('memory_bridge'),
610
+ memoryWrite: stores.getMeta('memory_write'),
611
+ toolsMode: toolMode,
612
+ }),
613
+ // Dreamer-slice KPIs for the panel: the viewed session's project
614
+ // verification coverage + the newest maintenance tally. Lazy getters —
615
+ // the panel polls them, nothing runs at mount.
616
+ memoryStats: (sessionId, identity) => {
617
+ if (memoryWriter === undefined)
618
+ return undefined;
619
+ // The identity telemetry resolved from this scope's newest recall comes
620
+ // first: it is the very project the header shows, and the process
621
+ // serves several sessions (first-seen would answer for someone else's
622
+ // project). Config and the session registry are the fallbacks.
623
+ const resolved = identity
624
+ ?? config.memory?.project
625
+ ?? (sessionId !== undefined ? projects.sessionIdentity.get(sessionId) : undefined)
626
+ ?? [...projects.seenIdentities][0];
627
+ if (resolved === undefined)
628
+ return undefined;
629
+ try {
630
+ const stats = memoryWriter.stats(resolved);
631
+ return { total: stats.rows, verified: stats.verified, unverified: stats.unverified };
632
+ }
633
+ catch {
634
+ return undefined;
635
+ }
636
+ },
637
+ maintenanceLast: maintenance.maintenanceLast,
638
+ startedAt,
639
+ });
640
+ const service = telemetry;
641
+ scope.effect(() => webServer.register({
642
+ kind: 'prefix',
643
+ path: '/mc-telemetry',
644
+ handler: (req, res) => service.handle(req, res),
645
+ }));
646
+ journal?.info('boot', 'telemetry route attached for the browser half', {
647
+ telemetry: '/mc-telemetry',
648
+ routes: ['/mc-telemetry/summary', '/mc-telemetry/events'],
649
+ });
650
+ });
651
+ // ── Maintenance tick (the adapter's slice of MC's dreamer) ─────────────────
652
+ const maintenance = wireMaintenance({
653
+ ctx,
654
+ config,
655
+ stores,
656
+ journal,
657
+ llm,
658
+ writer: memoryWriter,
659
+ profileWriter,
660
+ vectorBackfill: lane.vectorBackfill,
661
+ routeHealth,
662
+ lastSessionRoute: () => lastSessionRoute,
663
+ projects,
664
+ disposers,
665
+ isDisposed: () => adapterDisposed,
666
+ });
667
+ // The occupancy gauge the native meter shows, from the SAME source: the
668
+ // host's `contextPressure` projection. projectedTokens is the last
669
+ // request's prompt-side usage plus the surface's signed movement since the
670
+ // sample — the answer for the NEXT request, not the last-request bill our
671
+ // pass ledger carries (the two read differently and the user noticed).
672
+ // A reading lands in the journal only when it is genuinely new (see
673
+ // dsh/context-pressure.ts): every distinct reading used to write a row,
674
+ // measured at ~10 rows/minute during heavy work. Telemetry serves the newest
675
+ // row to the panel, so both gauges agree within one threshold step.
676
+ ctx.inject(['sessionProjections'], (scope) => {
677
+ // Structural, not host-typed on purpose: `SessionProjectionMap` is
678
+ // merge-extensible and `contextPressure` is registered at runtime by the
679
+ // token meter, so the host type's key union cannot name our key.
680
+ const registry = scope.sessionProjections;
681
+ if (journal === undefined || registry === undefined)
682
+ return;
683
+ const lastBySession = new Map();
684
+ const disposeListener = registry.onChanged((session, key, value) => {
685
+ try {
686
+ if (key !== 'contextPressure')
687
+ return;
688
+ const reading = value;
689
+ const sid = typeof session?.id === 'string' ? session.id : '';
690
+ if (!shouldLogContextPressure(lastBySession.get(sid), reading))
691
+ return;
692
+ lastBySession.set(sid, { used: pressureUsedTokens(reading), window: reading.contextWindow ?? 0 });
693
+ if (lastBySession.size > 64) {
694
+ const oldest = lastBySession.keys().next().value;
695
+ if (oldest !== undefined)
696
+ lastBySession.delete(oldest);
697
+ }
698
+ const used = pressureUsedTokens(reading);
699
+ journal.info('context-pressure', `context pressure reading: ${String(used)} / ${String(reading.contextWindow ?? 0)} tok`, {
700
+ projectedTokens: reading.projectedTokens,
701
+ pressureTokens: reading.pressureTokens,
702
+ contextWindow: reading.contextWindow,
703
+ }, sid === '' ? undefined : sid);
704
+ }
705
+ catch {
706
+ // a failed reading must never break the host's projection drive
707
+ }
708
+ });
709
+ // The listener's disposer rides this inject fiber — a scope reload
710
+ // re-runs the whole inject and swaps listeners cleanly.
711
+ void disposeListener;
712
+ });
713
+ // Config-drift alarm (2026-10-12): the profile's `config` block is REPLACE
714
+ // semantics and once silently dropped three switch keys — the switches fell
715
+ // back to defaults with no error row, found ~12h later by inference. Diff
716
+ // this mount's effective snapshot against the previous mount's (persisted in
717
+ // the adapter meta table) and say so LOUDLY when something went off. First
718
+ // mount only seeds. Fail-open: a meta failure must not break the boot.
719
+ const switches = buildSwitchSnapshot(config);
720
+ try {
721
+ const previousRaw = stores.getMeta(SWITCH_SNAPSHOT_META_KEY);
722
+ if (previousRaw !== undefined) {
723
+ const previous = JSON.parse(previousRaw);
724
+ for (const row of diffSwitches(previous, switches)) {
725
+ const detail = { key: row.key, before: row.before, after: row.after };
726
+ if (row.severity === 'went-off') {
727
+ journal?.warn('config-drift', `switch went OFF: ${row.key} (${String(row.before)} -> ${String(row.after)})`, detail);
728
+ }
729
+ else {
730
+ journal?.info('config-drift', `switch changed: ${row.key} (${String(row.before)} -> ${String(row.after)})`, detail);
731
+ }
732
+ }
733
+ }
734
+ stores.setMeta(SWITCH_SNAPSHOT_META_KEY, JSON.stringify(switches));
735
+ }
736
+ catch (error) {
737
+ journal?.warn('config-drift', `switch snapshot diff failed: ${error instanceof Error ? error.message : String(error)}`);
738
+ }
739
+ journal?.info('boot', `adapter booted (tools: ${toolMode}, historian: ${liveBoolean(config.historianEnabled, true) === false ? 'off' : llm === undefined ? 'no-llm' : 'on'})`, {
740
+ version: 'phase2',
741
+ dbPath: stores.dbPath,
742
+ schemaVersion: SCHEMA_VERSION,
743
+ recoveredHistorianRuns,
744
+ switches,
745
+ // Set only when this mount laid the own corpus down (fresh machine)
746
+ ...lane.corpusBootstrap === undefined ? {} : { corpusBootstrap: lane.corpusBootstrap },
747
+ tools: { mode: toolMode, registered: config.registerTools !== false },
748
+ engine: stores.getMeta('compaction_engine'),
749
+ memory: stores.getMeta('memory_bridge'),
750
+ memoryWrite: stores.getMeta('memory_write'),
751
+ telemetry: telemetry === undefined ? 'pending (registers with webServer)' : '/mc-telemetry',
752
+ // Resolved once for display; the tick itself resolves at decision time.
753
+ maintenance: {
754
+ enabled: liveBoolean(config.maintenance?.enabled, true) !== false,
755
+ intervalMs: Math.max(60_000, liveNumber(config.maintenance?.intervalMs, 1_800_000)),
756
+ verify: llm === undefined
757
+ ? 'off (no llm)'
758
+ : (() => {
759
+ const provider = liveString(config.maintenance?.verifyProvider, '');
760
+ const model = liveString(config.maintenance?.verifyModel, '');
761
+ if (provider.length > 0 && model.length > 0)
762
+ return `${provider}/${model}`;
763
+ // Unpinned: the tick follows the main conversation's latest route
764
+ // (last-seen-wins); the per-tick journal rows carry the actual route.
765
+ return 'follow-session';
766
+ })(),
767
+ },
768
+ historian: {
769
+ enabled: liveBoolean(config.historianEnabled, true) !== false && llm !== undefined,
770
+ // Resolved once for display; the lanes themselves re-read at use time.
771
+ route: historianRouteOf() === undefined
772
+ ? 'follow-session'
773
+ : `${historianRouteOf().provider}/${historianRouteOf().model}`,
774
+ reasoningEffort: historianEffortOf() ?? 'default',
775
+ },
776
+ journal: { enabled: journal !== undefined, mirror: journal?.mirrorPath, retentionDays: liveNumber(config.journal?.retentionDays, 30) },
777
+ // 融合 v7 的待观察翻转(boot 行可见,巡检核对配置漂移)。
778
+ // compartmentGuardMode 已于 2026-10-16 退役,不再进 boot 行。
779
+ forceWaitReadyMs: liveNumber(config.policy?.forceWaitReadyMs, 0),
780
+ memoryScoringEnabled: liveBoolean(config.memory?.scoring?.enabled, true),
781
+ policy: {
782
+ executeThreshold: policy.executeThreshold,
783
+ forceThreshold: policy.forceThreshold,
784
+ assumedContextWindow: policy.assumedContextWindow,
785
+ ttlMs: policy.ttlMs,
786
+ protectedToolResults: policy.protectedToolResults,
787
+ historianProtectedNodes: policy.historianProtectedNodes,
788
+ historianMinNodes: policy.historianMinNodes,
789
+ historianTimeoutMs: policy.historianTimeoutMs,
790
+ historianMaxTokens: policy.historianMaxTokens,
791
+ historianMaxTokensCeiling: policy.historianMaxTokensCeiling,
792
+ historianMinBudgetTokens: policy.historianMinBudgetTokens,
793
+ baselineTokens: policy.baselineTokens,
794
+ memoryLimit: policy.memoryLimit,
795
+ },
796
+ });
797
+ ctx.effect(() => () => {
798
+ // Flag FIRST: a stale engine holding the old closure must see disposed
799
+ // before it can touch any of the handles being closed below.
800
+ adapterDisposed = true;
801
+ for (const dispose of disposers.splice(0))
802
+ dispose();
803
+ telemetry?.close();
804
+ memoryWriter?.close();
805
+ profileWriter?.close();
806
+ memorySearch?.close();
807
+ memoryRecall?.close();
808
+ stores.close();
809
+ }, 'magic-context dsh adapter listeners and stores');
810
+ ctx.logger.info('magic-context dsh adapter: phase-2 management loop wired');
811
+ }
812
+ // Public entry surface: only what external consumers (the POC suite and the
813
+ // loader's default export) actually import. Everything else is imported
814
+ // in-package from its owning module.
815
+ export { replaceToolResultWithPlaceholder } from "./dsh/seams.js";
816
+ export { reclaimForEmergency } from "./dsh/emergency.js";
817
+ export { openStores } from "./core/stores.js";
818
+ export { DEFAULT_POLICY } from "./core/policy.js";
819
+ export { openJournal, redactData, formatJournalLine, recoverOrphanHistorianRuns } from "./core/journal.js";
820
+ export { runManagementPass } from "./dsh/management-pass.js";
821
+ export { policyOf } from "./dsh/volatile.js";
822
+ /**
823
+ * The runtime schema the loader (and the plugin page's settings form) reads.
824
+ *
825
+ * Only the fields a user may tune are declared, and only those are `.volatile()`
826
+ * — the Host's `settings.describe` projects volatile fields exclusively, so an
827
+ * undeclared or non-volatile field simply has no form control. schemastery
828
+ * passes undeclared keys through untouched (verified), so `defineTool`,
829
+ * `compactionBase`, `historian` and the rest stay exactly as configured.
830
+ *
831
+ * Volatility is also the mechanism behind "apply immediately": the loader
832
+ * commits a volatile-only change into the live reference without remounting the
833
+ * fiber, and this plugin reads those references through {@link policyOf} at
834
+ * every decision point.
835
+ *
836
+ * `memory` is deliberately NOT declared here. schemastery materialises declared
837
+ * nested objects with their defaults, so declaring it would hand every profile
838
+ * a `memory` object even when the user configured none — and in this plugin the
839
+ * mere presence of `memory` opens the read/write bridge on the shared store.
840
+ * Memory stays opt-in by configuration; its thresholds are edited in the patch.
841
+ */
842
+ export const Config = z.object({
843
+ historianEnabled: z.boolean().default(true).volatile(),
844
+ registerCompactionEngine: z.boolean().default(false).volatile(),
845
+ /** One notice entry in the session stream per action-bearing pass (see session-notice.ts). */
846
+ sessionNotices: z.boolean().default(true).volatile(),
847
+ /**
848
+ * Auto-search hint: one "you may recall something related" entry per user turn,
849
+ * built from the user's own words (see search-hint.ts).
850
+ */
851
+ searchHint: z.boolean().default(true).volatile(),
852
+ /**
853
+ * Maintenance tick (the adapter's slice of MC's dreamer): TTL expiry always
854
+ * runs when the memory writer is writable; LLM verification additionally
855
+ * needs an explicit route because a tick has no session to inherit one from.
856
+ */
857
+ maintenance: z.object({
858
+ enabled: z.boolean().default(true).volatile(),
859
+ intervalMs: z.number().min(60_000).default(1_800_000).volatile(),
860
+ /** Empty provider/model = verify off (schemastery objects cannot be optional). */
861
+ verifyProvider: z.string().default('').volatile(),
862
+ verifyModel: z.string().default('').volatile(),
863
+ }),
864
+ /**
865
+ * Generation route: the out-of-band historian and `/compact` summaries.
866
+ *
867
+ * A PAIR, like the maintenance route: both halves must be non-empty to pin;
868
+ * empty halves mean "follow the session's routed model", which is the
869
+ * shipped default (and the right one — the digest becomes future context, so
870
+ * the conversation's own model is usually the safe choice). Declaring the
871
+ * block is safe: unlike `memory`, its only effect is a route override, so a
872
+ * materialised empty object switches nothing on.
873
+ */
874
+ historian: z.object({
875
+ provider: z.string().default('').volatile(),
876
+ model: z.string().default('').volatile(),
877
+ /** Optional provider/model-specific reasoning effort; blank = host default. */
878
+ reasoningEffort: z.string().default('').volatile(),
879
+ }),
880
+ journal: z.object({
881
+ enabled: z.boolean().default(true).volatile(),
882
+ level: z.string().default('info').volatile(),
883
+ /** min(1): a stray 0/negative would prune the whole table at open. */
884
+ retentionDays: z.number().min(1).default(30).volatile(),
885
+ mirror: z.boolean().default(true).volatile(),
886
+ }),
887
+ policy: z.object({
888
+ /** Usage percent (0-100) at which the execute band opens. */
889
+ executeThreshold: z.number().min(0).max(100).default(65).volatile(),
890
+ /** Usage percent (0-100) at which reclaim becomes unconditional. */
891
+ forceThreshold: z.number().min(0).max(100).default(85).volatile(),
892
+ /**
893
+ * Window assumed for scheduling when the routed model discloses none.
894
+ * Without it a route that publishes no window defers 'no-window' forever,
895
+ * which silently disables the execute band.
896
+ */
897
+ assumedContextWindow: z.number().min(1).default(256_000).volatile(),
898
+ ttlMs: z.number().default(300_000).volatile(),
899
+ protectedToolResults: z.number().default(8).volatile(),
900
+ historianProtectedNodes: z.number().default(8).volatile(),
901
+ historianMinNodes: z.number().default(3).volatile(),
902
+ /** Wall-clock budget for one historian run; a spent budget journals `failed`.
903
+ * 120s: large backlogs (600+ pending nodes) measured ~60s+ per digest. */
904
+ historianTimeoutMs: z.number().default(120_000).volatile(),
905
+ /** Generation cap for the historian; the historian raises anything below 8192. */
906
+ historianMaxTokens: z.number().default(8_192).volatile(),
907
+ /** Ceiling the historian may escalate to after a token-cap truncation (retry doubling). */
908
+ historianMaxTokensCeiling: z.number().default(32_768).volatile(),
909
+ historianMinBudgetTokens: z.number().default(5_000).volatile(),
910
+ baselineTokens: z.number().default(2_000).volatile(),
911
+ memoryBudgetTokens: z.number().default(8_000).volatile(),
912
+ memoryLimit: z.number().default(40).volatile(),
913
+ /**
914
+ * Overflow-path wait on an in-flight historian before the drop lane
915
+ * (0 = never wait — the shipped default; flip after observation).
916
+ */
917
+ forceWaitReadyMs: z.number().min(0).default(0).volatile(),
918
+ }),
919
+ });
920
+ /**
921
+ * The plugin as a loadable object. This is what every loading mode mounts:
922
+ * the profile loader takes a package's default export, and in-tree glue and
923
+ * tests pass it to `ctx.plugin` directly — one source of truth for the
924
+ * required services (`inject`), the apply entry, and the runtime `Config`.
925
+ *
926
+ * `Config` MUST live on the default export: the loader's `unwrapExports` picks
927
+ * the default object and reads `Config` from it, so a named `export const
928
+ * Config` alone is silently ignored and the configuration never parses
929
+ * (measured on cordis 4.0.4).
930
+ */
931
+ export default { name, inject, apply, Config };