@elvisw/dsh-magic-context 0.0.0-bootstrap.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 +453 -0
  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 +110 -0
  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,302 @@
1
+ /**
2
+ * Global-domain candidates: what may become org/machine knowledge, and why.
3
+ *
4
+ * The plan's original source was a recurrence scan — "the same normalized hash
5
+ * seen in two different projects". Measured against the live store on
6
+ * 2026-10-10 that source is EMPTY, and not because of a bug:
7
+ *
8
+ * active rows 1292 · distinct projects 33 · hashes shared by 2+ projects 0
9
+ * content identical across projects 0 · even the first 60 characters 0
10
+ *
11
+ * Historian facts are project-bound (file paths, decisions, incident details),
12
+ * so exact-normalized recurrence does not happen at this corpus size and has no
13
+ * reason to start. Building the lane on that gate would produce an
14
+ * always-empty pool while looking implemented — the "feed it a source we know
15
+ * is inaccurate" failure this project explicitly rejects.
16
+ *
17
+ * So the scan stays here as a MEASUREMENT (it answers "is recurrence a thing
18
+ * yet?" and is cheap to re-run), and the actual source is a human promotion:
19
+ * someone who recognises a project memory as organisation-level knowledge. That
20
+ * is also the only source consistent with the design rule that global memories
21
+ * are human-confirmed and never auto-promoted.
22
+ *
23
+ * Both paths write ONLY the adapter's own `global_candidates` table. Publishing
24
+ * into the shared store under a domain identity is a separate step with its own
25
+ * gate: `publishGlobalCandidate` (WP9) does the write, and an approval that has
26
+ * not happened yet can never reach it.
27
+ */
28
+ import { DatabaseSync } from 'node:sqlite';
29
+ /** The provider identity that owns organisation-scoped memories. */
30
+ export const ORG_DOMAIN_ID = 'dir:global:org';
31
+ /**
32
+ * Credential shapes that must never cross a project boundary.
33
+ *
34
+ * The design rule is "global memories store only POINTERS to secrets, never
35
+ * credential bodies" — a project-local note may legitimately contain a
36
+ * credential, but promoting it to a shared domain would leak it to every member
37
+ * project.
38
+ *
39
+ * Deliberately conservative: long hex/base64 runs are NOT flagged, because this
40
+ * corpus is full of commit ids and digests (40-hex sha1s everywhere) and a rule
41
+ * that fires on those would block almost every candidate. Only contextual
42
+ * assignments, URL userinfo, private-key headers and known credential prefixes
43
+ * count.
44
+ */
45
+ const CREDENTIAL_RULES = [
46
+ // The leading boundary is `[^A-Za-z0-9]` rather than `\b`, because the shapes
47
+ // that matter most are SCREAMING_SNAKE env names (`DB_PASSWORD=…`), and `_` is
48
+ // a word character — `\bpassword` never matches there.
49
+ { rule: 'assign:password', pattern: /(?:^|[^A-Za-z0-9])(pass(word|wd|phrase)?|pwd)\s*[:=]\s*\S/i },
50
+ { rule: 'assign:secret', pattern: /(?:^|[^A-Za-z0-9])(client[_-]?secret|secret[_-]?key|secret)\s*[:=]\s*\S/i },
51
+ { rule: 'assign:api-key', pattern: /(?:^|[^A-Za-z0-9])(api[_-]?key|apikey|access[_-]?key|auth[_-]?token)\s*[:=]\s*\S/i },
52
+ { rule: 'assign:token', pattern: /(?:^|[^A-Za-z0-9])(token|credential|private[_-]?key)\s*[:=]\s*\S/i },
53
+ { rule: 'header:bearer', pattern: /authorization\s*[:=]\s*bearer\s+\S/i },
54
+ { rule: 'url-userinfo', pattern: /[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/i },
55
+ { rule: 'pem:private-key', pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
56
+ { rule: 'prefix:openai', pattern: /\bsk-[A-Za-z0-9_-]{16,}/ },
57
+ { rule: 'prefix:github', pattern: /\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/ },
58
+ { rule: 'prefix:aws', pattern: /\bAKIA[0-9A-Z]{16}\b/ },
59
+ { rule: 'prefix:slack', pattern: /\bxox[abprs]-[A-Za-z0-9-]{10,}/ },
60
+ { rule: 'prefix:google', pattern: /\bAIza[0-9A-Za-z_-]{30,}/ },
61
+ ];
62
+ /** Which credential rules a piece of text trips (empty = safe to share). */
63
+ export function scanForCredentials(content) {
64
+ return CREDENTIAL_RULES.filter(rule => rule.pattern.test(content)).map(rule => rule.rule);
65
+ }
66
+ /**
67
+ * The recurrence scan. Today this is a measurement: see the module comment.
68
+ * Returns the counts AND a sample of what it found, so a report can state the
69
+ * number instead of implying the lane works.
70
+ */
71
+ export function detectGlobalCandidates(store, options) {
72
+ const domainId = options.domainId ?? ORG_DOMAIN_ID;
73
+ const limit = options.limit ?? 25;
74
+ const db = new DatabaseSync(options.corpusDbPath, { readOnly: true });
75
+ try {
76
+ const groups = db.prepare(`SELECT normalized_hash AS hash, COUNT(DISTINCT project_path) AS projects, MAX(id) AS newest
77
+ FROM memories
78
+ WHERE status = 'active' AND normalized_hash IS NOT NULL AND normalized_hash <> ''
79
+ AND project_path NOT LIKE 'dir:global:%' AND project_path NOT LIKE 'git:global:%'
80
+ GROUP BY normalized_hash
81
+ HAVING COUNT(DISTINCT project_path) >= 2
82
+ ORDER BY projects DESC, newest DESC
83
+ LIMIT ?`).all(limit);
84
+ const samples = [];
85
+ let upserted = 0;
86
+ let blocked = 0;
87
+ for (const group of groups) {
88
+ const members = db.prepare(`SELECT id, project_path, content, category FROM memories
89
+ WHERE status = 'active' AND normalized_hash = ? ORDER BY id`).all(group.hash);
90
+ const representative = members[members.length - 1];
91
+ if (representative === undefined)
92
+ continue;
93
+ if (scanForCredentials(representative.content).length > 0) {
94
+ blocked += 1;
95
+ continue;
96
+ }
97
+ const candidate = store.upsertGlobalCandidate({
98
+ domainId,
99
+ content: representative.content,
100
+ normalizedHash: group.hash,
101
+ firstIdentity: representative.project_path,
102
+ category: representative.category,
103
+ sourceMemoryId: representative.id,
104
+ }, options.now);
105
+ for (const member of members) {
106
+ // `memory:<id>` marks an observation that came from a stored memory
107
+ // rather than a live session.
108
+ store.recordCandidateObservation({ candidateId: candidate.id, identity: member.project_path, sessionId: `memory:${String(member.id)}` }, options.now);
109
+ }
110
+ upserted += 1;
111
+ samples.push({
112
+ hash: group.hash,
113
+ projects: group.projects,
114
+ newestMemoryId: group.newest,
115
+ content: representative.content.slice(0, 120),
116
+ });
117
+ }
118
+ return { hashes: groups.length, upserted, blocked, samples };
119
+ }
120
+ finally {
121
+ db.close();
122
+ }
123
+ }
124
+ /** Read one memory from the contract (read-only) for a promotion decision. */
125
+ export function readCorpusMemory(corpusDbPath, memoryId) {
126
+ const db = new DatabaseSync(corpusDbPath, { readOnly: true });
127
+ try {
128
+ const row = db.prepare('SELECT id, project_path AS projectPath, category, content, normalized_hash AS normalizedHash, status '
129
+ + 'FROM memories WHERE id = ?').get(memoryId);
130
+ return row;
131
+ }
132
+ finally {
133
+ db.close();
134
+ }
135
+ }
136
+ /**
137
+ * Promote one project memory into a domain's candidate pool — the human-
138
+ * initiated source. Writes only the adapter's own table; publishing is a
139
+ * separate step.
140
+ *
141
+ * Eligibility is checked HERE rather than at publish time so the reviewer never
142
+ * sees a candidate that could not be published: the memory must exist, be
143
+ * active, live in a real project (not already a domain identity), and carry no
144
+ * credential body.
145
+ */
146
+ export function promoteMemoryToCandidate(store, options) {
147
+ const domainId = options.domainId ?? ORG_DOMAIN_ID;
148
+ if (store.getDomain(domainId) === undefined) {
149
+ return { ok: false, error: `unknown domain '${domainId}' — define it before promoting into it` };
150
+ }
151
+ const memory = readCorpusMemory(options.corpusDbPath, options.memoryId);
152
+ if (memory === undefined)
153
+ return { ok: false, error: `no memory #${String(options.memoryId)} in the shared store` };
154
+ if (memory.status !== 'active') {
155
+ return { ok: false, error: `memory #${String(memory.id)} is ${memory.status}, not active` };
156
+ }
157
+ if (memory.projectPath.startsWith('dir:global:') || memory.projectPath.startsWith('git:global:')) {
158
+ return { ok: false, error: `memory #${String(memory.id)} already lives in a global domain` };
159
+ }
160
+ const credentials = scanForCredentials(memory.content);
161
+ if (credentials.length > 0) {
162
+ return {
163
+ ok: false,
164
+ error: `memory #${String(memory.id)} trips ${credentials.join(', ')} — global memories carry POINTERS to secrets, `
165
+ + 'never credential bodies. Rewrite it as a reference and promote that.',
166
+ };
167
+ }
168
+ const candidate = store.upsertGlobalCandidate({
169
+ domainId,
170
+ content: memory.content,
171
+ normalizedHash: memory.normalizedHash,
172
+ firstIdentity: memory.projectPath,
173
+ category: memory.category,
174
+ sourceMemoryId: memory.id,
175
+ ...(options.suggestedDomain === undefined ? {} : { suggestedDomain: options.suggestedDomain }),
176
+ ...(options.suggestionReason === undefined ? {} : { suggestionReason: options.suggestionReason }),
177
+ }, options.now);
178
+ store.recordCandidateObservation({ candidateId: candidate.id, identity: memory.projectPath, sessionId: `memory:${String(memory.id)}` }, options.now);
179
+ // Re-read: the upsert deliberately writes zeroed counters and lets the
180
+ // observation rows define the weight, so the inserted snapshot is already
181
+ // stale by the time it is returned.
182
+ const refreshed = store.getGlobalCandidate(candidate.id) ?? candidate;
183
+ return { ok: true, candidate: refreshed, memory };
184
+ }
185
+ /**
186
+ * Whether a candidate may be approved.
187
+ *
188
+ * The recurrence gate exists to stop a MACHINE-detected one-off from becoming
189
+ * organisation-wide truth. A human promotion is a different signal: someone
190
+ * already judged it org-level, and approving it is a second human act — so the
191
+ * gate that applies is the publish pre-check (credentials, category), not the
192
+ * count.
193
+ */
194
+ export function approvalGate(store, candidate, capProbe) {
195
+ if (candidate.state !== 'pending') {
196
+ return { ok: false, error: `candidate #${String(candidate.id)} is ${candidate.state}, not pending` };
197
+ }
198
+ const credentials = scanForCredentials(candidate.content);
199
+ if (credentials.length > 0) {
200
+ return { ok: false, error: `candidate #${String(candidate.id)} trips ${credentials.join(', ')}` };
201
+ }
202
+ const domain = store.getDomain(candidate.domainId);
203
+ if (domain === undefined)
204
+ return { ok: false, error: `unknown domain '${candidate.domainId}'` };
205
+ // A published row needs a category (the contract has it NOT NULL and the
206
+ // reader filters on the allowlist) — refuse here rather than letting the
207
+ // INSERT fail with a constraint error the reviewer cannot act on.
208
+ if (candidate.category === undefined || candidate.category === '') {
209
+ return {
210
+ ok: false,
211
+ error: `candidate #${String(candidate.id)} carries no category; a published memory needs one `
212
+ + `(one of: ${domain.shareCategories.join(', ') || 'the domain allowlist'})`,
213
+ };
214
+ }
215
+ if (!domain.shareCategories.includes(candidate.category)) {
216
+ // Publishing under a category outside the allowlist succeeds and is then
217
+ // invisible forever — a silent failure, so it is refused up front.
218
+ return {
219
+ ok: false,
220
+ error: `category ${candidate.category} is outside ${candidate.domainId}'s allowlist `
221
+ + `(${domain.shareCategories.join(', ') || 'nothing'}) — publishing it would be invisible`,
222
+ };
223
+ }
224
+ // The admission cap (2026-10-16): `domains.cap` is DERIVED from the domain's
225
+ // token budget and was, until this check existed, displayed by /ctx-global
226
+ // list and enforced by nothing — a domain could grow past its ceiling with no
227
+ // signal anywhere. Counted against LIVE rows, so an archived row frees its slot.
228
+ if (capProbe !== undefined) {
229
+ const live = capProbe.countDomainMemories(candidate.domainId);
230
+ if (live >= domain.cap) {
231
+ return {
232
+ ok: false,
233
+ error: `${candidate.domainId} is at its cap (${String(live)}/${String(domain.cap)} live row(s)) — `
234
+ + 'withdraw a row or raise the domain budget before publishing another',
235
+ };
236
+ }
237
+ }
238
+ if (candidate.sourceMemoryId !== undefined)
239
+ return { ok: true };
240
+ if (candidate.distinctIdentities < 2) {
241
+ return {
242
+ ok: false,
243
+ error: `candidate #${String(candidate.id)} was observed by only ${String(candidate.distinctIdentities)} project(s); `
244
+ + 'a machine-detected promotion needs independent recurrence in at least two',
245
+ };
246
+ }
247
+ return { ok: true };
248
+ }
249
+ /**
250
+ * Publish an approved candidate into the shared store, under its domain
251
+ * identity. This is the step that makes organisation knowledge real: the domain
252
+ * identity IS a peer project, so the existing peer lane carries the row with no
253
+ * second pool, no second watermark and no second digest.
254
+ *
255
+ * Order is load-bearing and deliberate:
256
+ *
257
+ * 1. the approval gate (credentials, category allowlist, recurrence) — a
258
+ * refusal here must cost nothing;
259
+ * 2. the write;
260
+ * 3. the state transition, LAST.
261
+ *
262
+ * A candidate whose write failed therefore stays `pending` and can be retried,
263
+ * instead of being marked approved with an empty store behind it.
264
+ */
265
+ export function publishGlobalCandidate(store, options) {
266
+ const candidate = store.getGlobalCandidate(options.candidateId);
267
+ if (candidate === undefined) {
268
+ return { ok: false, error: `no candidate #${String(options.candidateId)}` };
269
+ }
270
+ // The gate is the single implementation of every publish prerequisite, so the
271
+ // command surface, the tests and this function can never disagree about what
272
+ // is publishable.
273
+ const gate = approvalGate(store, candidate, options.writer);
274
+ if (!gate.ok)
275
+ return { ok: false, error: gate.error };
276
+ const domain = store.getDomain(candidate.domainId);
277
+ if (domain === undefined)
278
+ return { ok: false, error: `unknown domain '${candidate.domainId}'` };
279
+ // The gate guarantees these; re-reading narrows the optional fields for the
280
+ // writer without a non-null assertion.
281
+ const category = candidate.category;
282
+ if (category === undefined || category === '') {
283
+ return { ok: false, error: `candidate #${String(candidate.id)} carries no category` };
284
+ }
285
+ const written = options.writer.publishDomainMemory({
286
+ identity: candidate.domainId,
287
+ category,
288
+ content: options.contentOverride ?? candidate.content,
289
+ ...options.importance === undefined ? {} : { importance: options.importance },
290
+ });
291
+ if (!written.ok)
292
+ return { ok: false, error: `publish refused: ${written.detail}` };
293
+ store.setGlobalCandidateState(candidate.id, 'approved', options.now);
294
+ return {
295
+ ok: true,
296
+ memoryId: written.id,
297
+ created: written.created,
298
+ ...written.revived === true ? { revived: true } : {},
299
+ identity: candidate.domainId,
300
+ category,
301
+ };
302
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * The activity journal — the adapter's observability layer for the validation
3
+ * period.
4
+ *
5
+ * Why it exists: the harness's `ctx.logger` output is not persisted by the
6
+ * Desktop application (there is no plugin log file to inspect afterwards), and
7
+ * the adapter's other tables hold *state*, not *history* — they can say what is
8
+ * true now, never why a pass deferred or what a request cost. The journal is an
9
+ * append-only record of decisions and outcomes, written to the adapter's own
10
+ * SQLite database next to that state, plus an optional human-readable mirror
11
+ * file for `tail`/grep.
12
+ *
13
+ * Privacy and size discipline: message text is NEVER recorded. Call sites pass
14
+ * counts, tag numbers, token estimates, reasons and durations; every string is
15
+ * truncated and every array capped by {@link redactData}, so a long session
16
+ * cannot bloat the store.
17
+ *
18
+ * The review side lives in `scripts/mc-report.mjs`, which reads this table
19
+ * read-only and prints per-day rollups, key metrics and health hints.
20
+ */
21
+ import { appendFileSync, mkdirSync, renameSync, statSync } from 'node:fs';
22
+ import { dirname } from 'node:path';
23
+ import { errText } from "./types.js";
24
+ /**
25
+ * Close the historian runs a previous mount left open. An hmr remount (or an
26
+ * app restart) disposes the stores while a run is in flight, so its `start`
27
+ * row never gets a terminal sibling — indistinguishable from a wedged run and
28
+ * a standing acceptance failure. At mount time no run of the NEW composition
29
+ * can be in flight yet, so every still-open start predates this process and
30
+ * gets exactly one synthetic terminal (`kind: 'adapter-remount'`). The audit
31
+ * invariant — start + exactly one terminal — is restored, not rewritten:
32
+ * the original start row stays untouched.
33
+ *
34
+ * @returns the number of recovered runs.
35
+ */
36
+ export function recoverOrphanHistorianRuns(stores, journal) {
37
+ const recovered = [];
38
+ try {
39
+ const rows = stores.listJournal({ kinds: ['historian'], limit: 10_000 });
40
+ // Pass 1: starts already banked by a previous mount carry
41
+ // `recoveredStartAt` on their synthetic terminal. The markers sit at the
42
+ // journal tail — after the historic rows they repair — so they must be
43
+ // collected BEFORE pairing, or the supersede branch would re-report the
44
+ // same historic double-start on every remount.
45
+ const alreadyRecovered = new Set();
46
+ for (const row of rows) {
47
+ const data = row.data;
48
+ if (data?.phase === 'failed' && typeof data.recoveredStartAt === 'number') {
49
+ alreadyRecovered.add(`${row.sessionId ?? '-'}:${String(data.recoveredStartAt)}`);
50
+ }
51
+ }
52
+ // Pass 2: the pairing scan (chronological).
53
+ const open = new Map();
54
+ for (const row of [...rows].reverse()) {
55
+ const phase = row.data?.phase;
56
+ const key = row.sessionId ?? '-';
57
+ if (phase === 'start') {
58
+ // A second start in one session means the first was superseded while
59
+ // still open — the pairing maps would silently overwrite it (the same
60
+ // shape the acceptance audit counts as a silent evaporate), so the
61
+ // superseded start is recovered right here.
62
+ const previous = open.get(key);
63
+ if (previous !== undefined && !alreadyRecovered.has(`${key}:${String(previous.at)}`)) {
64
+ recovered.push({ entry: previous, sessionId: row.sessionId });
65
+ }
66
+ open.set(key, row);
67
+ }
68
+ else if (phase === 'done' || phase === 'failed') {
69
+ open.delete(key);
70
+ }
71
+ }
72
+ for (const [key, start] of open) {
73
+ if (!alreadyRecovered.has(`${key}:${String(start.at)}`)) {
74
+ recovered.push({ entry: start, sessionId: key === '-' ? undefined : key });
75
+ }
76
+ }
77
+ }
78
+ catch {
79
+ return 0;
80
+ }
81
+ let count = 0;
82
+ for (const { entry: start, sessionId } of recovered) {
83
+ try {
84
+ journal.error('historian', 'historian run failed (adapter-remount): a previous mount was disposed before this run finished', {
85
+ phase: 'failed',
86
+ kind: 'adapter-remount',
87
+ reason: 'the adapter remounted (hmr or restart) while the run was in flight',
88
+ recovered: true,
89
+ recoveredStartAt: start.at,
90
+ }, sessionId);
91
+ count += 1;
92
+ }
93
+ catch {
94
+ /* a failing recovery must never block the mount */
95
+ }
96
+ }
97
+ return count;
98
+ }
99
+ const LEVEL_RANK = { info: 0, warn: 1, error: 2 };
100
+ const MAX_STRING = 300;
101
+ const MAX_ARRAY = 20;
102
+ const MAX_DEPTH = 3;
103
+ /**
104
+ * Bound one entry's structured payload: truncate long strings, cap arrays,
105
+ * limit nesting. Message content must never be passed here in the first place;
106
+ * this is the safety net that keeps a slip from bloating the store.
107
+ */
108
+ export function redactData(value, depth = 0) {
109
+ if (value === null || typeof value === 'number' || typeof value === 'boolean')
110
+ return value;
111
+ if (typeof value === 'string') {
112
+ return value.length <= MAX_STRING ? value : `${value.slice(0, MAX_STRING)}…(${String(value.length)} chars)`;
113
+ }
114
+ if (value === undefined)
115
+ return undefined;
116
+ if (depth >= MAX_DEPTH)
117
+ return '[depth]';
118
+ if (Array.isArray(value)) {
119
+ const items = value.slice(0, MAX_ARRAY).map(item => redactData(item, depth + 1));
120
+ return value.length > MAX_ARRAY ? [...items, `…(${String(value.length)} items)`] : items;
121
+ }
122
+ if (typeof value === 'object') {
123
+ const source = value;
124
+ const result = {};
125
+ let index = 0;
126
+ for (const [key, item] of Object.entries(source)) {
127
+ if (index >= MAX_ARRAY) {
128
+ result['…'] = 'more keys omitted';
129
+ break;
130
+ }
131
+ if (item === undefined)
132
+ continue;
133
+ result[key] = redactData(item, depth + 1);
134
+ index += 1;
135
+ }
136
+ return result;
137
+ }
138
+ return String(value);
139
+ }
140
+ /** One mirror line: `ISO LEVEL kind [session] summary {json}`. */
141
+ export function formatJournalLine(entry) {
142
+ const iso = new Date(entry.at).toISOString();
143
+ const session = entry.sessionId === undefined ? '' : ` ${entry.sessionId.slice(0, 12)}`;
144
+ const data = entry.data === undefined || Object.keys(entry.data).length === 0
145
+ ? ''
146
+ : ` ${JSON.stringify(entry.data)}`;
147
+ return `${iso} ${entry.level.toUpperCase().padEnd(5)} ${entry.kind}${session} ${entry.summary}${data}\n`;
148
+ }
149
+ /** Rotate the mirror once at open so it cannot grow without bound. */
150
+ function rotateMirror(path, maxBytes) {
151
+ try {
152
+ const size = statSync(path).size;
153
+ if (size > maxBytes)
154
+ renameSync(path, `${path}.1`);
155
+ }
156
+ catch {
157
+ /* missing file is the normal first-open case */
158
+ }
159
+ }
160
+ /**
161
+ * Open the journal. Pruning and mirror rotation happen once here; every later
162
+ * write is a single small insert. Journal failures never propagate: they are
163
+ * reported through the harness logger and the adapter keeps working (the
164
+ * journal must not be able to break a turn).
165
+ */
166
+ export function openJournal(options) {
167
+ const now = options.now ?? (() => Date.now());
168
+ const retentionDays = options.retentionDays ?? 30;
169
+ const mirrorMaxBytes = options.mirrorMaxBytes ?? 5 * 1024 * 1024;
170
+ const resolveLevel = () => {
171
+ if (typeof options.level !== 'function')
172
+ return options.level ?? 'info';
173
+ return options.level() ?? 'info';
174
+ };
175
+ const resolveMirror = () => {
176
+ const path = typeof options.mirrorPath === 'function' ? options.mirrorPath() : options.mirrorPath;
177
+ return typeof path === 'string' && path.length > 0 ? path : undefined;
178
+ };
179
+ /** Each distinct mirror path is prepared (mkdir + rotate) exactly once. */
180
+ const preparedMirrors = new Set();
181
+ const prepareMirror = (path) => {
182
+ if (preparedMirrors.has(path))
183
+ return;
184
+ preparedMirrors.add(path);
185
+ try {
186
+ mkdirSync(dirname(path), { recursive: true });
187
+ rotateMirror(path, mirrorMaxBytes);
188
+ }
189
+ catch (error) {
190
+ options.logger?.warn(`magic-context journal: mirror unavailable: ${errText(error)}`);
191
+ }
192
+ };
193
+ try {
194
+ const pruned = options.stores.pruneJournal(now() - retentionDays * 24 * 60 * 60_000);
195
+ if (pruned > 0)
196
+ options.logger?.info(`magic-context journal: pruned ${String(pruned)} entries past retention`);
197
+ }
198
+ catch (error) {
199
+ options.logger?.warn(`magic-context journal: retention prune failed: ${errText(error)}`);
200
+ }
201
+ const write = (entry) => {
202
+ try {
203
+ options.stores.appendJournal(entry);
204
+ }
205
+ catch (error) {
206
+ options.logger?.warn(`magic-context journal: write failed (${entry.kind}): ${errText(error)}`);
207
+ }
208
+ const mirror = resolveMirror();
209
+ if (mirror !== undefined) {
210
+ prepareMirror(mirror);
211
+ try {
212
+ appendFileSync(mirror, formatJournalLine(entry));
213
+ }
214
+ catch {
215
+ /* the mirror is a convenience; the database is authoritative */
216
+ }
217
+ }
218
+ };
219
+ const record = (level, kind, summary, data, sessionId) => {
220
+ if (LEVEL_RANK[level] < LEVEL_RANK[resolveLevel()])
221
+ return;
222
+ const entry = {
223
+ at: now(),
224
+ level,
225
+ kind,
226
+ summary,
227
+ ...sessionId === undefined ? {} : { sessionId },
228
+ ...data === undefined ? {} : { data: redactData(data) },
229
+ };
230
+ write(entry);
231
+ if (level === 'error')
232
+ options.logger?.warn(`magic-context [${kind}] ${summary}`);
233
+ };
234
+ return {
235
+ enabled: true,
236
+ record,
237
+ info: (kind, summary, data, sessionId) => record('info', kind, summary, data, sessionId),
238
+ warn: (kind, summary, data, sessionId) => record('warn', kind, summary, data, sessionId),
239
+ error: (kind, summary, data, sessionId) => record('error', kind, summary, data, sessionId),
240
+ tail: (limit = 50, sessionId) => options.stores.listJournal({
241
+ limit,
242
+ ...sessionId === undefined ? {} : { sessionId },
243
+ }),
244
+ byKind: since => options.stores.journalByKind(since),
245
+ /** Live: follows a getter-backed `mirrorPath` option without a reopen. */
246
+ get mirrorPath() { return resolveMirror(); },
247
+ };
248
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Wall-clock budgets for one auxiliary LLM round trip.
3
+ *
4
+ * The historian has carried this guard since it was wired (see
5
+ * `src/dsh/historian.ts` `collectAttempt`): its policy note spells the rule out —
6
+ * "a provider that never settles must not leave the run unaccounted". The
7
+ * maintenance lane (verify + profile review) never got one, and there the
8
+ * consequence is worse than a lost row: the ticker's `setInterval` is installed
9
+ * only after the FIRST tick resolves, so one never-settling stream would stop
10
+ * TTL expiry, verification, profile review and vector backfill for the whole
11
+ * life of the process — silently, because the lane simply never reports again.
12
+ *
13
+ * The budget is enforced twice on purpose, exactly like the historian's:
14
+ *
15
+ * - the abort signal tells a compliant provider stream to stop, so the socket is
16
+ * released instead of lingering until the host's own transport timeout;
17
+ * - `Promise.race` rejects even when the seam ignores the signal entirely, which
18
+ * is the case this module exists for (a stream that yields nothing forever
19
+ * never observes an abort).
20
+ */
21
+ export class LlmDeadlineError extends Error {
22
+ budgetMs;
23
+ constructor(budgetMs) {
24
+ super(`no terminal event within ${String(budgetMs)}ms`);
25
+ this.name = 'LlmDeadlineError';
26
+ this.budgetMs = budgetMs;
27
+ }
28
+ }
29
+ /**
30
+ * Run `work` against a fresh abort signal, rejecting with {@link LlmDeadlineError}
31
+ * once `budgetMs` elapses. The timer is `unref`ed so a pending budget never holds
32
+ * the host process open by itself.
33
+ */
34
+ export async function withDeadline(work, budgetMs) {
35
+ const controller = new AbortController();
36
+ let timer;
37
+ const expiry = new Promise((_resolve, reject) => {
38
+ timer = setTimeout(() => {
39
+ // Abort first: a stream that *would* have stopped on a signal gets the
40
+ // chance to release its socket before the rejection unwinds.
41
+ controller.abort(new LlmDeadlineError(budgetMs));
42
+ reject(new LlmDeadlineError(budgetMs));
43
+ }, budgetMs);
44
+ timer.unref?.();
45
+ });
46
+ try {
47
+ return await Promise.race([work(controller.signal), expiry]);
48
+ }
49
+ finally {
50
+ if (timer !== undefined)
51
+ clearTimeout(timer);
52
+ }
53
+ }