dsh-llm-fallbacks 0.5.0 → 0.5.2
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/README.md +5 -1
- package/README.zh-CN.md +5 -1
- package/dist/client/SubagentRoleBadge.d.ts +40 -15
- package/dist/client/fallbacks-store.d.ts +0 -53
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +68 -145
- package/dist/gateway.d.ts +2 -55
- package/dist/index.d.ts +0 -23
- package/dist/index.js +557 -66
- package/dist/role-notice.d.ts +156 -0
- package/dist/role-projection-key.d.ts +41 -0
- package/dist/role-projection.d.ts +151 -0
- package/dist/subagents-seam.d.ts +292 -0
- package/package.json +24 -23
- package/scripts/build-client.ts +1 -1
- package/scripts/repair-session-logs.ts +239 -57
- package/scripts/session-logs/publish.ts +79 -26
|
@@ -154,24 +154,42 @@ function recordLine(record: Record<string, unknown>, subject: string): string {
|
|
|
154
154
|
* frame holds EXACTLY the header line, the second (when the log has events)
|
|
155
155
|
* holds the event batch.
|
|
156
156
|
*
|
|
157
|
+
* The event batch is folded one line at a time into a byte chunk list: each
|
|
158
|
+
* event's JSON string is dropped as soon as its chunk exists, so those strings
|
|
159
|
+
* never accumulate. The chunk list is then concatenated ONCE for the compressor,
|
|
160
|
+
* so the batch's whole plaintext DOES exist at that moment — as that single
|
|
161
|
+
* contiguous buffer, which is the batch's peak (the per-record chunks it was built
|
|
162
|
+
* from are still referenced by the list while it lives). What the fold avoids is a
|
|
163
|
+
* SECOND copy of the batch, not every copy: no `join('')` string is built beside
|
|
164
|
+
* the chunks. The bytes are the same ones a `join('')` + encode would produce.
|
|
165
|
+
*
|
|
157
166
|
* @param headerRecord released current header record.
|
|
158
167
|
* @param eventRecords released current event records, in log order.
|
|
159
168
|
* @returns the complete container bytes.
|
|
160
169
|
*/
|
|
161
170
|
export function encodeZstdFrames(
|
|
162
171
|
headerRecord: Record<string, unknown>,
|
|
163
|
-
eventRecords:
|
|
172
|
+
eventRecords: Iterable<Record<string, unknown>>,
|
|
164
173
|
): Buffer {
|
|
165
174
|
const frames = [compressFrame(recordLine(headerRecord, 'session header'))]
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
175
|
+
const batch: Buffer[] = []
|
|
176
|
+
let index = 0
|
|
177
|
+
for (const record of eventRecords) {
|
|
178
|
+
batch.push(Buffer.from(recordLine(record, `session event ${index}`), 'utf8'))
|
|
179
|
+
index += 1
|
|
169
180
|
}
|
|
181
|
+
if (batch.length > 0) frames.push(compressFrame(Buffer.concat(batch)))
|
|
170
182
|
return Buffer.concat(frames)
|
|
171
183
|
}
|
|
172
184
|
|
|
173
185
|
/**
|
|
174
|
-
* Decode every frame of one concatenated container, in file order
|
|
186
|
+
* Decode every frame of one concatenated container, in file order, ONE FRAME AT A
|
|
187
|
+
* TIME: the consumer receives frame N's plaintext and decides when to ask for frame
|
|
188
|
+
* N+1, so this generator holds ONE frame's plaintext at a time and never two. That
|
|
189
|
+
* bounds the FRAME, not the session: the event batch is a single frame, so it IS the
|
|
190
|
+
* session's whole event plaintext (see {@link MAX_FRAME_PLAINTEXT_BYTES}). The frame
|
|
191
|
+
* STRUCTURE (the scanner) is still walked before the first frame's plaintext is
|
|
192
|
+
* produced, so a torn or corrupt container is refused up front.
|
|
175
193
|
*
|
|
176
194
|
* Bounded: the scanner reports each frame's DECLARED content size, a declaration
|
|
177
195
|
* above {@link MAX_FRAME_PLAINTEXT_BYTES} is refused before any decompression, and
|
|
@@ -180,10 +198,10 @@ export function encodeZstdFrames(
|
|
|
180
198
|
* coded exit), never an out-of-memory crash.
|
|
181
199
|
*
|
|
182
200
|
* @param bytes container bytes (an original log or a published generation).
|
|
183
|
-
* @
|
|
201
|
+
* @yields the plaintext of each frame, in file order.
|
|
184
202
|
*/
|
|
185
|
-
export function
|
|
186
|
-
|
|
203
|
+
export function* decodeZstdFrameTexts(bytes: Buffer): Generator<string> {
|
|
204
|
+
for (const [index, { start, end, declaredBytes }] of scanZstdFrames(bytes).entries()) {
|
|
187
205
|
if (declaredBytes !== null && declaredBytes > MAX_FRAME_PLAINTEXT_BYTES) {
|
|
188
206
|
throw new Error(
|
|
189
207
|
`refusing to decompress Zstandard frame ${index} at byte ${start}: it declares ${declaredBytes} bytes of `
|
|
@@ -206,13 +224,26 @@ export function decodeZstdFrames(bytes: Buffer): string[] {
|
|
|
206
224
|
+ `${plaintext.length}`,
|
|
207
225
|
)
|
|
208
226
|
}
|
|
209
|
-
|
|
210
|
-
}
|
|
227
|
+
yield plaintext.toString('utf8')
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Materializing convenience over {@link decodeZstdFrameTexts}: every frame's
|
|
233
|
+
* plaintext as one array. Specification code uses it as the independent read-back
|
|
234
|
+
* oracle; nothing on the tool's own path needs the whole container in memory, so
|
|
235
|
+
* production code consumes the generator instead.
|
|
236
|
+
*
|
|
237
|
+
* @param bytes container bytes.
|
|
238
|
+
* @returns the plaintext of each frame, in file order.
|
|
239
|
+
*/
|
|
240
|
+
export function decodeZstdFrames(bytes: Buffer): string[] {
|
|
241
|
+
return [...decodeZstdFrameTexts(bytes)]
|
|
211
242
|
}
|
|
212
243
|
|
|
213
244
|
/** Compress one frame with the released writer's checksummed options. */
|
|
214
|
-
function compressFrame(text: string): Buffer {
|
|
215
|
-
return zstdCompressSync(Buffer.from(text, 'utf8'), ZSTD_CHECKSUM_OPTIONS)
|
|
245
|
+
function compressFrame(text: string | Buffer): Buffer {
|
|
246
|
+
return zstdCompressSync(typeof text === 'string' ? Buffer.from(text, 'utf8') : text, ZSTD_CHECKSUM_OPTIONS)
|
|
216
247
|
}
|
|
217
248
|
|
|
218
249
|
/**
|
|
@@ -296,8 +327,16 @@ function sizeToNumber(buffer: Buffer, offset: number, width: number): number | n
|
|
|
296
327
|
* Canonical successor filename for one generation (SSOT:
|
|
297
328
|
* `generationLogFilename` in `@deepseek-ai/dsh-session-persistence-jsonl`):
|
|
298
329
|
* version 0 keeps the suffix-only name, later generations carry `v<N>`.
|
|
330
|
+
*
|
|
331
|
+
* Exported because this is the ONE implementation of the rule: the CLI's
|
|
332
|
+
* report-mode existing-successor probe resolves the same name through this
|
|
333
|
+
* function, which it receives from `runRepair`'s dynamic import of this module
|
|
334
|
+
* (a static import would defeat the `node:zlib` zstd runtime probe that must run
|
|
335
|
+
* first). `publishSuccessor` derives its target from the RESTORED header's
|
|
336
|
+
* version, the CLI's probe from the resolved catalog's declared
|
|
337
|
+
* `currentVersion` — see the probe's comment in `repair-session-logs.ts`.
|
|
299
338
|
*/
|
|
300
|
-
function successorFilename(version: number): string {
|
|
339
|
+
export function successorFilename(version: number): string {
|
|
301
340
|
return version === 0 ? 'session.jsonl.zstd' : `session.v${version}.jsonl.zstd`
|
|
302
341
|
}
|
|
303
342
|
|
|
@@ -362,9 +401,15 @@ export async function publishSuccessor(
|
|
|
362
401
|
throw new Error(`refusing to publish the successor generation over its own source: ${logPath}`)
|
|
363
402
|
}
|
|
364
403
|
|
|
404
|
+
// The released encoder's records are handed to `encodeZstdFrames` one at a time
|
|
405
|
+
// (lazily): materializing them all first would hold a second copy of the session
|
|
406
|
+
// alongside the restored artifact it was built from.
|
|
407
|
+
const encodedEvents = function* (): Generator<Record<string, unknown>> {
|
|
408
|
+
for (const event of artifact.events) yield handle.catalog.encodeCurrentEvent(event)
|
|
409
|
+
}
|
|
365
410
|
const bytes = encodeZstdFrames(
|
|
366
411
|
handle.catalog.encodeCurrentHeader(artifact.header, artifact.inheritedEventCount),
|
|
367
|
-
|
|
412
|
+
encodedEvents(),
|
|
368
413
|
)
|
|
369
414
|
|
|
370
415
|
const temporaryPath = join(dirname(logPath), `session.repair.${randomBytes(6).toString('hex')}.jsonl.zstd.tmp`)
|
|
@@ -474,19 +519,27 @@ function verifyGeneration(
|
|
|
474
519
|
bytes: Buffer,
|
|
475
520
|
expectedGeneration: number,
|
|
476
521
|
): ReleasedArtifact {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
522
|
+
// The staged container is read back frame by frame: no array of every frame's
|
|
523
|
+
// plaintext, no joined event batch and no split of it — each frame's lines are
|
|
524
|
+
// decoded as that frame is decompressed.
|
|
525
|
+
let restore: ReturnType<ReleasedCatalog['createRestore']> | null = null
|
|
526
|
+
for (const frame of decodeZstdFrameTexts(bytes)) {
|
|
527
|
+
if (restore === null) {
|
|
528
|
+
if (frame.indexOf('\n') !== frame.length - 1) {
|
|
529
|
+
throw new Error('published generation frame 1 is not exactly one header line')
|
|
530
|
+
}
|
|
531
|
+
restore = catalog.createRestore(JSON.parse(frame.slice(0, -1)), {
|
|
532
|
+
recovery: 'strict',
|
|
533
|
+
validation: 'current',
|
|
534
|
+
})
|
|
535
|
+
continue
|
|
536
|
+
}
|
|
537
|
+
for (const line of frame.split('\n')) {
|
|
538
|
+
if (line.length === 0) continue
|
|
539
|
+
restore.decodeRow(JSON.parse(line))
|
|
540
|
+
}
|
|
489
541
|
}
|
|
542
|
+
if (restore === null) throw new Error('published generation carries no Zstandard frame')
|
|
490
543
|
const artifact = restore.finish()
|
|
491
544
|
if (artifact.header.version !== expectedGeneration) {
|
|
492
545
|
throw new Error(
|