@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,186 @@
1
+ /**
2
+ * Auto-search hint — proactive recall (upstream `auto-search-hint.ts` +
3
+ * `auto-search-runner.ts`).
4
+ *
5
+ * When a new user turn arrives, optionally search this project's memories with the
6
+ * user's OWN words and append a short "you may recall something related" entry to
7
+ * the session stream. The hint deliberately fragments and compresses: the goal is
8
+ * to nudge the agent into running its memory search for full context, not to hand
9
+ * it the answer. Guardrails are upstream's: ≤3 fragments, ~80 chars each, ≤800
10
+ * chars total.
11
+ *
12
+ * Two deliberate differences from upstream:
13
+ *
14
+ * - **Carrier.** Upstream appends the hint to the user message itself; the
15
+ * adapter never edits the user's text (one durable surface, and the title
16
+ * derives from the first `source: 'user'` message). The hint instead travels
17
+ * as its own user-role message INSIDE the admitted batch: `planSearchHint`
18
+ * returns the built message and the pre-step listener appends it to
19
+ * `decision.messages`, which the host persists verbatim after its own
20
+ * system-prompt commits (host agent-loop `step()`: system commits first, then
21
+ * `for (const message of decision.messages) session.append('user/message',
22
+ * message)`). That ordering is the fix for the 2026-10-09 corruption
23
+ * incident: the previous carrier appended directly from inside the
24
+ * `agent/pre-step` waterfall, which on a fresh session's first step ran
25
+ * BEFORE the host wrote `step/start` and the first `system/message`, making
26
+ * the hint surface node 0 — the loader's `foldSurface` then rejected the log
27
+ * on every replay ("requires a protected first surface head"; 10/10 corrupt
28
+ * logs had the hint as the early node). Inside the batch, the host's own
29
+ * commits always precede the hint, the hint still rides the request it was
30
+ * computed for (the request is built from the surface after the batch lands),
31
+ * and the transcript renders the same collapsed `source.kind` row as before.
32
+ * - **FTS-only search.** `lane.searchForSession` skips the embedding request, so a
33
+ * cold local model cannot delay the turn the hint is meant to inform. The
34
+ * stored-vector lane remains available to `ctx_memory` (`action:"search"`),
35
+ * where the agent asked for it.
36
+ *
37
+ * Fail-open everywhere: a hint is an optimisation, and no turn may fail over it.
38
+ */
39
+ import { randomUUID } from 'node:crypto';
40
+ import { magicContextSource } from "./seams.js";
41
+ /** Upstream's limits (auto-search-hint.ts). */
42
+ export const HINT_MAX_FRAGMENTS = 3;
43
+ export const HINT_FRAGMENT_CHARS = 80;
44
+ export const HINT_MAX_CHARS = 800;
45
+ /** Upstream's deadline is 3s; local FTS is milliseconds, so this is a backstop. */
46
+ export const HINT_TIMEOUT_MS = 2_000;
47
+ function truncate(text, limit) {
48
+ const normalized = text.replace(/\s+/g, ' ').trim();
49
+ return normalized.length <= limit ? normalized : `${normalized.slice(0, Math.max(0, limit - 1)).trimEnd()}…`;
50
+ }
51
+ /**
52
+ * Render the hint entry, or null when there is nothing worth saying.
53
+ * @param hits - ranked memory hits from the FTS lane.
54
+ * @param options - fragment count and per-fragment character cap.
55
+ * @returns the `<ctx-search-hint>` block, bounded by {@link HINT_MAX_CHARS}.
56
+ */
57
+ export function buildSearchHint(hits, options = {}) {
58
+ const maxFragments = Math.max(1, options.maxFragments ?? HINT_MAX_FRAGMENTS);
59
+ const fragmentChars = Math.max(20, options.fragmentChars ?? HINT_FRAGMENT_CHARS);
60
+ const lines = hits
61
+ .slice(0, maxFragments)
62
+ .map(hit => truncate(`[${hit.category}] ${hit.preview}`, fragmentChars))
63
+ .filter(fragment => fragment.length > 0);
64
+ if (lines.length === 0)
65
+ return null;
66
+ const header = lines.length === 1
67
+ ? 'Your memory may contain 1 related fragment:'
68
+ : `Your memory may contain ${String(lines.length)} related fragments:`;
69
+ const footer = 'If the fragments above seem relevant to the current request, you may run ctx_memory '
70
+ + 'with action "search" to retrieve full context. Otherwise ignore.';
71
+ const body = [header, ...lines.map(line => `- ${line}`), footer].join('\n');
72
+ const wrapped = `<ctx-search-hint>\n${body}\n</ctx-search-hint>`;
73
+ if (wrapped.length <= HINT_MAX_CHARS)
74
+ return wrapped;
75
+ const overflow = wrapped.length - HINT_MAX_CHARS;
76
+ const trimmed = body.slice(0, Math.max(0, body.length - overflow - 1)).trimEnd();
77
+ return `<ctx-search-hint>\n${trimmed}…\n</ctx-search-hint>`;
78
+ }
79
+ /**
80
+ * One hint per user turn. preStep runs once per model request, so a turn with
81
+ * tool calls would otherwise append the same hint at every step.
82
+ */
83
+ export class SearchHintTracker {
84
+ turns = new Map();
85
+ /**
86
+ * Claim the right to hint for one turn.
87
+ * @param sessionId - the session the turn belongs to.
88
+ * @param turnKey - the newest user message id (stable across this turn's steps).
89
+ * @returns true when this turn has not been hinted yet.
90
+ */
91
+ claim(sessionId, turnKey) {
92
+ if (this.turns.get(sessionId) === turnKey)
93
+ return false;
94
+ this.turns.set(sessionId, turnKey);
95
+ if (this.turns.size > 64) {
96
+ const oldest = this.turns.keys().next().value;
97
+ if (oldest !== undefined)
98
+ this.turns.delete(oldest);
99
+ }
100
+ return true;
101
+ }
102
+ /** Forget a session (its hints restart from scratch). */
103
+ forget(sessionId) {
104
+ this.turns.delete(sessionId);
105
+ }
106
+ }
107
+ /** Bound a promise so a slow search can never hold the turn. */
108
+ async function withDeadline(work, timeoutMs) {
109
+ let timer;
110
+ try {
111
+ return await Promise.race([
112
+ work,
113
+ new Promise(resolve => {
114
+ timer = setTimeout(() => { resolve(undefined); }, timeoutMs);
115
+ }),
116
+ ]);
117
+ }
118
+ finally {
119
+ if (timer !== undefined)
120
+ clearTimeout(timer);
121
+ }
122
+ }
123
+ /**
124
+ * Build the hint's durable message: the exact shape the direct-append carrier
125
+ * used (`appendMagicContextEntry`), now delivered inside the admitted batch.
126
+ * Structural, not host-typed on purpose: the host persists batch messages
127
+ * verbatim (`session.append('user/message', message)`), so the producer source
128
+ * round-trips without a declared kind in the host's `MessageSourceMap`.
129
+ */
130
+ export function buildHintMessage(hint, hits) {
131
+ return {
132
+ id: randomUUID(),
133
+ role: 'user',
134
+ content: [{ type: 'text', text: hint }],
135
+ source: magicContextSource(`Magic Context · ${String(hits)} possible memory match(es) — ctx_memory (action "search") for full context`),
136
+ };
137
+ }
138
+ /**
139
+ * Decide whether this turn gets a hint, and build its message when it does.
140
+ * The caller carries the message inside `decision.messages` — the hint never
141
+ * appends to the session directly from here (protected-head incident,
142
+ * 2026-10-09; see the carrier note in the header).
143
+ *
144
+ * Every outcome is journaled as `search-hint` so the thresholds can be tuned from
145
+ * telemetry rather than guessed at.
146
+ * @param deps - the search closure, journal, and knobs.
147
+ * @param input - the user's text and this turn's key.
148
+ * @returns what happened, for callers that want to log or assert.
149
+ */
150
+ export async function planSearchHint(deps, input) {
151
+ const now = deps.now ?? Date.now;
152
+ const record = (outcome, detail = {}) => {
153
+ deps.journal?.info('search-hint', `auto-search hint: ${outcome.action === 'quiet' ? outcome.reason : outcome.action}`, { action: outcome.action, ...'reason' in outcome ? { reason: outcome.reason } : {}, ...detail, trigger: 'pre-step' }, input.sessionId);
154
+ return outcome;
155
+ };
156
+ if (deps.search === undefined)
157
+ return record({ action: 'quiet', reason: 'lane-off' });
158
+ const text = input.text.trim();
159
+ const minChars = deps.minChars ?? 24;
160
+ // A command or one of our own hints is never a prompt, however short: the
161
+ // length gate below must not answer for them.
162
+ if (text.startsWith('/') || text.includes('<ctx-search-hint>')) {
163
+ return record({ action: 'quiet', reason: 'not-a-prompt' });
164
+ }
165
+ if (text.length < minChars)
166
+ return record({ action: 'quiet', reason: 'too-short' }, { chars: text.length });
167
+ if (deps.tracker !== undefined && !deps.tracker.claim(input.sessionId, input.turnKey)) {
168
+ return record({ action: 'quiet', reason: 'already-hinted' });
169
+ }
170
+ const queries = text.slice(0, 400);
171
+ const started = now();
172
+ let hits;
173
+ try {
174
+ const found = await withDeadline(deps.search(input.session, queries, deps.limit ?? 5), deps.timeoutMs ?? HINT_TIMEOUT_MS);
175
+ if (found === undefined)
176
+ return record({ action: 'failed', reason: 'timeout' }, { ms: now() - started });
177
+ hits = found;
178
+ }
179
+ catch (error) {
180
+ return record({ action: 'failed', reason: 'search-failed' }, { detail: error instanceof Error ? error.message : String(error) });
181
+ }
182
+ const hint = buildSearchHint(hits);
183
+ if (hint === null)
184
+ return record({ action: 'quiet', reason: 'no-hits' }, { ms: now() - started });
185
+ return record({ action: 'hinted', hits: hits.length, text: hint, message: buildHintMessage(hint, hits.length) }, { ms: now() - started, chars: hint.length });
186
+ }
@@ -0,0 +1,100 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { magicContextSource } from "./seams.js";
3
+ /** The host's one-line bound for `notice` summaries. */
4
+ export const NOTICE_SUMMARY_MAX_CHARS = 120;
5
+ function boundSummary(summary) {
6
+ return summary.length <= NOTICE_SUMMARY_MAX_CHARS
7
+ ? summary
8
+ : `${summary.slice(0, NOTICE_SUMMARY_MAX_CHARS - 1)}…`;
9
+ }
10
+ function grouped(n) {
11
+ return n.toLocaleString('en-US');
12
+ }
13
+ /** `1 compartment` / `2 compartments`; the notice carries no plural-rule table. */
14
+ function plural(count, one, many) {
15
+ return count === 1 ? one : many;
16
+ }
17
+ /**
18
+ * The one-line account of a pass, or null when nothing notable happened.
19
+ * Baseline-only refreshes (a negative token delta with no discrete action)
20
+ * stay quiet: an execute pass runs every step while pressure is high and a
21
+ * notice per step would spam the transcript.
22
+ */
23
+ export function sessionNoticeSummary(result) {
24
+ const parts = [];
25
+ const delta = result.tokensAfter - result.tokensBefore;
26
+ if (result.forceDropped > 0) {
27
+ parts.push(`emergency-dropped ${grouped(result.forceDropped)} `
28
+ + plural(result.forceDropped, 'tool output', 'tool outputs'));
29
+ }
30
+ if (result.published.length > 0) {
31
+ parts.push(`compacted ${grouped(result.published.length)} `
32
+ + plural(result.published.length, 'compartment', 'compartments')
33
+ + ` ${result.published.map(id => `§C${String(id)}§`).join(' ')}`
34
+ + (delta < 0 ? ` · reclaimed ${grouped(-delta)} tok` : ''));
35
+ }
36
+ else if (delta < 0 && result.forceDropped > 0) {
37
+ parts.push(`reclaimed ${grouped(-delta)} tok`);
38
+ }
39
+ if (result.factsPromoted > 0) {
40
+ parts.push(`promoted ${grouped(result.factsPromoted)} `
41
+ + plural(result.factsPromoted, 'memory', 'memories')
42
+ + (result.factsSeenAgain > 0 ? ` (${grouped(result.factsSeenAgain)} already known)` : ''));
43
+ }
44
+ if (parts.length === 0)
45
+ return null;
46
+ return boundSummary(`Magic Context · ${parts.join(' · ')}`);
47
+ }
48
+ /**
49
+ * Append one magic-context entry to the live stream. The single home of the entry
50
+ * shape: a user-role message carrying the plugin source, appended with
51
+ * `surfaceOp: 'append'` so the host accepts it as a normal tail node.
52
+ *
53
+ * Refuses on a headless surface: an out-of-band append before the host has
54
+ * written surface node 0 (the first `system/message`) would take that position
55
+ * itself and make the log unloadable on replay (2026-10-09 incident; the class
56
+ * rule lives in {@link SessionNoticeSession}).
57
+ *
58
+ * Never throws: a broken transcript nicety must not wedge the turn.
59
+ * @param session - the session whose stream is appended to.
60
+ * @param text - the model-visible body (keep it short; it becomes context).
61
+ * @param summary - the one-line collapsed row the transcript shows.
62
+ * @returns the appended seq, or the refusal/append failure.
63
+ */
64
+ export function appendMagicContextEntry(session, text, summary) {
65
+ if (session.surface !== undefined && session.surface.nodes.length === 0) {
66
+ return { ok: false, reason: 'headless-surface' };
67
+ }
68
+ const message = {
69
+ id: randomUUID(),
70
+ role: 'user',
71
+ content: [{ type: 'text', text }],
72
+ source: magicContextSource(boundSummary(summary)),
73
+ };
74
+ try {
75
+ return { ok: true, seq: session.append('user/message', message, { surfaceOp: 'append' }) };
76
+ }
77
+ catch (error) {
78
+ return { ok: false, reason: 'append-failed', detail: error instanceof Error ? error.message : String(error) };
79
+ }
80
+ }
81
+ /**
82
+ * Append one notice entry for a management pass outcome. Never throws: a
83
+ * broken transcript nicety must not wedge the turn (fail-open, like every
84
+ * other best-effort surface in the adapter).
85
+ */
86
+ export function appendSessionNotice(input) {
87
+ const summary = sessionNoticeSummary(input.result);
88
+ if (summary === null)
89
+ return { ok: false, reason: 'not-notable' };
90
+ const lines = [summary];
91
+ if (input.result.baselineUpdated)
92
+ lines.push('Baseline refreshed (m[0] frozen point advanced)');
93
+ const appended = appendMagicContextEntry(input.session, lines.join('\n'), summary);
94
+ if (!appended.ok) {
95
+ return appended.reason === 'headless-surface'
96
+ ? { ok: false, reason: 'headless-surface' }
97
+ : { ok: false, reason: 'append-failed', detail: appended.detail };
98
+ }
99
+ return { ok: true, summary, seq: appended.seq };
100
+ }
@@ -0,0 +1,75 @@
1
+ import { decidePass, pressurePercent, pressureWindowTokens } from "../core/policy.js";
2
+ import { enumerateSurfaceToolResults, requestContextOf } from "./seams.js";
3
+ export function registerCtxStatusCommand(ctx, deps) {
4
+ const commands = ctx.get('commands');
5
+ if (commands === undefined) {
6
+ ctx.logger.warn('magic-context dsh adapter: commands service not mounted; /ctx-status unavailable');
7
+ return;
8
+ }
9
+ commands.register({
10
+ name: 'ctx-status',
11
+ description: 'Magic Context: pressure, reclaim ledger, and pass state for this session.',
12
+ handler: (invocation) => {
13
+ const sessionId = invocation.agent.session.id;
14
+ const policy = typeof deps.policy === 'function' ? deps.policy() : deps.policy;
15
+ const latest = deps.stores.latestUsage(sessionId);
16
+ const { contextWindow } = requestContextOf(invocation.agent.session);
17
+ // The same denominator the pass decision uses, so the reported percent
18
+ // and the decision printed below it can never disagree.
19
+ const pressureWindow = pressureWindowTokens(contextWindow, policy);
20
+ const pressure = pressurePercent(latest, pressureWindow);
21
+ const decision = decidePass({ latest, pressure, now: Date.now(), policy });
22
+ const nodes = enumerateSurfaceToolResults(invocation.agent.session, callId => deps.stores.getTag(sessionId, { kind: 'tool-result', callId }));
23
+ const tagged = nodes.filter(node => node.tagNumber !== undefined);
24
+ const dropped = tagged.filter(node => node.isPlaceholder);
25
+ const pending = deps.stores.listPendingDrops(sessionId);
26
+ const compartments = deps.stores.listCompartments(sessionId);
27
+ const unlanded = deps.stores.listUnlandedCompartments(sessionId);
28
+ const baseline = deps.stores.baselineState(sessionId);
29
+ const coveredTokens = compartments.reduce((total, compartment) => total + compartment.coveredTokens, 0);
30
+ // Journal health: the validation-period at-a-glance check (the full
31
+ // review lives in scripts/mc-report.mjs).
32
+ let journalLine = '- journal: unavailable';
33
+ try {
34
+ const bounds = deps.stores.journalBounds();
35
+ const dayAgo = Date.now() - 24 * 60 * 60_000;
36
+ const recent = deps.stores.journalByKind(dayAgo);
37
+ const errors = recent.filter(row => row.level === 'error').reduce((total, row) => total + row.count, 0);
38
+ const passes = recent.filter(row => row.kind === 'pass').reduce((total, row) => total + row.count, 0);
39
+ journalLine = `- journal: ${String(bounds.rows)} entries`
40
+ + `${bounds.newest === undefined ? '' : `, latest ${new Date(bounds.newest).toISOString()}`}`
41
+ + `; last 24h: ${String(passes)} pass event(s), ${String(errors)} error(s)`
42
+ + `${bounds.rows === 0 ? ' (audit layer empty: was this build reloaded?)' : ''}`;
43
+ }
44
+ catch (error) {
45
+ journalLine = `- journal: read failed (${error instanceof Error ? error.message : String(error)})`;
46
+ }
47
+ const lines = [
48
+ 'Magic Context (dsh adapter) — status',
49
+ `- usage: ${latest === undefined
50
+ ? 'no response recorded yet'
51
+ : `${pressure === undefined ? '?' : pressure.toFixed(1)}% of ${String(pressureWindow)} tokens`
52
+ + `${contextWindow === undefined ? ' (assumed; the route discloses none)' : ''} `
53
+ + `(last input ${String(latest.inputTokens ?? 0)} + cache `
54
+ + `${String((latest.cacheReadTokens ?? 0) + (latest.cacheWriteTokens ?? 0))}, `
55
+ + `at ${new Date(latest.recordedAt).toISOString()})`}`,
56
+ `- pass: ${decision.kind} (${decision.reason})`,
57
+ `- tool outputs: ${String(tagged.length)} tagged, ${String(dropped.length)} dropped as placeholders`,
58
+ `- reclaim queue: ${pending.length === 0 ? 'empty' : pending.map(tag => `§${String(tag)}§`).join(', ')}`,
59
+ `- historian: ${String(compartments.length)} compartment(s), ~${String(coveredTokens)} tokens digested`
60
+ + `${unlanded.length === 0 ? '' : `, ${String(unlanded.length)} awaiting publication`}`,
61
+ `- baseline: ${baseline === undefined
62
+ ? 'not built yet'
63
+ : `epoch ${String(baseline.epoch)}, node ${String(baseline.nodeSeq ?? '?')}, `
64
+ + `frozen ${new Date(baseline.frozenAt).toISOString()}`
65
+ + `${baseline.memoriesDigest === undefined ? '' : ', memories injected'}`}`,
66
+ `- policy: execute ≥ ${String(policy.executeThreshold)}%, force ≥ ${String(policy.forceThreshold)}%, `
67
+ + `ttl ${String(Math.round(policy.ttlMs / 1000))}s, protect newest ${String(policy.protectedToolResults)} `
68
+ + `(+${String(policy.historianProtectedNodes)} from the historian), `
69
+ + `assumed window ${String(policy.assumedContextWindow)} tokens`,
70
+ journalLine,
71
+ ];
72
+ return { kind: 'success', text: lines.join('\n') };
73
+ },
74
+ });
75
+ }