dsh-mnemon 0.5.8 → 0.5.10

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.
@@ -4,18 +4,428 @@ import { createReadStream, realpathSync } from 'node:fs'
4
4
  import { link, mkdtemp, open, rm } from 'node:fs/promises'
5
5
  import { dirname, join, resolve } from 'node:path'
6
6
  import { fileURLToPath } from 'node:url'
7
+ import { isDeepStrictEqual } from 'node:util'
7
8
  import * as zlib from 'node:zlib'
8
9
 
9
10
  const maximumBytes = 128 * 1024 * 1024
10
11
  const summaries = {
11
- recall: 'Memory View snapshot',
12
- instructions: 'Optional memory recall and remember reminder',
12
+ recall: ['Memory View snapshot', 'Runtime memory snapshot'],
13
+ instructions: ['Optional memory recall and remember reminder'],
13
14
  }
14
15
  const sha256 = value => createHash('sha256').update(value).digest('hex')
16
+ const record = value => value !== null && typeof value === 'object' && !Array.isArray(value)
17
+ const nonempty = value => typeof value === 'string' && value.length > 0
18
+ const integer = value => Number.isSafeInteger(value) && !Object.is(value, -0)
19
+ const count = value => integer(value) && value >= 0
20
+ const exactKeys = (value, required, optional = []) => record(value) && required.every(key => Object.hasOwn(value, key)) &&
21
+ Object.keys(value).every(key => required.includes(key) || optional.includes(key))
22
+
23
+ // Official dsh-subagent 0.1.1-rc.2 v2 -> 0.1.2-alpha.2 v3 adds only the
24
+ // optional agentReasoningEffort composition input. With that field absent,
25
+ // these exact old records reconstruct the same declared child options. Apply
26
+ // the stricter frozen-v3 constraints too; accepting v2 alone is insufficient.
27
+ function supportedDescriptorV2(value) {
28
+ if (!record(value) || value.version !== 2 || !nonempty(value.provider)) return false
29
+ const base = ['version', 'mode', 'provider', 'label']
30
+ if (value.mode === 'one-shot') return Object.keys(value).every(key => base.includes(key)) &&
31
+ (!Object.hasOwn(value, 'label') || typeof value.label === 'string')
32
+ if (value.mode !== 'continuable' || !nonempty(value.label)) return false
33
+ if (!Object.keys(value).every(key => [...base, 'agentProvider', 'agentModel', 'persona', 'toolFilter'].includes(key))) return false
34
+ for (const key of ['agentProvider', 'agentModel', 'persona']) if (Object.hasOwn(value, key) && !nonempty(value[key])) return false
35
+ if (Object.hasOwn(value, 'agentProvider') !== Object.hasOwn(value, 'agentModel')) return false
36
+ if (!Object.hasOwn(value, 'toolFilter')) return true
37
+ const filter = value.toolFilter
38
+ return record(filter) && Object.keys(filter).length > 0 && Object.keys(filter).every(key =>
39
+ ['allow', 'deny'].includes(key) && Array.isArray(filter[key]) && filter[key].every(nonempty))
40
+ }
41
+
42
+ function supportedPackedToolRun(row, placeholdersOnly = true) {
43
+ if (!exactKeys(row, ['type', 'seq0', 'time0', 'data']) || !count(row.seq0) || !integer(row.time0)) return false
44
+ const data = row.data
45
+ if (!exactKeys(data, ['turn', 'step', 'index', 'id', 'dt', 'args'], ['name']) || typeof data.id !== 'string' || (placeholdersOnly && data.id !== '' && data.name !== '' && data.name !== null) ||
46
+ (Object.hasOwn(data, 'name') && data.name !== null && typeof data.name !== 'string')) return false
47
+ if (![data.turn, data.step, data.index].every(count) || !Array.isArray(data.args) || data.args.length === 0 ||
48
+ !data.args.every(value => typeof value === 'string') || !Array.isArray(data.dt) || data.dt.length !== data.args.length - 1 ||
49
+ data.args.length - 1 > Number.MAX_SAFE_INTEGER - row.seq0) return false
50
+ let time = row.time0
51
+ return data.dt.every(gap => integer(gap) && integer(time += gap))
52
+ }
53
+
54
+ const rawChunkEnvelope = row => exactKeys(row, ['type', 'seq', 'time', 'data'], ['ignorable']) && count(row.seq) && integer(row.time) &&
55
+ (!Object.hasOwn(row, 'ignorable') || row.ignorable === true) && exactKeys(row.data, ['turn', 'step', 'chunk']) && [row.data.turn, row.data.step].every(count)
56
+ const surfaceEnvelope = row => exactKeys(row, ['type', 'seq', 'time', 'data'], ['surfaceOp', 'sourceEventSeqs', 'ignorable']) && count(row.seq) && integer(row.time) &&
57
+ (!Object.hasOwn(row, 'ignorable') || row.ignorable === true)
58
+ const messageShape = value => exactKeys(value, ['role', 'id', 'source', 'content']) && nonempty(value.id) && Array.isArray(value.content)
59
+ const toolBlockShape = value => exactKeys(value, ['type', 'id', 'name', 'arguments']) && value.type === 'tool-call' && typeof value.id === 'string' && nonempty(value.name) && typeof value.arguments === 'string'
60
+
61
+ function supportedPackedTextRun(row) {
62
+ const data = row.data
63
+ return exactKeys(data, ['turn', 'step', 'index', 'dt', 'texts']) && supportedPackedToolRun({ ...row,
64
+ data: { turn: data.turn, step: data.step, index: data.index, id: 'unchanged-text-run', dt: data.dt, args: data.texts } }, false)
65
+ }
66
+
67
+ function supportedNullToolDelta(row) {
68
+ if (!rawChunkEnvelope(row)) return false
69
+ const chunk = row.data.chunk
70
+ return exactKeys(chunk, ['type', 'index', 'id', 'name', 'argumentsDelta']) && chunk.type === 'tool-call-delta' &&
71
+ chunk.name === null && count(chunk.index) && typeof chunk.id === 'string' && typeof chunk.argumentsDelta === 'string'
72
+ }
73
+
74
+ // This is a proof for the published DeepSeek empty-continuation-ID bug, not a
75
+ // nearest-call heuristic. Everything is planned before any output is published.
76
+ function recoverRecordedToolIdentities(entries) {
77
+ const rawPatches = new Map()
78
+ const packedIds = new Map()
79
+ const refusals = { occurrences: 0, samples: [] }
80
+ let chains = 0
81
+ let fields = 0
82
+ const emptyBlock = block => block?.type === 'tool-call' && block.id === '' || block?.type === 'tool-result' && block.toolCallId === ''
83
+ const emptyDurable = row => row.type === 'tool/call' && row.data?.callId === '' ||
84
+ row.type === 'assistant/chunk' && row.data?.chunk?.type === 'block-end' && emptyBlock(row.data.chunk.block) ||
85
+ ['assistant/message', 'tool/result'].includes(row.type) && (row.data?.message?.content?.some?.(emptyBlock) || row.data?.message?.source?.callId === '')
86
+ const result = () => ({ rawPatches, packedIds, chains, fields, refusals })
87
+ if (!entries.some(entry => emptyDurable(entry.row))) return result()
88
+ const require = (condition, reason) => { if (!condition) throw reason }
89
+ const refuse = (entry, reason) => {
90
+ refusals.occurrences++
91
+ if (refusals.samples.length < 10) refusals.samples.push({ line: entry.index + 1, ...(count(entry.row.seq) ? { seq: entry.row.seq } : {}), reason })
92
+ }
93
+ const logical = []
94
+ let turn, step
95
+ try {
96
+ for (const entry of entries.slice(1)) {
97
+ const row = entry.row
98
+ objectMembers(entry.line, 0) // Sequence identity cannot come from duplicate envelope keys.
99
+ if (row.type === 'turn/start') turn = row.data?.turn
100
+ if (row.type === 'step/start') { turn = row.data?.turn; step = row.data?.step }
101
+ entry.scope = { turn: row.data?.turn ?? turn, step: row.data?.step ?? step }
102
+ if (['tool-call-chunks', 'text-chunks', 'reasoning-chunks'].includes(row.type)) {
103
+ const tool = row.type === 'tool-call-chunks'
104
+ require(tool ? supportedPackedToolRun(row, false) : supportedPackedTextRun(row), 'A packed source has unsupported fields or coordinates.')
105
+ const fragments = tool ? row.data.args : row.data.texts
106
+ let time = row.time0
107
+ for (let offset = 0; offset < fragments.length; offset++) {
108
+ if (offset) time += row.data.dt[offset - 1]
109
+ require(logical.length < maximumBytes / 128, 'The identity proof exceeds its bounded event count.')
110
+ logical.push({ entry, offset, row: { type: 'assistant/chunk', seq: row.seq0 + offset, time,
111
+ data: { turn: row.data.turn, step: row.data.step, chunk: tool ? { type: 'tool-call-delta', index: row.data.index, id: row.data.id,
112
+ ...(Object.hasOwn(row.data, 'name') ? { name: row.data.name } : {}), argumentsDelta: fragments[offset] }
113
+ : { type: row.type === 'text-chunks' ? 'text-delta' : 'reasoning-delta', index: row.data.index, text: fragments[offset] } } } })
114
+ }
115
+ } else logical.push({ entry, row })
116
+ if (row.type === 'step/end') step = undefined
117
+ if (row.type === 'turn/end') { turn = undefined; step = undefined }
118
+ }
119
+ require(logical.every((value, index) => value.row.seq === index && integer(value.row.time)), 'Logical events must have unique complete sequence coordinates and safe times.')
120
+ } catch (error) {
121
+ if (typeof error !== 'string') throw error
122
+ refuse(entries.find(entry => emptyDurable(entry.row)), error)
123
+ return result()
124
+ }
125
+ const sourceSeqs = (row, nonempty = true) => {
126
+ const values = row.sourceEventSeqs
127
+ require(Array.isArray(values), 'Tool history needs explicit, valid assistant chunk provenance.')
128
+ const selected = new Set()
129
+ const hasRanges = values.some(Array.isArray)
130
+ let previous = -1
131
+ for (const member of values) {
132
+ const range = Array.isArray(member)
133
+ require(!range || member.length === 2, 'A provenance range must contain exactly two coordinates.')
134
+ const start = range ? member[0] : member
135
+ const end = range ? member[1] : member
136
+ require(count(start) && count(end) && start <= end && end < row.seq && end - start < logical.length - selected.size,
137
+ 'Provenance contains an unsafe, missing or excessive source range.')
138
+ require((!hasRanges && row.type !== 'assistant/message') || start > previous, 'Chunk provenance is not one complete ordered attempt.')
139
+ for (let seq = start; seq <= end; seq++) {
140
+ require(!selected.has(seq), 'Provenance contains duplicate or overlapping source coordinates.')
141
+ selected.add(seq)
142
+ }
143
+ previous = end
144
+ }
145
+ require(!nonempty || selected.size > 0, 'A completed tool stream needs nonempty chunk provenance.')
146
+ // v0 accepts unordered flat sets, but the active v1 -> v2 stage requires an
147
+ // ordered complete attempt. Do not silently rewrite that provenance here.
148
+ return [...selected].sort((left, right) => left - right)
149
+ }
150
+ const sameStep = (left, right) => left?.turn === right?.turn && left?.step === right?.step
151
+ const messages = entries.filter(entry => entry.row.type === 'assistant/message')
152
+ for (const assistant of messages) {
153
+ const row = assistant.row
154
+ const data = row.data
155
+ const content = data?.message?.content
156
+ if (!Array.isArray(content) || !content.some(block => block?.type === 'tool-call') ||
157
+ !entries.some(entry => sameStep(entry.scope, data) && emptyDurable(entry.row))) continue
158
+ try {
159
+ require(surfaceEnvelope(row) && row.surfaceOp === 'append' && exactKeys(data, ['turn', 'step', 'message'], ['usage']) && count(data.turn) && count(data.step) &&
160
+ messageShape(data.message) && data.message.role === 'assistant' && exactKeys(data.message.source, ['kind', 'provider', 'model']) &&
161
+ data.message.source.kind === 'model' && data.message.source.provider === 'deepseek-official' && nonempty(data.message.source.model),
162
+ 'Only the uninterrupted recorded DeepSeek append-message shape without owner replay state is supported.')
163
+ const sources = sourceSeqs(row)
164
+ const selected = new Set(sources)
165
+ const stream = sources.map(seq => logical[seq])
166
+ require(stream.every(value => value?.row.type === 'assistant/chunk' && sameStep(value.row.data, data)), 'Chunk provenance cites another event, turn or step.')
167
+ require(logical.slice(sources[0], sources.at(-1) + 1).every(value => value.row.type !== 'assistant/chunk' || !sameStep(value.row.data, data) || selected.has(value.row.seq)),
168
+ 'Chunk provenance omits a delta inside this stream attempt.')
169
+ for (const other of messages) if (other !== assistant && Object.hasOwn(other.row, 'sourceEventSeqs')) {
170
+ require(!sourceSeqs(other.row, false).some(seq => selected.has(seq)), 'Two assistant messages share source chunks.')
171
+ }
172
+ const blocks = new Map()
173
+ let finished = false
174
+ let usage
175
+ for (const value of stream) {
176
+ const current = value.row
177
+ const chunk = current.data.chunk
178
+ require(rawChunkEnvelope(current) && record(chunk) && !finished, 'The stream has unsupported fields or chunks after finish.')
179
+ assertUniqueJson(value.entry.line, value.entry.row)
180
+ if (chunk.type === 'usage') {
181
+ require(exactKeys(chunk, ['type', 'usage']) && record(chunk.usage), 'An unrecognized usage chunk prevents identity recovery.')
182
+ usage = chunk.usage
183
+ continue
184
+ }
185
+ if (chunk.type === 'finish') {
186
+ require(exactKeys(chunk, ['type', 'reason']) && exactKeys(chunk.reason, ['kind']) && chunk.reason.kind === 'tool-calls', 'The tool stream is interrupted, truncated or has owner replay state.')
187
+ finished = true
188
+ continue
189
+ }
190
+ require(count(chunk.index), 'A stream block index is unsafe.')
191
+ let block = blocks.get(chunk.index)
192
+ if (chunk.type === 'block-start') {
193
+ require(exactKeys(chunk, ['type', 'index', 'blockType']) && ['text', 'reasoning', 'tool-call'].includes(chunk.blockType) && !block,
194
+ 'A block start is repeated or unsupported.')
195
+ blocks.set(chunk.index, { type: chunk.blockType, started: true, text: '', arguments: '', deltas: [] })
196
+ continue
197
+ }
198
+ const type = chunk.type === 'text-delta' ? 'text' : chunk.type === 'reasoning-delta' ? 'reasoning' : chunk.type === 'tool-call-delta' ? 'tool-call' : chunk.block?.type
199
+ require(['text', 'reasoning', 'tool-call'].includes(type), 'A stream chunk has an unsupported block type.')
200
+ if (!block) {
201
+ require(type !== 'tool-call', 'A tool delta or end is missing its block start.')
202
+ block = { type, text: '', arguments: '', deltas: [] }
203
+ blocks.set(chunk.index, block)
204
+ }
205
+ require(block.type === type && !block.end, 'A block changes type, repeats its end or receives a late delta.')
206
+ if (chunk.type === 'block-end') {
207
+ require(exactKeys(chunk, ['type', 'index', 'block']) && record(chunk.block), 'A block end has unsupported fields.')
208
+ block.end = value
209
+ block.content = chunk.block
210
+ if (type === 'tool-call') require(toolBlockShape(chunk.block) &&
211
+ chunk.block.name === block.name && chunk.block.arguments === block.arguments, 'The final tool name or arguments disagree with its deltas.')
212
+ continue
213
+ }
214
+ if (type === 'tool-call') {
215
+ require(exactKeys(chunk, ['type', 'index', 'id', 'argumentsDelta'], ['name']) && typeof chunk.id === 'string' && typeof chunk.argumentsDelta === 'string' &&
216
+ (!Object.hasOwn(chunk, 'name') || chunk.name === null || typeof chunk.name === 'string'), 'A tool delta has unsupported identity or argument fields.')
217
+ if (chunk.name) block.name = chunk.name
218
+ block.arguments += chunk.argumentsDelta
219
+ block.deltas.push(value)
220
+ } else {
221
+ require(exactKeys(chunk, ['type', 'index', 'text']) && typeof chunk.text === 'string', 'A text or reasoning delta has unsupported fields.')
222
+ block.text += chunk.text
223
+ }
224
+ }
225
+ require(finished && blocks.size === content.length, 'The completed stream and assistant block list do not match.')
226
+ require(isDeepStrictEqual(usage, data.usage), 'The completed stream and assistant usage do not match.')
227
+ const ordered = [...blocks.values()]
228
+ const advertised = messages.filter(entry => sameStep(entry.row.data, data)).flatMap(entry => entry.row.data?.message?.content ?? []).filter(block => block?.type === 'tool-call')
229
+ const calls = entries.filter(entry => entry.row.type === 'tool/call' && sameStep(entry.row.data, data))
230
+ const results = entries.filter(entry => entry.row.type === 'tool/result' && sameStep(entry.row.data, data))
231
+ const proposals = []
232
+ const provenChains = []
233
+ for (const [position, block] of ordered.entries()) {
234
+ const actual = block.content ?? { type: block.type, text: block.text }
235
+ const declared = content[position]
236
+ if (block.type !== 'tool-call') {
237
+ require(isDeepStrictEqual(actual, declared), 'Text or reasoning differs between the stream and assistant message.')
238
+ continue
239
+ }
240
+ require(block.end && toolBlockShape(declared) &&
241
+ isDeepStrictEqual({ ...actual, id: '' }, { ...declared, id: '' }), 'The tool block and assistant advertisement disagree.')
242
+ const matchingCalls = calls.filter(entry => entry.row.data.name === actual.name && entry.row.data.arguments === actual.arguments)
243
+ require(matchingCalls.length === 1 && advertised.filter(value => value.name === actual.name && value.arguments === actual.arguments).length === 1,
244
+ 'The tool advertisement and execution cannot be paired uniquely by name and arguments.')
245
+ const call = matchingCalls[0]
246
+ require(surfaceEnvelope(call.row) && !Object.hasOwn(call.row, 'surfaceOp') && !Object.hasOwn(call.row, 'sourceEventSeqs') &&
247
+ exactKeys(call.row.data, ['turn', 'step', 'callId', 'name', 'arguments']) && row.seq < call.row.seq && typeof call.row.data.callId === 'string',
248
+ 'The tool execution has unsupported fields, precedes its advertisement or has an invalid ID.')
249
+ const matchingResults = results.filter(entry => Object.hasOwn(entry.row, 'sourceEventSeqs') && sourceSeqs(entry.row).includes(call.row.seq))
250
+ let toolResult
251
+ if (matchingResults.length === 1) {
252
+ toolResult = matchingResults[0]
253
+ require(isDeepStrictEqual(sourceSeqs(toolResult.row), [call.row.seq]), 'The result provenance does not select exactly this tool execution.')
254
+ } else {
255
+ require(matchingResults.length === 0 && advertised.length === 1 && calls.length === 1 && results.length === 1 && !Object.hasOwn(results[0].row, 'sourceEventSeqs'),
256
+ 'The tool result is missing, ambiguous or has conflicting provenance.')
257
+ require(!logical.some(value => value.row.type === 'assistant/chunk' && sameStep(value.row.data, data) && !selected.has(value.row.seq)),
258
+ 'A result without provenance cannot be matched across a retry or another stream attempt.')
259
+ toolResult = results[0]
260
+ }
261
+ const resultMessage = toolResult.row.data?.message
262
+ const resultBlock = resultMessage?.content?.[0]
263
+ require(surfaceEnvelope(toolResult.row) && exactKeys(toolResult.row.data, ['turn', 'step', 'message'], ['error', 'meta']) &&
264
+ call.row.seq < toolResult.row.seq && toolResult.row.surfaceOp === 'append' && messageShape(resultMessage) && resultMessage.role === 'user' &&
265
+ exactKeys(resultMessage.source, ['kind', 'callId']) && resultMessage.source.kind === 'tool' && typeof resultMessage.source.callId === 'string' && resultMessage.content.length === 1 &&
266
+ exactKeys(resultBlock, ['type', 'toolCallId', 'content'], ['isError']) && resultBlock.type === 'tool-result' && typeof resultBlock.toolCallId === 'string' &&
267
+ Array.isArray(resultBlock.content) && (!Object.hasOwn(resultBlock, 'isError') || typeof resultBlock.isError === 'boolean'), 'The tool result is not a complete append result with a single identity.')
268
+ const slots = [
269
+ [block.end, ['data', 'chunk', 'block', 'id'], actual.id],
270
+ [{ entry: assistant, row }, ['data', 'message', 'content', position, 'id'], declared.id],
271
+ [{ entry: call, row: call.row }, ['data', 'callId'], call.row.data.callId],
272
+ [{ entry: toolResult, row: toolResult.row }, ['data', 'message', 'source', 'callId'], resultMessage.source.callId],
273
+ [{ entry: toolResult, row: toolResult.row }, ['data', 'message', 'content', 0, 'toolCallId'], resultMessage.content[0].toolCallId],
274
+ ]
275
+ if (!slots.some(slot => slot[2] === '')) {
276
+ require(slots.every(slot => slot[2] === actual.id) && block.deltas.every(value => value.row.data.chunk.id === '' || value.row.data.chunk.id === actual.id),
277
+ 'An already nonempty tool chain contains conflicting recorded identities.')
278
+ provenChains.push(slots)
279
+ continue
280
+ }
281
+ const candidates = new Set(block.deltas.map(value => value.row.data.chunk.id).filter(nonempty))
282
+ require(candidates.size === 1, 'The tool stream has no unique previously recorded provider ID.')
283
+ const candidate = [...candidates][0]
284
+ require(slots.every(slot => slot[2] === '' || slot[2] === candidate), 'A durable ID conflicts with the recorded provider ID.')
285
+ require(advertised.every(value => value === declared || value.id !== candidate), 'The recorded ID is already advertised by another tool in this step.')
286
+ let seen = false
287
+ const deltas = []
288
+ for (const value of block.deltas) {
289
+ if (value.row.data.chunk.id === candidate) seen = true
290
+ else if (seen) deltas.push([value, ['data', 'chunk', 'id'], ''])
291
+ }
292
+ require(deltas.length > 0, 'No empty continuation delta follows the recorded provider ID.')
293
+ provenChains.push(slots)
294
+ proposals.push({ candidate, name: actual.name, assistant, call, toolResult, block, slots: [...deltas, ...slots.filter(slot => slot[2] === '')], sources })
295
+ }
296
+ require(new Set(proposals.map(value => value.candidate)).size === proposals.length, 'Multiple tools claim the same recorded provider ID.')
297
+ for (const proposal of proposals) {
298
+ const owned = new Map()
299
+ const allow = (entry, path) => {
300
+ if (!owned.has(entry)) owned.set(entry, new Set())
301
+ owned.get(entry).add(JSON.stringify(path))
302
+ }
303
+ // Only the exact slots of proven advertisement/call/result chains are
304
+ // owned; another core-looking call is not automatically trustworthy.
305
+ for (const slots of provenChains) for (const [value, path] of slots) allow(value.entry, path)
306
+ const linkedSeqs = new Set([...sources, row.seq, proposal.call.row.seq, proposal.toolResult.row.seq])
307
+ const linkedMessages = new Set([data.message.id, proposal.toolResult.row.data.message.id])
308
+ const cites = value => {
309
+ if (!record(value) && !Array.isArray(value)) return false
310
+ return Object.entries(value).some(([key, child]) => {
311
+ if (key === 'sourceEventSeq' && linkedSeqs.has(child)) return true
312
+ if ((/^(?:messageId|.*MessageId)s?$/.test(key) || key === 'id' && ['assistant', 'user'].includes(value.role)) &&
313
+ (linkedMessages.has(child) || Array.isArray(child) && child.some(id => linkedMessages.has(id)))) return true
314
+ if (key === 'shadowedRange' && record(child) && [...linkedSeqs].some(seq => count(child.start) && count(child.end) && child.start <= seq && seq <= child.end)) return true
315
+ if (['sourceEventSeqs', 'shadowedSeqs', 'shadowedRange'].includes(key) && Array.isArray(child)) {
316
+ if (key === 'shadowedRange' && child.length === 2 && [...linkedSeqs].some(seq => count(child[0]) && count(child[1]) && child[0] <= seq && seq <= child[1])) return true
317
+ if (child.some(member => linkedSeqs.has(member) || Array.isArray(member) && member.length === 2 && [...linkedSeqs].some(seq => count(member[0]) && count(member[1]) && member[0] <= seq && seq <= member[1]))) return true
318
+ }
319
+ return cites(child)
320
+ })
321
+ }
322
+ for (const entry of entries.slice(1)) {
323
+ const value = entry.row
324
+ const replacement = value.surfaceOp
325
+ const surfaces = [row.seq, proposal.toolResult.row.seq]
326
+ const replaces = record(replacement) && replacement.op === 'replace' && surfaces.some(seq => count(replacement.start) && count(replacement.end) && replacement.start <= seq && seq <= replacement.end)
327
+ require(!replaces, 'A surface replacement also owns the affected historical message.')
328
+ const copiesMessage = [data.message.id, proposal.toolResult.row.data.message.id].includes(value.data?.message?.id) && entry !== assistant && entry !== proposal.toolResult
329
+ require(!copiesMessage, 'A copied message identity needs owner-aware recovery.')
330
+ if (!sameStep(entry.scope, data) && !cites(value)) continue
331
+ assertUniqueJson(entry.line, value)
332
+ require(!(value.type === 'approval/asked' && value.data?.toolName === proposal.name &&
333
+ (value.data.callId === undefined || value.data.callId === '' || value.data.callId === proposal.candidate)), 'An associated approval retains the historical tool identity.')
334
+ const visit = (current, path = []) => {
335
+ if (!record(current) && !Array.isArray(current)) return
336
+ for (const [key, child] of Object.entries(current)) {
337
+ const next = [...path, Array.isArray(current) ? Number(key) : key]
338
+ const identity = ['callId', 'toolCallId', 'rootCallId', 'parentCallId', 'subCallId'].includes(key) || key === 'id' && current.type === 'tool-call'
339
+ if (key === 'replayState') require(false, 'Owner replay state needs owner-aware identity recovery.')
340
+ if (identity && (child === '' || child === proposal.candidate || typeof child === 'string' && (child.startsWith(`${proposal.candidate}:code:`) || child.startsWith(':code:')))) {
341
+ require(owned.get(entry)?.has(JSON.stringify(next)), 'An additional approval, dispatch, plugin or copied-content identity reference remains.')
342
+ }
343
+ visit(child, next)
344
+ }
345
+ }
346
+ visit(value)
347
+ }
348
+ }
349
+ for (const proposal of proposals) {
350
+ chains++
351
+ for (const [value, path] of proposal.slots) {
352
+ fields++
353
+ if (value.offset !== undefined) {
354
+ if (!packedIds.has(value.entry.index)) packedIds.set(value.entry.index, new Map())
355
+ packedIds.get(value.entry.index).set(value.offset, proposal.candidate)
356
+ } else {
357
+ if (!rawPatches.has(value.entry.index)) rawPatches.set(value.entry.index, [])
358
+ rawPatches.get(value.entry.index).push({ path, value: proposal.candidate })
359
+ }
360
+ }
361
+ }
362
+ } catch (error) {
363
+ if (typeof error !== 'string') throw error
364
+ refuse(assistant, error)
365
+ }
366
+ }
367
+ if (refusals.occurrences > 0) { rawPatches.clear(); packedIds.clear(); chains = 0; fields = 0 }
368
+ return result()
369
+ }
370
+
371
+ // Diagnose the other concrete legacy shapes reported in #251 without inventing
372
+ // descriptor authority or tool associations. This is not a Session validator:
373
+ // the owning DSH installation still validates the complete repaired artifact.
374
+ function legacyBlockers() {
375
+ const issues = new Map()
376
+ const add = (code, message, row, line, path) => {
377
+ if (!issues.has(code)) issues.set(code, { code, message, occurrences: 0, samples: [] })
378
+ const issue = issues.get(code)
379
+ issue.occurrences++
380
+ const seq = row.seq ?? row.seq0
381
+ if (issue.samples.length < 10) issue.samples.push({ line, type: row.type, ...(count(seq) ? { seq } : {}), path })
382
+ }
383
+ const identifier = (value, row, line, path) => {
384
+ if (value === '') add('empty-durable-tool-id', 'An empty durable tool ID lacks a proven closed chain to its recorded provider identity; new identities are not synthesized.', row, line, path)
385
+ }
386
+ const block = (value, row, line, path) => {
387
+ if (value?.type === 'tool-call') identifier(value.id, row, line, `${path}.id`)
388
+ if (value?.type === 'tool-result') identifier(value.toolCallId, row, line, `${path}.toolCallId`)
389
+ }
390
+ const message = (value, row, line, path) => {
391
+ if (value?.source?.kind === 'tool') identifier(value.source.callId, row, line, `${path}.source.callId`)
392
+ if (Array.isArray(value?.content)) value.content.forEach((value, index) => block(value, row, line, `${path}.content[${index}]`))
393
+ }
394
+ return {
395
+ inspect(row, line) {
396
+ const data = row?.data
397
+ if (row?.type === 'subagent/descriptor' && data?.version !== 3 && !supportedDescriptorV2(data)) {
398
+ add('unsupported-subagent-descriptor', 'Only exact historical v2 descriptors with equivalent v3 composition are supported; this descriptor needs recovery by its original writer.', row, line, 'data.version')
399
+ }
400
+ if (row?.type === 'tool-call-chunks') {
401
+ if ((data?.id === '' || data?.name === '' || data?.name === null) && !supportedPackedToolRun(row)) add('unsupported-packed-tool-chunks', 'The packed row cannot be expanded losslessly: it needs exact fields, string deltas and safe sequence/time coordinates.', row, line, 'data')
402
+ }
403
+ if (row?.type === 'assistant/chunk') {
404
+ if (data?.chunk?.type === 'tool-call-delta' && data.chunk.name === null && !supportedNullToolDelta(row)) {
405
+ add('null-stream-tool-name', 'A null streaming tool name needs exact raw fields, string ID/arguments and safe coordinates before normalization.', row, line, 'data.chunk.name')
406
+ }
407
+ if (data?.chunk?.type === 'block-end') block(data.chunk.block, row, line, 'data.chunk.block')
408
+ }
409
+ if (row?.type === 'assistant/message') {
410
+ message(data?.message, row, line, 'data.message')
411
+ // Frozen v0 also normalizes the older unwrapped assistant shape.
412
+ if (record(data) && !Object.hasOwn(data, 'message') && Object.hasOwn(data, 'content') && Object.hasOwn(data, 'provenance')) {
413
+ message(data, row, line, 'data')
414
+ }
415
+ }
416
+ if (row?.type === 'tool/call') identifier(data?.callId, row, line, 'data.callId')
417
+ if (row?.type === 'tool/result') {
418
+ identifier(data?.callId, row, line, 'data.callId') // Historical unwrapped result.
419
+ message(data?.message, row, line, 'data.message')
420
+ }
421
+ },
422
+ report: () => [...issues.values()],
423
+ }
424
+ }
15
425
 
