dsh-mnemon 0.5.8 → 0.5.9
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.
- package/bin/repair-legacy-session.mjs +547 -25
- package/lib/client.js +1 -1
- package/lib/index.js +25 -6
- package/package.json +2 -2
|
@@ -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
|
|
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
|
-
|
|
459
|
+
const close = array ? ']' : '}'
|
|
460
|
+
while (text[cursor] !== close) {
|
|
50
461
|
const start = cursor
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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] ===
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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) ||
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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
|
package/lib/client.js
CHANGED
|
@@ -7483,7 +7483,7 @@ window.__ModuleLoader__.load({
|
|
|
7483
7483
|
}
|
|
7484
7484
|
//#endregion
|
|
7485
7485
|
//#region \0dsh-mnemon-css:/home/runner/work/dsh-mnemon/dsh-mnemon/src/client/MnemonWorkspace.module.css.mjs
|
|
7486
|
-
const css = ".NS3bAW_workspacePanel{box-sizing:border-box;background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base));pointer-events:auto;min-width:0;min-height:0;display:flex;position:fixed;overflow:hidden}.NS3bAW_workspacePanel>*{flex:auto;width:100%;min-width:0}.NS3bAW_betterSidebarSeat{width:100%;min-width:0;height:100%;min-height:0;display:flex;overflow:hidden}.NS3bAW_betterSidebarSeat>*{flex:auto;min-width:0}.NS3bAW_entry{box-sizing:border-box;width:100%;min-height:36px;color:var(--dsw-alias-label-secondary);white-space:nowrap;cursor:pointer;background:0 0;border:none;border-radius:8px;align-items:center;gap:
|
|
7486
|
+
const css = ".NS3bAW_workspacePanel{box-sizing:border-box;background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base));pointer-events:auto;min-width:0;min-height:0;display:flex;position:fixed;overflow:hidden}.NS3bAW_workspacePanel>*{flex:auto;width:100%;min-width:0}.NS3bAW_betterSidebarSeat{width:100%;min-width:0;height:100%;min-height:0;display:flex;overflow:hidden}.NS3bAW_betterSidebarSeat>*{flex:auto;min-width:0}.NS3bAW_entry{box-sizing:border-box;width:100%;min-height:36px;color:var(--dsw-alias-label-secondary);white-space:nowrap;cursor:pointer;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:0 10px;font-size:13px;display:flex}.NS3bAW_entry:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.NS3bAW_entry[data-active]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-active);font-weight:600}.NS3bAW_entryIcon{flex:none;justify-content:center;align-items:center;width:24px;height:24px;display:inline-flex}.NS3bAW_entryIcon svg{width:18px;height:18px;display:block}.NS3bAW_entryLabel{text-overflow:ellipsis;overflow:hidden}[data-sidebar-collapsed] .NS3bAW_entry{width:36px;min-height:36px;color:var(--dsw-alias-label-primary);border-radius:50%;justify-content:center;margin:0 auto 12px;padding:0}[data-sidebar-collapsed] .NS3bAW_entryIcon svg{width:20px;height:20px}[data-sidebar-collapsed] .NS3bAW_entryLabel{display:none}";
|
|
7487
7487
|
const tagId = "dsh-mnemon/src/client/MnemonWorkspace.module.css";
|
|
7488
7488
|
if (typeof document !== "undefined") {
|
|
7489
7489
|
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
|
package/lib/index.js
CHANGED
|
@@ -3727,7 +3727,11 @@ var MnemonSubagentCoordinator = class {
|
|
|
3727
3727
|
return {
|
|
3728
3728
|
source,
|
|
3729
3729
|
cleanup,
|
|
3730
|
-
memoryBodyIds: new Set(strings(object$4(grant.value).memoryBodyIds))
|
|
3730
|
+
memoryBodyIds: new Set(strings(object$4(grant.value).memoryBodyIds)),
|
|
3731
|
+
writeScope: {
|
|
3732
|
+
viewId: view.id,
|
|
3733
|
+
grant
|
|
3734
|
+
}
|
|
3731
3735
|
};
|
|
3732
3736
|
}
|
|
3733
3737
|
runtimeModel(scope, parent, signal, operation, label, prompt, schema, persona) {
|
|
@@ -3973,13 +3977,27 @@ ${naturalRequest(request)}`;
|
|
|
3973
3977
|
if (request.target === "user") return this.compactUserAndCommit(context, request, plan, signal);
|
|
3974
3978
|
const archive = await context.memorySpaces();
|
|
3975
3979
|
const memoryService = archive.source;
|
|
3976
|
-
|
|
3980
|
+
let allowed = archive.memoryBodyIds;
|
|
3981
|
+
const readDirectory = async () => {
|
|
3982
|
+
const catalog = await memoryService.read("body-directory", archive.writeScope === void 0 ? null : { writeScope: archive.writeScope }, signal);
|
|
3983
|
+
allowed = archive.memoryBodyIds;
|
|
3984
|
+
if (catalog.writeScope !== void 0) {
|
|
3985
|
+
const scope = catalog.writeScope;
|
|
3986
|
+
if (archive.writeScope === void 0 || scope === null || scope.viewId !== archive.writeScope.viewId || scope.sourceInstanceKey !== archive.writeScope.grant.sourceInstanceKey || !Array.isArray(scope.memoryBodyIds) || scope.memoryBodyIds.length > 1e4 || scope.memoryBodyIds.some((id) => typeof id !== "string" || id.trim() === "" || id !== id.trim())) throw new Error("Runtime archival received an invalid Memory Spaces write scope; pending write was not committed; retry the same request in a new turn");
|
|
3987
|
+
allowed = new Set(scope.memoryBodyIds);
|
|
3988
|
+
}
|
|
3989
|
+
return catalog;
|
|
3990
|
+
};
|
|
3991
|
+
const catalog = await readDirectory();
|
|
3992
|
+
const writable = (body) => body.active && body.providerEnabled !== false && body.provider.capabilities.remember === true && (allowed === void 0 || allowed.has(body.id));
|
|
3977
3993
|
const eligible = (body) => writable(body) && body.provider.capabilities.writeMode === "exact" && body.provider.capabilities.forget === true;
|
|
3978
|
-
const catalog = await memoryService.read("body-directory", null, signal);
|
|
3979
3994
|
const eligibleBodies = catalog.items.filter(eligible);
|
|
3980
3995
|
if (eligibleBodies.length === 0) {
|
|
3981
|
-
const
|
|
3982
|
-
|
|
3996
|
+
const writableBodies = catalog.items.filter(writable);
|
|
3997
|
+
const unsupported = writableBodies.map((body) => `${body.id} (provider=${body.provider.id}, writeMode=${body.provider.capabilities.writeMode}, forget=${body.provider.capabilities.forget})`).join(", ");
|
|
3998
|
+
const reason = catalog.items.length === 0 ? "Memory Space body-directory is empty" : allowed?.size === 0 ? "current View write scope is empty" : allowed !== void 0 && catalog.items.some((body) => body.active && body.providerEnabled !== false && body.provider.capabilities.remember && !allowed?.has(body.id)) && writableBodies.length === 0 ? "current View write scope excludes the active writable Memory Spaces" : writableBodies.length === 0 ? "no active writable Memory Space in the authorized scope" : "authorized destinations lack exact writes or safe forget";
|
|
3999
|
+
const scope = archive.writeScope === void 0 ? "management" : `${archive.writeScope.grant.sourceInstanceKey}/${catalog.writeScope === void 0 ? "pinned-read" : "source-write"}`;
|
|
4000
|
+
throw new Error(`runtime memory archival requires an existing active writable Memory Space with exact writes and safe forget; ${reason}; scope=${scope}, catalog=${catalog.items.length}, authorized=${allowed?.size ?? "management"}, writable=${writableBodies.length}; pending write was not committed and existing runtime entries are unchanged; retry the same request in a new turn after activating a supported authorized Memory Space, or increase runtimeMemory.memoryLimitBytes${unsupported === "" ? "" : `; unsupported destinations: ${unsupported}`}`);
|
|
3983
4001
|
}
|
|
3984
4002
|
const eligibleById = new Map(eligibleBodies.map((body) => [body.id, body]));
|
|
3985
4003
|
const budget = compactedBudget(plan);
|
|
@@ -4043,7 +4061,7 @@ ${chunk.context}
|
|
|
4043
4061
|
...branches === void 0 ? {} : { branches }
|
|
4044
4062
|
}));
|
|
4045
4063
|
if ((await runtimeMemory.read("maintenance-plan", request, signal)).revision !== plan.revision) throw new Error("runtime memory changed while archival was running; no archive writes were attempted");
|
|
4046
|
-
const destinations = await
|
|
4064
|
+
const destinations = await readDirectory();
|
|
4047
4065
|
for (const memoryBodyId of new Set(routed.values())) if (!destinations.items.some((body) => body.id === memoryBodyId && eligible(body))) throw new Error(`runtime archive Memory Space ${memoryBodyId} is no longer eligible; no archive writes were attempted`);
|
|
4048
4066
|
signal.throwIfAborted();
|
|
4049
4067
|
context.assertWritable?.();
|
|
@@ -4067,6 +4085,7 @@ ${chunk.context}
|
|
|
4067
4085
|
for (const memoryBodyId of new Set(routed.values())) {
|
|
4068
4086
|
signal.throwIfAborted();
|
|
4069
4087
|
context.assertWritable?.();
|
|
4088
|
+
if (archive.writeScope !== void 0 && !(await readDirectory()).items.some((body) => body.id === memoryBodyId && eligible(body))) throw new Error(`runtime archive Memory Space ${memoryBodyId} is no longer authorized or eligible; pending write was not committed`);
|
|
4070
4089
|
const batch = sources.filter((source) => routed.get(source.index) === memoryBodyId);
|
|
4071
4090
|
const written = await memoryService.mutate("remember-many", { requests: batch.map((source) => requests[source.index - 1]) }, signal);
|
|
4072
4091
|
for (const [offset, result] of written.entries()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mnemon",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "Composable three-tier memory control plane for DeepSeek Harness: persistent runtime context, searchable project documents, pluggable long-term memory, guarded strategies, WebUI, and headless tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -149,7 +149,7 @@
|
|
|
149
149
|
"dsh-mnemon-provider-retaindb": "0.5.4",
|
|
150
150
|
"dsh-mnemon-provider-supermemory": "0.5.4",
|
|
151
151
|
"dsh-mnemon-source-documents": "0.5.6",
|
|
152
|
-
"dsh-mnemon-source-memory-spaces": "0.5.
|
|
152
|
+
"dsh-mnemon-source-memory-spaces": "0.5.7",
|
|
153
153
|
"dsh-mnemon-source-runtime": "0.5.6",
|
|
154
154
|
"dsh-mnemon-strategy-auto-capture": "0.5.4",
|
|
155
155
|
"dsh-mnemon-strategy-default-three-tier": "0.5.5",
|