dsh-llm-fallbacks 0.6.5 → 0.6.7
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 +60 -231
- package/README.zh-CN.md +77 -248
- package/dist/authorized-route.d.ts +1 -1
- package/dist/index.d.ts +23 -3
- package/dist/index.js +170 -106
- package/dist/role-persona.d.ts +71 -0
- package/dist/role-resolution.d.ts +25 -12
- package/dist/roles.d.ts +6 -4
- package/dist/subagents-seam.d.ts +83 -70
- package/package.json +26 -25
- package/scripts/repair-session-logs.ts +68 -34
- package/scripts/session-logs/catalog.ts +103 -12
- package/scripts/session-logs/publish.ts +23 -9
|
@@ -67,12 +67,20 @@ const CATALOG_PACKAGE_DIR = 'dsh-session-format-catalog'
|
|
|
67
67
|
export const CATALOG_ENV_VAR = 'DSH_SESSION_FORMAT_CATALOG'
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
* The oldest released catalog this tool accepts as its oracle
|
|
70
|
+
* The oldest released catalog this tool accepts as its oracle AND the format
|
|
71
71
|
* generation it repairs TO (`CURRENT_VERSION_FLOOR`, the pre-V3 scope). A catalog
|
|
72
72
|
* below it cannot read a successor this tool would write, so `--apply` must
|
|
73
73
|
* refuse rather than verify its own output against a build the GUI does not run.
|
|
74
74
|
* One constant for both the selection scope and the oracle floor: they are the
|
|
75
75
|
* same number and must not drift.
|
|
76
|
+
*
|
|
77
|
+
* The repair target stays at this floor even when the installed build is newer:
|
|
78
|
+
* the tool only repairs logs BELOW the floor, and the next released edge
|
|
79
|
+
* (V3→V4) requires historical child evidence (one parent's complete child
|
|
80
|
+
* descriptors) that a single-log repair cannot produce. The successor is
|
|
81
|
+
* therefore encoded through the newer build's own `historicalSessionFormatCatalog`
|
|
82
|
+
* (see {@link CatalogHandle.floor}) and the host migrates it onward, with its own
|
|
83
|
+
* child facts, exactly like any other readable pre-V3 session it opens.
|
|
76
84
|
*/
|
|
77
85
|
export const REQUIRED_CATALOG_VERSION = 3
|
|
78
86
|
|
|
@@ -131,24 +139,77 @@ export interface CatalogHandle {
|
|
|
131
139
|
readonly packageVersion: string | null
|
|
132
140
|
/** The released catalog module's `sessionFormatCatalog` export. */
|
|
133
141
|
readonly catalog: ReleasedCatalog
|
|
142
|
+
/**
|
|
143
|
+
* The catalog generation this tool REPAIRS TO — the format floor
|
|
144
|
+
* ({@link REQUIRED_CATALOG_VERSION}): the declared catalog itself when the
|
|
145
|
+
* resolved build is AT the floor, otherwise that build's
|
|
146
|
+
* `historicalSessionFormatCatalog` export (the V0–V3 reader/encoder).
|
|
147
|
+
*
|
|
148
|
+
* Every restore, classification and published successor goes through this
|
|
149
|
+
* catalog, so the successor's header version (and the `N` in its canonical
|
|
150
|
+
* filename) is the floor, never a generation whose migration needs evidence a
|
|
151
|
+
* single-log repair cannot supply. `null` when the resolved build is newer than
|
|
152
|
+
* the floor but exposes no usable historical catalog: the caller refuses the
|
|
153
|
+
* whole run through {@link catalogVersionRefusal} (fatal, exit 2) instead of
|
|
154
|
+
* publishing a generation it cannot restore, verify or hand to the host.
|
|
155
|
+
*/
|
|
156
|
+
readonly floor: ReleasedCatalog | null
|
|
134
157
|
}
|
|
135
158
|
|
|
136
159
|
/**
|
|
137
160
|
* Why a resolved catalog may not be used as this run's oracle, or `null` when it
|
|
138
161
|
* may. The caller turns a non-`null` reason into a fatal (exit 2) refusal: a
|
|
139
162
|
* below-floor catalog would encode a successor this tool's own read-back cannot
|
|
140
|
-
* vouch for, and the GUI would not run it either
|
|
163
|
+
* vouch for, and the GUI would not run it either; a newer build without a usable
|
|
164
|
+
* repair floor could not be restored or encoded at all.
|
|
141
165
|
*/
|
|
142
166
|
export function catalogVersionRefusal(handle: CatalogHandle): string | null {
|
|
143
167
|
const version = handle.catalog.currentVersion
|
|
144
|
-
if (typeof version === 'number' && Number.isSafeInteger(version) && version >= REQUIRED_CATALOG_VERSION) {
|
|
145
|
-
return
|
|
168
|
+
if (!(typeof version === 'number' && Number.isSafeInteger(version) && version >= REQUIRED_CATALOG_VERSION)) {
|
|
169
|
+
return `the resolved released catalog at ${handle.modulePath}`
|
|
170
|
+
+ `${handle.packageVersion === null ? '' : ` (package version ${handle.packageVersion})`}`
|
|
171
|
+
+ ` declares currentVersion ${JSON.stringify(version)}, but this tool requires at least `
|
|
172
|
+
+ `${REQUIRED_CATALOG_VERSION}: it would publish a successor that build cannot read. `
|
|
173
|
+
+ 'Point --catalog (or DSH_SESSION_FORMAT_CATALOG) at the catalog of the release the GUI runs.'
|
|
174
|
+
}
|
|
175
|
+
if (handle.floor === null) {
|
|
176
|
+
return `the resolved released catalog at ${handle.modulePath}`
|
|
177
|
+
+ `${handle.packageVersion === null ? '' : ` (package version ${handle.packageVersion})`}`
|
|
178
|
+
+ ` declares currentVersion ${version}, but it exposes no usable historicalSessionFormatCatalog at the `
|
|
179
|
+
+ `repair floor (v${REQUIRED_CATALOG_VERSION}). This tool repairs pre-V3 logs by publishing a v`
|
|
180
|
+
+ `${REQUIRED_CATALOG_VERSION} successor: the V3→V4 edge needs historical child evidence that a single-log `
|
|
181
|
+
+ 'repair cannot supply, so the floor reader/encoder is what restores and verifies the successor and the '
|
|
182
|
+
+ 'host migrates it onward when it opens the session. Point --catalog (or DSH_SESSION_FORMAT_CATALOG) at '
|
|
183
|
+
+ `a build that declares currentVersion >= ${REQUIRED_CATALOG_VERSION} and still exports `
|
|
184
|
+
+ 'historicalSessionFormatCatalog.'
|
|
185
|
+
}
|
|
186
|
+
const floorVersion = handle.floor.currentVersion
|
|
187
|
+
if (!(typeof floorVersion === 'number' && Number.isSafeInteger(floorVersion)
|
|
188
|
+
&& floorVersion >= REQUIRED_CATALOG_VERSION && floorVersion <= version)) {
|
|
189
|
+
return `the resolved released catalog at ${handle.modulePath} exposes a repair-floor catalog that declares `
|
|
190
|
+
+ `currentVersion ${JSON.stringify(floorVersion)}, but this tool requires a floor of at least `
|
|
191
|
+
+ `${REQUIRED_CATALOG_VERSION} no greater than the declared currentVersion ${version}. `
|
|
192
|
+
+ 'Point --catalog (or DSH_SESSION_FORMAT_CATALOG) at the catalog of the release the GUI runs.'
|
|
146
193
|
}
|
|
147
|
-
return
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
194
|
+
return null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The catalog generation this tool repairs TO (see {@link CatalogHandle.floor}).
|
|
199
|
+
*
|
|
200
|
+
* Callers reach this only after {@link catalogVersionRefusal} accepted the handle;
|
|
201
|
+
* the throw is the fail-closed backstop for a caller that skipped that gate (a
|
|
202
|
+
* restore or encode through a missing floor must never degrade to "nothing was
|
|
203
|
+
* published").
|
|
204
|
+
*/
|
|
205
|
+
export function repairCatalog(handle: CatalogHandle): ReleasedCatalog {
|
|
206
|
+
if (handle.floor === null) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`the resolved session-format catalog exposes no usable repair-floor catalog (v${REQUIRED_CATALOG_VERSION}): `
|
|
209
|
+
+ 'refusing to restore or encode a successor with it',
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
return handle.floor
|
|
152
213
|
}
|
|
153
214
|
|
|
154
215
|
/** Inputs for {@link resolveCatalog}. */
|
|
@@ -195,19 +256,44 @@ async function loadCatalog(
|
|
|
195
256
|
const modulePath = resolveCatalogEntry(candidate, env)
|
|
196
257
|
if (modulePath === null) return null
|
|
197
258
|
try {
|
|
198
|
-
const loaded = (await import(pathToFileURL(modulePath).href)) as {
|
|
259
|
+
const loaded = (await import(pathToFileURL(modulePath).href)) as {
|
|
260
|
+
sessionFormatCatalog?: unknown
|
|
261
|
+
historicalSessionFormatCatalog?: unknown
|
|
262
|
+
}
|
|
199
263
|
if (!isReleasedCatalog(loaded.sessionFormatCatalog)) return null
|
|
200
264
|
return {
|
|
201
265
|
modulePath,
|
|
202
266
|
resolvedBy,
|
|
203
267
|
packageVersion: catalogPackageVersion(modulePath),
|
|
204
268
|
catalog: loaded.sessionFormatCatalog,
|
|
269
|
+
floor: floorCatalogOf(loaded.sessionFormatCatalog, loaded.historicalSessionFormatCatalog),
|
|
205
270
|
}
|
|
206
271
|
} catch {
|
|
207
272
|
return null
|
|
208
273
|
}
|
|
209
274
|
}
|
|
210
275
|
|
|
276
|
+
/**
|
|
277
|
+
* The catalog generation this tool repairs TO: the declared catalog itself when
|
|
278
|
+
* the resolved build is at or below the floor, otherwise that build's
|
|
279
|
+
* `historicalSessionFormatCatalog` export.
|
|
280
|
+
*
|
|
281
|
+
* A build newer than the floor publishes nothing but its own current generation,
|
|
282
|
+
* and the released V3→V4 edge needs historical child evidence no single-log
|
|
283
|
+
* repair can produce; the historical export is that build's own V0–V3
|
|
284
|
+
* reader/encoder, so restoring through it and encoding the floor generation is
|
|
285
|
+
* the only successor the tool can both verify and hand to the host.
|
|
286
|
+
*
|
|
287
|
+
* @param catalog the module's declared `sessionFormatCatalog`.
|
|
288
|
+
* @param historical the module's `historicalSessionFormatCatalog`, unknown.
|
|
289
|
+
* @returns the floor catalog, or `null` when a newer build exposes none.
|
|
290
|
+
*/
|
|
291
|
+
function floorCatalogOf(catalog: ReleasedCatalog, historical: unknown): ReleasedCatalog | null {
|
|
292
|
+
if (catalog.currentVersion <= REQUIRED_CATALOG_VERSION) return catalog
|
|
293
|
+
if (isReleasedCatalog(historical) && historical.currentVersion === REQUIRED_CATALOG_VERSION) return historical
|
|
294
|
+
return null
|
|
295
|
+
}
|
|
296
|
+
|
|
211
297
|
/**
|
|
212
298
|
* The owning catalog package's own `version`, walking up from a module entry to
|
|
213
299
|
* the nearest `package.json` that names the catalog package; `null` when none is
|
|
@@ -526,7 +612,12 @@ function refusalClass(error: unknown): RefusalClass {
|
|
|
526
612
|
* The restore runs under the pinned policy
|
|
527
613
|
* `createRestore(header, { recovery: 'recoverable', validation: 'transformed' })`
|
|
528
614
|
* and every row is decoded before `finish()`, so the class is the one the
|
|
529
|
-
* released chain raises first for this log
|
|
615
|
+
* released chain raises first for this log on its way to the REPAIR FLOOR
|
|
616
|
+
* ({@link CatalogHandle.floor}) — the pre-V3 chain this tool repairs along and
|
|
617
|
+
* the generation it publishes. With a newer installed build the floor is that
|
|
618
|
+
* build's `historicalSessionFormatCatalog`; the host's forward migration from
|
|
619
|
+
* the floor to current (which needs corpus-level child evidence) is outside a
|
|
620
|
+
* single log's verdict.
|
|
530
621
|
*
|
|
531
622
|
* Returned classes: `'ok'` when the restore completes, `'source-kind'`,
|
|
532
623
|
* `'subagent-descriptor-version'`, `'unknown-event-type'` for the three released
|
|
@@ -539,7 +630,7 @@ function refusalClass(error: unknown): RefusalClass {
|
|
|
539
630
|
*/
|
|
540
631
|
export function classifyWithCatalog(rows: readonly ParsedRow[], catalog: CatalogHandle): RefusalClass {
|
|
541
632
|
try {
|
|
542
|
-
restoreRows(catalog
|
|
633
|
+
restoreRows(repairCatalog(catalog), rows, { recovery: 'recoverable', validation: 'transformed' })
|
|
543
634
|
return 'ok'
|
|
544
635
|
} catch (error) {
|
|
545
636
|
return refusalClass(error)
|
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
* (`ZSTD_c_checksumFlag`), exactly like the released writer.
|
|
15
15
|
* The successor's records come from the RELEASED encoder
|
|
16
16
|
* (`encodeCurrentHeader` / `encodeCurrentEvent`), not from the input rows, so
|
|
17
|
-
* the written generation is whatever the catalog considers
|
|
17
|
+
* the written generation is whatever the catalog's REPAIR FLOOR considers
|
|
18
|
+
* current — v3 when the installed build is newer (see `CatalogHandle.floor`).
|
|
19
|
+
* The host then migrates that v3 generation onward to the installed current
|
|
20
|
+
* format with its own historical child evidence; this module never paints a
|
|
21
|
+
* generation whose migration needs corpus-level facts a single log lacks.
|
|
18
22
|
*
|
|
19
23
|
* COMPRESSION — Node's `node:zlib` zstd API, no `zstd` CLI (measured on
|
|
20
24
|
* v24.18.0: `zstdCompress`/`zstdDecompress` exist and are used here):
|
|
@@ -66,7 +70,7 @@ import { link, readFile, rm, writeFile } from 'node:fs/promises'
|
|
|
66
70
|
import { dirname, join, resolve } from 'node:path'
|
|
67
71
|
import { constants, zstdCompressSync, zstdDecompressSync } from 'node:zlib'
|
|
68
72
|
import type { CatalogHandle, ReleasedArtifact, ReleasedCatalog } from './catalog.ts'
|
|
69
|
-
import { restoreRows } from './catalog.ts'
|
|
73
|
+
import { repairCatalog, restoreRows } from './catalog.ts'
|
|
70
74
|
import type { ParsedRow } from './rules.ts'
|
|
71
75
|
|
|
72
76
|
/** Zstandard frame magic (little-endian `0xFD2FB528`). */
|
|
@@ -333,8 +337,9 @@ function sizeToNumber(buffer: Buffer, offset: number, width: number): number | n
|
|
|
333
337
|
* function, which it receives from `runRepair`'s dynamic import of this module
|
|
334
338
|
* (a static import would defeat the `node:zlib` zstd runtime probe that must run
|
|
335
339
|
* first). `publishSuccessor` derives its target from the RESTORED header's
|
|
336
|
-
* version, the CLI's probe
|
|
337
|
-
* `currentVersion` —
|
|
340
|
+
* version (the repair floor), and the CLI's probe resolves the floor's
|
|
341
|
+
* `currentVersion` through the catalog handle — the two agree for every released
|
|
342
|
+
* catalog, whose restore settles at the floor it was chosen for.
|
|
338
343
|
*/
|
|
339
344
|
export function successorFilename(version: number): string {
|
|
340
345
|
return version === 0 ? 'session.jsonl.zstd' : `session.v${version}.jsonl.zstd`
|
|
@@ -382,6 +387,13 @@ export async function publishSuccessor(
|
|
|
382
387
|
sourceDigest: string,
|
|
383
388
|
): Promise<PublishedSuccessor> {
|
|
384
389
|
const handle = assertCatalog(catalog)
|
|
390
|
+
// The generation this tool repairs TO: the declared catalog itself on a build
|
|
391
|
+
// at the floor, the build's own historical V0–V3 catalog on a newer one. A
|
|
392
|
+
// restore through the newer build's current catalog would demand historical
|
|
393
|
+
// child evidence this single-log tool cannot produce; the floor restore has no
|
|
394
|
+
// such requirement, and the host migrates the published floor generation
|
|
395
|
+
// onward with its own child facts.
|
|
396
|
+
const floor = repairCatalog(handle)
|
|
385
397
|
const originalBytes = await readFile(logPath)
|
|
386
398
|
const originalDigest = sha256(originalBytes)
|
|
387
399
|
if (originalDigest !== sourceDigest) {
|
|
@@ -394,7 +406,7 @@ export async function publishSuccessor(
|
|
|
394
406
|
// Defensive copy: the caller's rows alias `form` / `summary` / `sections`
|
|
395
407
|
// objects, and the released restore may normalize the artifact in place.
|
|
396
408
|
const rows = structuredClone(normalizedRows)
|
|
397
|
-
const artifact = restoreRows(
|
|
409
|
+
const artifact = restoreRows(floor, rows, { recovery: 'strict', validation: 'transformed' })
|
|
398
410
|
const generation = artifact.header.version
|
|
399
411
|
const targetPath = join(dirname(logPath), successorFilename(generation))
|
|
400
412
|
if (resolve(targetPath) === resolve(logPath)) {
|
|
@@ -405,10 +417,10 @@ export async function publishSuccessor(
|
|
|
405
417
|
// (lazily): materializing them all first would hold a second copy of the session
|
|
406
418
|
// alongside the restored artifact it was built from.
|
|
407
419
|
const encodedEvents = function* (): Generator<Record<string, unknown>> {
|
|
408
|
-
for (const event of artifact.events) yield
|
|
420
|
+
for (const event of artifact.events) yield floor.encodeCurrentEvent(event)
|
|
409
421
|
}
|
|
410
422
|
const bytes = encodeZstdFrames(
|
|
411
|
-
|
|
423
|
+
floor.encodeCurrentHeader(artifact.header, artifact.inheritedEventCount),
|
|
412
424
|
encodedEvents(),
|
|
413
425
|
)
|
|
414
426
|
|
|
@@ -422,8 +434,10 @@ export async function publishSuccessor(
|
|
|
422
434
|
// names instead of ignoring them.
|
|
423
435
|
await writeFile(temporaryPath, bytes, { flag: 'wx', mode: 0o600 })
|
|
424
436
|
// Read the staged generation back with the host's current-generation policy
|
|
425
|
-
// BEFORE it becomes visible under its canonical name.
|
|
426
|
-
|
|
437
|
+
// BEFORE it becomes visible under its canonical name. The floor catalog is
|
|
438
|
+
// the one that must read it: it is the build whose encoder wrote it and the
|
|
439
|
+
// same historical reader the host uses for a pre-current generation.
|
|
440
|
+
verifyGeneration(floor, await readFile(temporaryPath), generation)
|
|
427
441
|
outcome = await publishExclusive(temporaryPath, targetPath, sha256(bytes))
|
|
428
442
|
} catch (error) {
|
|
429
443
|
try {
|