@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,588 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ /**
3
+ * Provider-neutral code the adapter's emergency band reacts to. Defined
4
+ * locally on purpose: a runtime import here would break the cross-repo
5
+ * patch-overlay loading mode (the adapter's runtime has zero @deepseek-ai
6
+ * imports). Drift guard: upstream owns this string in `@deepseek-ai/dsh-llm`
7
+ * (exported as CONTEXT_WINDOW_EXCEEDED_CODE); the seam contract test pins it.
8
+ */
9
+ export const CONTEXT_WINDOW_EXCEEDED_CODE = 'CONTEXT_WINDOW_EXCEEDED';
10
+ /** The seam event names, centralized so renames touch one file. */
11
+ export const DSH_EVENTS = {
12
+ preStep: 'agent/pre-step',
13
+ postToolExecute: 'tools/post-execute',
14
+ sessionEvent: 'session/event',
15
+ requestError: 'agent/request-error',
16
+ };
17
+ /**
18
+ * Admission-time message stamping is deliberately ABSENT (removed 2026-10-08).
19
+ *
20
+ * `tagUserMessage` used to prefix `§N§ ` onto the admitted user batch. Those
21
+ * handles were dead: every reclaim lane in this adapter resolves tags against
22
+ * surface tool results only, so `ctx_reduce` refused them ("no such tool-output
23
+ * tag on the current surface"). The prefix, however, is durable, so it reached
24
+ * the conversation transcript and — through `session-title`'s deterministic
25
+ * first-prompt fallback, which reads the first `source: 'user'` message — the
26
+ * session title. opencode/pi never had this problem because they stamp a
27
+ * per-call array the host throws away; DSH derives every request from the log,
28
+ * so nothing stamped here can stay invisible. Tool results still carry their
29
+ * handle: they are stamped at landing (`tools/post-execute`, index.ts), the one
30
+ * lane with reclaim consumers. Ruling: `docs/dsh-port-feasibility-2026-10-06.md`
31
+ * §7.4.
32
+ */
33
+ /** A tagged copy of a tool-result content projection. */
34
+ export function tagToolResultContent(content, prefix, tag) {
35
+ const firstText = content.findIndex(block => block.type === 'text');
36
+ if (firstText === -1) {
37
+ return [{ type: 'text', text: prefix(tag) }, ...content];
38
+ }
39
+ return content.map((block, index) => index === firstText && block.type === 'text'
40
+ ? { ...block, text: `${prefix(tag)}${block.text}` }
41
+ : block);
42
+ }
43
+ /**
44
+ * Durable drop: replace one `tool/result` surface node with a placeholder
45
+ * citing the original event, so replay can recover the full-fidelity result
46
+ * (the DSH spelling of Magic Context's `[dropped §N§]` + ctx_expand).
47
+ * Returns the replacement event's seq.
48
+ */
49
+ export function replaceToolResultWithPlaceholder(session, targetSeq, placeholderText) {
50
+ return replaceToolResultContent(session, targetSeq, [{ type: 'text', text: placeholderText }]);
51
+ }
52
+ const DROPPED_PATTERN = /^\[dropped §(\d+)§\]$/;
53
+ /**
54
+ * The tag number of a `[dropped §N§]` placeholder content, when the content is
55
+ * one. The placeholder is the adapter's own landing format (`dropPlaceholderContent`),
56
+ * so the check is exact and anchored to the first block.
57
+ */
58
+ export function dropTagOfContent(content) {
59
+ const first = content[0];
60
+ const firstText = first?.type === 'text' ? first.text : undefined;
61
+ if (firstText === undefined)
62
+ return undefined;
63
+ const dropped = DROPPED_PATTERN.exec(firstText);
64
+ return dropped === null ? undefined : Number(dropped[1]);
65
+ }
66
+ /**
67
+ * Enumerate CURRENT tool-result surface nodes (model-visible order —
68
+ * `session.surface.nodes` excludes shadowed log events, which is what the
69
+ * replace validator requires targets to be).
70
+ * @param tagOf - resolves a node's adapter tag from its callId.
71
+ */
72
+ export function enumerateSurfaceToolResults(session, tagOf) {
73
+ const nodes = [];
74
+ const events = session.snapshotEvents();
75
+ const toolNameByCallId = new Map();
76
+ for (const event of events) {
77
+ if (event.type === 'tool/call') {
78
+ toolNameByCallId.set(event.data.callId, event.data.name);
79
+ }
80
+ }
81
+ for (const seq of session.surface.nodes) {
82
+ const event = events.find(candidate => candidate.seq === seq);
83
+ if (event?.type !== 'tool/result')
84
+ continue;
85
+ const callId = event.data.message.toolCallId;
86
+ const droppedTag = dropTagOfContent(event.data.message.content);
87
+ const textLength = event.data.message.content.reduce((total, block) => total + (block.type === 'text' ? block.text.length : 0), 0);
88
+ nodes.push({
89
+ seq,
90
+ callId,
91
+ turn: event.data.turn,
92
+ step: event.data.step,
93
+ tagNumber: tagOf(callId),
94
+ isPlaceholder: droppedTag !== undefined,
95
+ droppedTag,
96
+ ...toolNameByCallId.get(callId) === undefined ? {} : { toolName: toolNameByCallId.get(callId) },
97
+ textLength,
98
+ });
99
+ }
100
+ return nodes;
101
+ }
102
+ /**
103
+ * Replace one current tool-result node's CONTENT while preserving its whole
104
+ * identity envelope (the surface validator restricts tool/result rewrites to
105
+ * exactly the content field).
106
+ */
107
+ export function replaceToolResultContent(session, targetSeq, content) {
108
+ const events = session.snapshotEvents();
109
+ const original = events.find((event) => event.seq === targetSeq && event.type === 'tool/result');
110
+ if (original === undefined) {
111
+ throw new Error(`magic-context dsh adapter: seq ${targetSeq} is not a tool/result surface node`);
112
+ }
113
+ const { content: _replaced, ...messageRest } = original.data.message;
114
+ const replacement = session.append('tool/result', {
115
+ turn: original.data.turn,
116
+ step: original.data.step,
117
+ message: { ...messageRest, content: [...content] },
118
+ ...original.data.error === undefined ? {} : { error: original.data.error },
119
+ ...original.data.meta === undefined ? {} : { meta: original.data.meta },
120
+ }, {
121
+ surfaceOp: { op: 'replace', startSeq: targetSeq, endSeq: targetSeq },
122
+ sourceEventSeqs: [original.seq],
123
+ });
124
+ return replacement.seq;
125
+ }
126
+ /** The `[dropped §N§]` placeholder content (Magic Context's canonical string). */
127
+ export function dropPlaceholderContent(tagNumber) {
128
+ return [{ type: 'text', text: `[dropped §${tagNumber}§]` }];
129
+ }
130
+ /** The full-fidelity original content cited by a placeholder node, when recoverable. */
131
+ export function originalContentOfPlaceholder(session, placeholderSeq) {
132
+ const events = session.snapshotEvents();
133
+ const node = events.find((event) => event.seq === placeholderSeq);
134
+ if (node?.type !== 'tool/result')
135
+ return undefined;
136
+ const cited = node.sourceEventSeqs;
137
+ if (cited === undefined || cited.length !== 1)
138
+ return undefined;
139
+ const original = events.find((event) => event.seq === cited[0]);
140
+ if (original?.type !== 'tool/result')
141
+ return undefined;
142
+ return [...original.data.message.content];
143
+ }
144
+ /**
145
+ * The original result content of a persisted tool call, resolved from the
146
+ * session log by call identity — the read side of `ctx_expand`'s result-return
147
+ * path. Never requires a placeholder to be live: a dropped node's original
148
+ * event stays in the log (shadowed, not deleted), and a legacy in-place
149
+ * restore is itself a non-placeholder event.
150
+ *
151
+ * Prefers a full-fidelity event; when only placeholders remain it follows
152
+ * their cited source (the same citation path `originalContentOfPlaceholder`
153
+ * uses). Never fabricates and never re-executes the tool.
154
+ */
155
+ export function originalContentOfToolCall(session, callId) {
156
+ const events = session.snapshotEvents();
157
+ const results = [];
158
+ for (const event of events) {
159
+ if (event.type !== 'tool/result')
160
+ continue;
161
+ if (event.data.message.toolCallId !== callId)
162
+ continue;
163
+ results.push(event);
164
+ }
165
+ if (results.length === 0)
166
+ return { kind: 'missing-event' };
167
+ let droppedTag;
168
+ for (const event of results) {
169
+ droppedTag ??= dropTagOfContent(event.data.message.content);
170
+ }
171
+ const withDrop = droppedTag === undefined ? {} : { droppedTag };
172
+ // The call's own full-fidelity result (its landing, or a legacy in-place
173
+ // restore). Placeholders only cite such an event, so the first one is the
174
+ // original.
175
+ for (const event of results) {
176
+ if (dropTagOfContent(event.data.message.content) !== undefined)
177
+ continue;
178
+ return { kind: 'content', content: [...event.data.message.content], ...withDrop };
179
+ }
180
+ // Only placeholders remain: follow the newest cited original (the landing
181
+ // replaces the original on the surface but both stay in the log).
182
+ for (const event of [...results].reverse()) {
183
+ const content = originalContentOfPlaceholder(session, event.seq);
184
+ if (content === undefined)
185
+ continue;
186
+ if (dropTagOfContent(content) !== undefined)
187
+ continue;
188
+ return { kind: 'content', content, ...withDrop };
189
+ }
190
+ return { kind: 'missing-source', ...withDrop };
191
+ }
192
+ /**
193
+ * Resolve a DSH log event seq to a persisted `tool/result` identity without
194
+ * reading its content. The three-way result keeps "no such seq" distinct from
195
+ * "seq exists but is not a tool/result" for the `ctx_expand` message locator;
196
+ * call-level placeholder evidence comes from `originalContentOfToolCall`.
197
+ */
198
+ export function toolResultAt(session, seq) {
199
+ const event = session.snapshotEvents().find(candidate => candidate.seq === seq);
200
+ if (event === undefined)
201
+ return { kind: 'unknown' };
202
+ if (event.type !== 'tool/result')
203
+ return { kind: 'not-tool-result', seq: event.seq };
204
+ return { kind: 'tool-result', seq: event.seq, callId: event.data.message.toolCallId };
205
+ }
206
+ /** The logged request envelope facts pressure math needs. */
207
+ export function requestContextOf(session) {
208
+ const context = session.requestContext();
209
+ if (context === undefined)
210
+ return {};
211
+ return {
212
+ provider: context.provider,
213
+ model: context.model,
214
+ contextWindow: context.contextWindow,
215
+ };
216
+ }
217
+ /** Neutral usage fields read from an `assistant/message` durable event. */
218
+ export function readUsageFields(event) {
219
+ if (event.type !== 'assistant/message')
220
+ return undefined;
221
+ // Tolerant reads: this helper runs in the `session/event` listener, which the
222
+ // host emits on EVERY append (the user's own messages included). A drift in
223
+ // the assistant event shape must skip the usage row (fail-open; the next
224
+ // assistant message anchors a fresh one), not throw into that emission.
225
+ const data = event.data;
226
+ const source = data?.message?.source;
227
+ const usage = data?.usage;
228
+ if (source === undefined)
229
+ return undefined;
230
+ return {
231
+ seq: event.seq,
232
+ turn: data?.turn ?? 0,
233
+ step: data?.step ?? 0,
234
+ provider: source.provider,
235
+ model: source.model,
236
+ inputTokens: usage?.inputTokens,
237
+ outputTokens: usage?.outputTokens,
238
+ cacheReadTokens: usage?.cacheReadTokens,
239
+ cacheWriteTokens: usage?.cacheWriteTokens,
240
+ };
241
+ }
242
+ /** Whether a Cordis context exposes the seam surface this adapter needs. */
243
+ export function probeDshSeams(ctx) {
244
+ const problems = [];
245
+ if (typeof ctx.on !== 'function')
246
+ problems.push('ctx.on is not a function (Cordis event API missing)');
247
+ if (typeof ctx.logger?.info !== 'function')
248
+ problems.push('ctx.logger missing (diagnostics would be invisible)');
249
+ return { ok: problems.length === 0, problems };
250
+ }
251
+ const ROLE_BY_TYPE = {
252
+ 'system/message': 'system',
253
+ 'developer/message': 'developer',
254
+ 'user/message': 'user',
255
+ 'assistant/message': 'assistant',
256
+ 'tool/result': 'tool',
257
+ };
258
+ const PLACEHOLDER_PATTERN = /^\[(?:dropped|compacted) §(?:C)?\d+§\]$/;
259
+ /**
260
+ * The message content blocks of a message-bearing surface event.
261
+ *
262
+ * Both shapes matter: reading only `data.message.content` left every user node
263
+ * with empty text, which silently disabled `refreshBaseline`'s byte-identical
264
+ * short-circuit — i.e. every execute pass rewrote the baseline node (new
265
+ * `mc-baseline-<uuid>` id, replaceGeneration++) instead of leaving the cache
266
+ * prefix untouched (measured: docs/dsh-plugin-memory-goal-status).
267
+ */
268
+ function contentOf(event) {
269
+ const data = event.data;
270
+ return data?.message?.content ?? data?.content ?? [];
271
+ }
272
+ /** Concatenated text of a surface event, ignoring tool-call/reasoning blocks. */
273
+ export function textOfSurfaceEvent(event) {
274
+ return contentOf(event)
275
+ .filter((block) => block.type === 'text')
276
+ .map(block => block.text)
277
+ .join('\n');
278
+ }
279
+ /**
280
+ * `data.source.kind` of a message-bearing surface event. `user/message` carries
281
+ * the message itself as its data, so the source sits on `data.source`; other
282
+ * message types wrap it (`data.message.source`).
283
+ */
284
+ function sourceKindOf(event) {
285
+ const data = event.data;
286
+ const kind = data?.source?.kind ?? data?.message?.source?.kind;
287
+ return typeof kind === 'string' ? kind : undefined;
288
+ }
289
+ /** The provider call ids requested by a node's `tool-call` blocks. */
290
+ function toolCallIdsOf(blocks) {
291
+ return blocks
292
+ .filter((block) => block.type === 'tool-call')
293
+ .map(block => block.id);
294
+ }
295
+ /** One node view, or undefined when the event carries no surface message. */
296
+ function surfaceNodeOf(seq, event) {
297
+ const role = ROLE_BY_TYPE[event.type];
298
+ if (role === undefined)
299
+ return undefined;
300
+ const text = textOfSurfaceEvent(event);
301
+ const sourceKind = sourceKindOf(event);
302
+ const blocks = contentOf(event);
303
+ return {
304
+ seq,
305
+ type: event.type,
306
+ role,
307
+ text,
308
+ hasText: text.trim().length > 0,
309
+ isPlaceholder: PLACEHOLDER_PATTERN.test(text.trim()),
310
+ hasToolCalls: blocks.some(block => block.type === 'tool-call'),
311
+ ...(role === 'assistant' ? { toolCallIds: toolCallIdsOf(blocks) } : {}),
312
+ ...(event.type === 'tool/result' ? { toolCallId: event.data.message.toolCallId } : {}),
313
+ ...sourceKind === undefined ? {} : { sourceKind },
314
+ };
315
+ }
316
+ /**
317
+ * Enumerate the CURRENT surface in provider-visible order. `session.surface.nodes`
318
+ * excludes shadowed events, which is exactly the live window the historian may
319
+ * digest and the publish lane may replace.
320
+ */
321
+ export function enumerateSurfaceNodes(session) {
322
+ const events = session.snapshotEvents();
323
+ const bySeq = new Map(events.map(event => [event.seq, event]));
324
+ const nodes = [];
325
+ for (const seq of session.surface.nodes) {
326
+ const event = bySeq.get(seq);
327
+ if (event === undefined)
328
+ continue;
329
+ const node = surfaceNodeOf(seq, event);
330
+ if (node !== undefined)
331
+ nodes.push(node);
332
+ }
333
+ return nodes;
334
+ }
335
+ /**
336
+ * Widen the window `nodes[first..last]` until no live node pairs across the
337
+ * window boundary, and pull in stranded results — a `tool/result` whose
338
+ * `toolCallId` no live assistant requests can never pair again, so folding it
339
+ * into the same replacement is the only way to get it off the wire.
340
+ *
341
+ * Why this exists: the provider rejects a request whose `tool_result` blocks
342
+ * lack the matching `tool_use` in the preceding assistant message. Publishing
343
+ * a range that cuts a pair strands one side and the whole next turn fails with
344
+ * `invalid_request_error` — observed live 2026-10-09, when drop placeholders
345
+ * (`[dropped §N§]`) re-sequenced results to the surface tail moments before
346
+ * the publish computed its positional range.
347
+ *
348
+ * Widening is a positional range operation — everything between the old window
349
+ * and a pulled-in node is shadowed by the same replacement — so the window only
350
+ * ever grows and never splits. Orphans are pulled forward-only so the window
351
+ * cannot drift backward onto the frozen m[0] baseline at the surface head.
352
+ * @returns the closed window; callers must slice `nodes` with it.
353
+ */
354
+ export function pairingClosedWindow(nodes, first, last) {
355
+ const liveCallIds = new Set();
356
+ for (const node of nodes) {
357
+ for (const id of node.toolCallIds ?? [])
358
+ liveCallIds.add(id);
359
+ }
360
+ let lo = first;
361
+ let hi = last;
362
+ let changed = true;
363
+ while (changed) {
364
+ changed = false;
365
+ const windowCallIds = new Set();
366
+ const windowResultIds = new Set();
367
+ for (let i = lo; i <= hi; i += 1) {
368
+ const node = nodes[i];
369
+ if (node === undefined)
370
+ continue;
371
+ for (const id of node.toolCallIds ?? [])
372
+ windowCallIds.add(id);
373
+ if (node.toolCallId !== undefined)
374
+ windowResultIds.add(node.toolCallId);
375
+ }
376
+ for (let i = 0; i < nodes.length; i += 1) {
377
+ if (i >= lo && i <= hi)
378
+ continue;
379
+ const node = nodes[i];
380
+ if (node === undefined)
381
+ continue;
382
+ const pairsWithWindow = (node.toolCallId !== undefined && windowCallIds.has(node.toolCallId))
383
+ || (node.toolCallIds ?? []).some(id => windowResultIds.has(id));
384
+ const stranded = node.toolCallId !== undefined && !liveCallIds.has(node.toolCallId) && i > hi;
385
+ if (!pairsWithWindow && !stranded)
386
+ continue;
387
+ if (i < lo) {
388
+ lo = i;
389
+ changed = true;
390
+ }
391
+ if (i > hi) {
392
+ hi = i;
393
+ changed = true;
394
+ }
395
+ }
396
+ }
397
+ return { first: lo, last: hi };
398
+ }
399
+ /**
400
+ * Live `tool/result` nodes whose tool-call id no live assistant requests.
401
+ *
402
+ * One of these on the wire fails every strict-provider request
403
+ * (`invalid_request_error: unexpected tool_use_id ... found in tool_result
404
+ * blocks`) — the one adapter failure mode that can take the main conversation
405
+ * down. The decision layer forces the execute band when this is non-empty and
406
+ * the pass's repair lane sweeps it. Light on purpose: no text extraction, so
407
+ * the pre-step can afford it every model step.
408
+ */
409
+ export function wireStrandedResults(session) {
410
+ const events = session.snapshotEvents();
411
+ const bySeq = new Map(events.map(event => [event.seq, event]));
412
+ const liveCallIds = new Set();
413
+ for (const seq of session.surface.nodes) {
414
+ const event = bySeq.get(seq);
415
+ if (event?.type !== 'assistant/message')
416
+ continue;
417
+ for (const block of contentOf(event)) {
418
+ if (block.type === 'tool-call')
419
+ liveCallIds.add(block.id);
420
+ }
421
+ }
422
+ const stranded = [];
423
+ for (const seq of session.surface.nodes) {
424
+ const event = bySeq.get(seq);
425
+ if (event?.type !== 'tool/result')
426
+ continue;
427
+ const callId = event.data.message.toolCallId;
428
+ if (!liveCallIds.has(callId))
429
+ stranded.push({ seq, callId });
430
+ }
431
+ return stranded;
432
+ }
433
+ /**
434
+ * Node views for EXPLICIT seqs, shadowed events included. The recomp lane
435
+ * re-digests history that already left the surface (a replacement cites the
436
+ * events it shadowed, and they stay in the log forever), so
437
+ * `enumerateSurfaceNodes` — the live window — cannot serve it.
438
+ * @param session - the session whose log is the source of truth.
439
+ * @param seqs - the seqs to render, in the order they should appear.
440
+ * @returns one node per resolvable message-bearing event, skipping the rest.
441
+ */
442
+ export function nodesForSeqs(session, seqs) {
443
+ const events = session.snapshotEvents();
444
+ const bySeq = new Map(events.map(event => [event.seq, event]));
445
+ const nodes = [];
446
+ for (const seq of seqs) {
447
+ const event = bySeq.get(seq);
448
+ if (event === undefined)
449
+ continue;
450
+ const node = surfaceNodeOf(seq, event);
451
+ if (node !== undefined)
452
+ nodes.push(node);
453
+ }
454
+ return nodes;
455
+ }
456
+ /**
457
+ * The provider-visible messages of a surface span, in surface order — the
458
+ * prefix replayed into an auxiliary (historian) call so the request stays a
459
+ * true prefix of the last routed one and the provider's KV cache is reused.
460
+ *
461
+ * The message objects are passed through VERBATIM (identity envelope
462
+ * included): the runtime reads `source.replayState` off assistant messages
463
+ * (packages/llm/llm/src/index.ts:985), and a reduced projection would both
464
+ * break that and lose the cache-relevant bytes.
465
+ */
466
+ /** The provider message of a message-bearing surface event, in the exact shapes
467
+ * `deriveEventMessage` uses (packages/core/session/src/surface.ts:132-145):
468
+ * `user/message` carries the message ITSELF as `event.data`, while
469
+ * system/developer/assistant/tool wrap it in `data.message`. Reading only the
470
+ * wrapped shape silently dropped every user turn from the historian's replayed
471
+ * prefix.
472
+ */
473
+ function messageOf(event) {
474
+ const data = event.data;
475
+ if (data === undefined)
476
+ return undefined;
477
+ return event.type === 'user/message' ? data : data.message;
478
+ }
479
+ /**
480
+ * Replay the request messages a set of surface seqs would produce.
481
+ *
482
+ * The return type is the LLM seam's own message contract: this is the one wire
483
+ * boundary the adapter owns, and its consumer (the historian) feeds the result
484
+ * straight to `ctx.llm.stream`. The events' payloads arrive as `unknown`, so the
485
+ * per-message cast happens here rather than at every call site.
486
+ */
487
+ export function spanRequestMessages(session, seqs) {
488
+ const events = session.snapshotEvents();
489
+ const bySeq = new Map(events.map(event => [event.seq, event]));
490
+ const messages = [];
491
+ const requested = new Set();
492
+ for (const seq of seqs) {
493
+ const event = bySeq.get(seq);
494
+ if (event === undefined)
495
+ continue;
496
+ if (ROLE_BY_TYPE[event.type] === undefined)
497
+ continue;
498
+ const message = messageOf(event);
499
+ if (message === undefined)
500
+ continue;
501
+ // A request is a wire boundary: a provider rejects a tool result whose
502
+ // tool-call is not in the same request (`host: packages/core/session/src/
503
+ // repair.ts:164` states the mirror rule — "providers reject dangling
504
+ // assistant calls"). Every edge this adapter controls is kept pairing-safe
505
+ // (`planChunkSpans`, the historian's span scan), but a surface written by an
506
+ // OLDER build can still hold an interior orphan: the publish lane used to
507
+ // cite only the covered subset and the live journal carries the 400 it
508
+ // produced. Drop the orphan rather than send a request that cannot succeed.
509
+ if (event.type === 'assistant/message') {
510
+ const content = message.content ?? [];
511
+ for (const block of content) {
512
+ if (block.type === 'tool-call' && typeof block.id === 'string')
513
+ requested.add(block.id);
514
+ }
515
+ }
516
+ else if (event.type === 'tool/result') {
517
+ const callId = message.toolCallId;
518
+ if (typeof callId === 'string' && !requested.has(callId))
519
+ continue;
520
+ }
521
+ messages.push(message);
522
+ }
523
+ return messages;
524
+ }
525
+ /**
526
+ * A plugin-injected context source — the ONE shape this adapter uses for every
527
+ * synthetic node it authors (baseline refresh, compartment publication, pass
528
+ * notices). `form`/`summary` are the host's display vocabulary
529
+ * (ui-chat ContextBody.tsx:551-591; `notice` is the only form that renders its
530
+ * summary on a collapsed row), and the producer label comes from `kind`
531
+ * (ui-chat conversation-nodes/event-projection.ts:60-76). The model keeps
532
+ * reading the full content either way: only the presentation changes.
533
+ *
534
+ * Synthetic nodes must NOT claim `{ kind: 'user' }`. They are model-only by
535
+ * contract (`session/src/surface.ts:80-85`: "replacement copies stay
536
+ * model-only"), but `session-title` folds the log for the first
537
+ * `source.kind === 'user'` message and would count a plugin-authored node as
538
+ * authored material.
539
+ * @param summary - the collapsed row's one-line account.
540
+ * @returns the durable source object.
541
+ */
542
+ export function magicContextSource(summary) {
543
+ return { kind: 'magic-context', form: 'notice', summary };
544
+ }
545
+ /**
546
+ * The host's compaction-checkpoint source (`@deepseek-ai/dsh-compaction`
547
+ * checkpoint.ts:33 `compactCheckpointSource`). Defined locally like every other
548
+ * seam constant: this adapter's runtime has zero `@deepseek-ai/*` imports (the
549
+ * patch-overlay loading mode). The marker is load-bearing — the Chat client
550
+ * correlates a compaction card to its checkpoint by exactly this kind
551
+ * (ui-chat conversation-nodes/command.ts:82-92) — so a checkpoint written with
552
+ * any other source is invisible to that correlation.
553
+ * @param compactionId - the owning `compaction/start … compaction/end` identity.
554
+ * @param sourceCommandId - the initiating manual command, when present.
555
+ * @returns the durable source object.
556
+ */
557
+ export function compactCheckpointSource(compactionId, sourceCommandId) {
558
+ return {
559
+ kind: 'compact-checkpoint',
560
+ compactionId,
561
+ ...sourceCommandId === undefined ? {} : { sourceCommandId },
562
+ };
563
+ }
564
+ /**
565
+ * Replace a surface RANGE with one fresh `user/message` node, citing every
566
+ * shadowed event. The range is expressed as the seqs of its first and last
567
+ * current node (surface positions — the validator accepts either numeric
568
+ * order). Returns the replacement event's seq.
569
+ * @param source - the synthetic node's durable source: `magicContextSource`
570
+ * for this adapter's own nodes, `compactCheckpointSource` for a real
571
+ * compaction checkpoint. Never `{ kind: 'user' }` (see `magicContextSource`).
572
+ */
573
+ export function replaceSurfaceRange(session, firstSeq, lastSeq, content, shadowed, id, source) {
574
+ const replacement = session.append('user/message', {
575
+ id,
576
+ role: 'user',
577
+ content: [...content],
578
+ source,
579
+ }, {
580
+ surfaceOp: { op: 'replace', startSeq: firstSeq, endSeq: lastSeq },
581
+ sourceEventSeqs: [...shadowed],
582
+ });
583
+ return replacement.seq;
584
+ }
585
+ /** A fresh synthetic-node identity (the session validates message shapes, not ids). */
586
+ export function syntheticNodeId(prefix) {
587
+ return `${prefix}-${randomUUID()}`;
588
+ }