16
426
  // Locate JSON object members without reserializing message content or numbers.
17
427
  // JSON.parse validates each complete row before this scanner is used.
18
- function objectMembers(text, start) {
428
+ function objectMembers(text, start, array = false) {
19
429
  let cursor = start
20
430
  const space = () => { while (/\s/.test(text[cursor] ?? '') && cursor < text.length) cursor++ }
21
431
  const stringEnd = () => {
@@ -42,30 +452,53 @@ function objectMembers(text, start) {
42
452
  }
43
453
  }
44
454
  space()
45
- if (text[cursor++] !== '{') throw new Error('Expected a JSON object in the legacy message path.')
455
+ if (text[cursor++] !== (array ? '[' : '{')) throw new Error('Expected a JSON container in the legacy message path.')
46
456
  const members = []
47
457
  const names = new Set()
48
458
  space()
49
- while (text[cursor] !== '}') {
459
+ const close = array ? ']' : '}'
460
+ while (text[cursor] !== close) {
50
461
  const start = cursor
51
- stringEnd()
52
- const name = JSON.parse(text.slice(start, cursor))
53
- if (names.has(name)) throw new Error('Duplicate JSON property in the legacy message path; refusing ambiguous repair.')
54
- names.add(name)
55
- space()
56
- cursor++ // colon, already validated by JSON.parse
57
- space()
462
+ let name = members.length
463
+ if (!array) {
464
+ stringEnd()
465
+ name = JSON.parse(text.slice(start, cursor))
466
+ if (names.has(name)) throw new Error('Duplicate JSON property in the legacy message path; refusing ambiguous repair.')
467
+ names.add(name)
468
+ space()
469
+ cursor++ // colon, already validated by JSON.parse
470
+ space()
471
+ }
58
472
  const valueStart = cursor
59
473
  valueEnd()
60
474
  members.push({ name, start, end: cursor, valueStart })
61
475
  space()
62
- if (text[cursor] === '}') break
476
+ if (text[cursor] === close) break
63
477
  cursor++ // comma
64
478
  space()
65
479
  }
66
480
  return members
67
481
  }
68
482
 
483
+ function assertUniqueJson(line, row, start = 0) {
484
+ if (!record(row) && !Array.isArray(row)) return
485
+ for (const member of objectMembers(line, start, Array.isArray(row))) assertUniqueJson(line, row[member.name], member.valueStart)
486
+ }
487
+
488
+ function replaceIdentityValues(line, patches) {
489
+ for (const patch of patches) {
490
+ let start = 0
491
+ let member
492
+ for (const key of patch.path) {
493
+ member = objectMembers(line, start, typeof key === 'number').find(value => value.name === key)
494
+ if (!member) throw new Error('An identity patch no longer matches its proven JSON path.')
495
+ start = member.valueStart
496
+ }
497
+ line = line.slice(0, member.valueStart) + JSON.stringify(patch.value) + line.slice(member.end)
498
+ }
499
+ return line
500
+ }
501
+
69
502
  function removeSummary(line) {
70
503
  const data = objectMembers(line, 0).find(member => member.name === 'data')
71
504
  const source = objectMembers(line, data.valueStart).find(member => member.name === 'source')
@@ -77,6 +510,47 @@ function removeSummary(line) {
77
510
  return line.slice(0, start) + line.slice(end)
78
511
  }
79
512
 
513
+ function promoteDescriptor(line) {
514
+ const data = objectMembers(line, 0).find(member => member.name === 'data')
515
+ const members = objectMembers(line, data.valueStart)
516
+ const filter = members.find(member => member.name === 'toolFilter')
517
+ if (filter) objectMembers(line, filter.valueStart) // Reject ambiguous nested fields as well.
518
+ const version = members.find(member => member.name === 'version')
519
+ return line.slice(0, version.valueStart) + '3' + line.slice(version.end)
520
+ }
521
+
522
+ function normalizeNullToolName(line) {
523
+ const data = objectMembers(line, 0).find(member => member.name === 'data')
524
+ const chunk = objectMembers(line, data.valueStart).find(member => member.name === 'chunk')
525
+ const name = objectMembers(line, chunk.valueStart).find(member => member.name === 'name')
526
+ // Old/current assemblers only assign truthy names; token timing instead uses
527
+ // name !== undefined. Keep this property: deleting it would change TTFT for
528
+ // an empty argument fragment. Every other raw byte remains unchanged.
529
+ return line.slice(0, name.valueStart) + '""' + line.slice(name.end)
530
+ }
531
+
532
+ function expandPackedToolRun(line, row, retain, recoveredIds) {
533
+ const data = objectMembers(line, 0).find(member => member.name === 'data')
534
+ objectMembers(line, data.valueStart) // Reject duplicate fields before reserializing this row.
535
+ let time = row.time0
536
+ const expanded = []
537
+ for (let index = 0; index < row.data.args.length; index++) {
538
+ if (index > 0) time += row.data.dt[index - 1]
539
+ // Historical packed-row expansion after preserving null-name presence as
540
+ // an empty string. No ID/name inference and
541
+ // no dropped chunks: the active v1 -> v2 migration retains these as raw
542
+ // stream records when their IDs or names are empty strings.
543
+ expanded.push(retain(JSON.stringify({
544
+ type: 'assistant/chunk', seq: row.seq0 + index, time,
545
+ data: { turn: row.data.turn, step: row.data.step, chunk: {
546
+ type: 'tool-call-delta', index: row.data.index, id: recoveredIds?.get(index) ?? row.data.id,
547
+ ...(Object.hasOwn(row.data, 'name') ? { name: row.data.name ?? '' } : {}), argumentsDelta: row.data.args[index],
548
+ } },
549
+ }) + (line.endsWith('\r\n') ? '\r\n' : '\n')))
550
+ }
551
+ return expanded.join('')
552
+ }
553
+
80
554
  async function readBounded(path) {
81
555
  const chunks = []
82
556
  let length = 0
@@ -150,10 +624,25 @@ export async function repairLegacySession(input, output) {
150
624
  const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(decoded)
151
625
  if (!text.endsWith('\n')) throw new Error('Session must end with a complete newline-terminated JSON record.')
152
626
  const lines = text.match(/[^\n]*\n/g) ?? []
627
+ const entries = lines.map((line, index) => {
628
+ try { return { line, index, row: JSON.parse(line) } } catch { throw new Error(`Invalid JSON at line ${index + 1}; no output written.`) }
629
+ })
630
+ const recovery = recoverRecordedToolIdentities(entries)
153
631
  let changes = 0
154
- const repaired = lines.map((line, index) => {
155
- let row
156
- try { row = JSON.parse(line) } catch { throw new Error(`Invalid JSON at line ${index + 1}; no output written.`) }
632
+ let descriptors = 0
633
+ let expandedRows = 0
634
+ let expandedChunks = 0
635
+ let normalizedNames = 0
636
+ let outputBytes = 0
637
+ const retain = line => {
638
+ outputBytes += Buffer.byteLength(line)
639
+ if (outputBytes > maximumBytes) throw new Error('Expanded Session exceeds the 128 MiB repair limit; no output written.')
640
+ return line
641
+ }
642
+ const diagnostics = legacyBlockers()
643
+ const repaired = entries.map(entry => {
644
+ const { index } = entry
645
+ let { line, row } = entry
157
646
  if (index === 0) {
158
647
  objectMembers(line, 0)
159
648
  if (row.type !== 'session' || row.version !== 0 || typeof row.id !== 'string' || !row.id ||
@@ -161,18 +650,37 @@ export async function repairLegacySession(input, output) {
161
650
  !Number.isSafeInteger(row.delegationDepth) || row.delegationDepth < 0) {
162
651
  throw new Error('Only a legacy DSH format v0 Session header is supported.')
163
652
  }
164
- return line
653
+ return retain(line)
654
+ }
655
+ const patches = recovery.rawPatches.get(index)
656
+ if (patches) { line = replaceIdentityValues(line, patches); row = JSON.parse(line) }
657
+ diagnostics.inspect(row, index + 1)
658
+ if (row?.type === 'subagent/descriptor' && supportedDescriptorV2(row.data)) {
659
+ descriptors++
660
+ return retain(promoteDescriptor(line))
661
+ }
662
+ if (row?.type === 'tool-call-chunks' && supportedPackedToolRun(row)) {
663
+ expandedRows++
664
+ expandedChunks += row.data.args.length
665
+ if (row.data.name === null) normalizedNames += row.data.args.length
666
+ return expandPackedToolRun(line, row, retain, recovery.packedIds.get(index))
667
+ }
668
+ if (row?.type === 'assistant/chunk' && supportedNullToolDelta(row)) {
669
+ normalizedNames++
670
+ return retain(normalizeNullToolName(line))
165
671
  }
166
672
  const source = row?.data?.source
167
673
  if (row?.type !== 'user/message' || source?.kind !== 'plugin' || source.plugin !== 'dsh-mnemon' ||
168
- !Object.hasOwn(summaries, source.form) || source.summary !== summaries[source.form]) return line
674
+ !Object.hasOwn(summaries, source.form) || !summaries[source.form].includes(source.summary)) return retain(line)
169
675
  changes++
170
- return removeSummary(line)
676
+ return retain(removeSummary(line))
171
677
  }).join('')
172
678
  const plain = Buffer.from(repaired)
173
679
  // A clean artifact stays byte-identical, including its existing frame layout.
174
- const result = changes === 0 ? original : compressed ? encodeFrames(repaired) : plain
175
- if (output !== undefined) {
680
+ const result = changes + descriptors + expandedRows + normalizedNames + recovery.fields === 0 ? original : compressed ? encodeFrames(repaired) : plain
681
+ if (result.length > maximumBytes) throw new Error('Repaired Session exceeds the 128 MiB repair limit; no output written.')
682
+ const blockers = diagnostics.report()
683
+ if (output !== undefined && blockers.length === 0) {
176
684
  if (resolve(input) === resolve(output)) throw new Error('Output must be a new copy; the input cannot be overwritten.')
177
685
  const temporary = await mkdtemp(join(dirname(resolve(output)), '.mnemon-repair-'))
178
686
  try {
@@ -184,18 +692,27 @@ export async function repairLegacySession(input, output) {
184
692
  } finally { await rm(temporary, { recursive: true, force: true }) }
185
693
  }
186
694
  return {
187
- mode: output === undefined ? 'preview' : 'copy',
695
+ mode: output === undefined ? 'preview' : blockers.length === 0 ? 'copy' : 'refused',
188
696
  format: 0,
189
697
  encoding: compressed ? 'zstd' : 'jsonl',
190
698
  repairedMessages: changes,
699
+ repairedDescriptors: descriptors,
700
+ expandedToolChunkRows: expandedRows,
701
+ expandedToolChunks: expandedChunks,
702
+ normalizedToolChunkNames: normalizedNames,
703
+ recoveredToolIdentityChains: recovery.chains,
704
+ recoveredToolIdentityFields: recovery.fields,
705
+ toolIdentityRecoveryRefusals: recovery.refusals,
191
706
  inputSha256: sha256(original),
192
- outputSha256: sha256(result),
707
+ outputSha256: blockers.length === 0 ? sha256(result) : null,
193
708
  originalPreserved: true,
709
+ migrationValidated: false,
710
+ blockers,
194
711
  }
195
712
  }
196
713
 
197
714
  async function main(args) {
198
- const usage = 'Usage: dsh-mnemon-repair-session --input FILE [--output NEW_FILE]\nWithout --output, only preview the two known Mnemon v0 metadata repairs. Stop DSH and work on a backup copy. The input and existing output files are never overwritten.'
715
+ const usage = 'Usage: dsh-mnemon-repair-session --input FILE [--output NEW_FILE]\nWithout --output, preview the three known Mnemon v0 summary repairs, exact compatible v2 subagent descriptors, packed deltas with empty IDs/names, property-preserving null-to-empty delta names and proven DeepSeek chains with a previously recorded provider ID. Unsupported descriptor/tool shapes are reported with exit status 1 and prevent output. DSH must still validate the repaired copy. Stop DSH and work on a backup copy. The input and existing output files are never overwritten.'
199
716
  if (args.length === 1 && args[0] === '--help') { console.log(usage); return }
200
717
  const options = new Map()
201
718
  for (let index = 0; index < args.length; index += 2) {
@@ -205,7 +722,12 @@ async function main(args) {
205
722
  options.set(name, value)
206
723
  }
207
724
  if (!options.has('--input')) throw new Error(usage)
208
- console.log(JSON.stringify(await repairLegacySession(options.get('--input'), options.get('--output')), null, 2))
725
+ const report = await repairLegacySession(options.get('--input'), options.get('--output'))
726
+ console.log(JSON.stringify(report, null, 2))
727
+ if (report.blockers.length > 0) {
728
+ console.error('Unsupported legacy descriptor/tool data remains; no output written. Review blockers and request recovery from the original DSH writer with a sanitized reproducer.')
729
+ process.exitCode = 1
730
+ }
209
731
  }
210
732
 
211
733
  // npm global installs expose this executable through a symlink. Resolve both