From af9c69bd09ada7492bdd8c8d657a91ecff0ae2a9 Mon Sep 17 00:00:00 2001 From: bowenliang123 Date: Sun, 20 Sep 2026 10:30:01 +0800 Subject: [PATCH] fix(host): classify backfill format refusals by the observable name family The persistence seam translates the format edge's SessionFormatUnsupportedMigrationError into SessionFormatUnsupportedError (with the "source vN artifact remains unchanged" suffix) before the error escapes, so the exact-name check never matched on any supported dsh baseline: the skip summary never fired and every refused legacy session kept its own WARN (issue #75). Classify by the SessionFormatUnsupported prefix instead, and cover both faces of the translation in the fixture. --- src/host/backfill.ts | 27 +++++++++++++++++---------- tests/host/backfill.spec.ts | 24 ++++++++++++++++++------ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/host/backfill.ts b/src/host/backfill.ts index e0be9e1..f898efc 100644 --- a/src/host/backfill.ts +++ b/src/host/backfill.ts @@ -93,8 +93,15 @@ interface ConnectionHostFace { */ const YIELD_MS = 100 -/** The `name` dsh raises for a durable log its migration surface refuses. */ -const UNSUPPORTED_MIGRATION = 'SessionFormatUnsupportedMigrationError' +/** + * The `name` family dsh raises for a durable log it cannot interpret. The + * format edge throws `SessionFormatUnsupportedMigrationError`, but the + * persistence seam translates it — together with every other "intact yet not + * interpretable" refusal — into `SessionFormatUnsupportedError` before the + * error escapes (issue #75): classify by the family prefix, since either + * exact name may be the one that arrives. + */ +const UNSUPPORTED_FORMAT = 'SessionFormatUnsupported' function asRecord(value: unknown): Record | null { return value !== null && typeof value === 'object' ? value as Record : null @@ -170,15 +177,15 @@ function isLive(sessions: Record | null, id: string): boolean { } /** - * Whether the cold read failed because the running harness refuses to - * migrate this log — a permanent, source-side refusal (the artifact is left - * unchanged, so retrying cannot succeed). Matched by the error's documented - * `name`, so no dsh symbol needs importing and every supported baseline - * classifies alike: an unknown error shape just falls to the warn path. + * Whether the cold read failed because the running harness refuses this log's + * format — a permanent, source-side refusal (the artifact is left unchanged, + * so retrying cannot succeed). Matched by the error's documented `name`, so no + * dsh symbol needs importing and every supported baseline classifies alike: an + * unknown error shape just falls to the warn path. */ -function isUnsupportedMigration(error: unknown): boolean { +function isUnsupportedFormat(error: unknown): boolean { try { - return asRecord(error)?.name === UNSUPPORTED_MIGRATION + return String(asRecord(error)?.name).startsWith(UNSUPPORTED_FORMAT) } catch { return false } @@ -271,7 +278,7 @@ export function watchActivityBackfill(ctx: Context): () => void { ;(cache as unknown as ProjectionCacheLike).coldSnapshot(log.header, log.inheritedEventCount, log.events) folded++ } catch (error: unknown) { - if (isUnsupportedMigration(error)) { + if (isUnsupportedFormat(error)) { // Permanent and source-side: the per-session detail drops to // debug; the summary below is the pass's only above-debug word. refused++ diff --git a/tests/host/backfill.spec.ts b/tests/host/backfill.spec.ts index 2176250..598c876 100644 --- a/tests/host/backfill.spec.ts +++ b/tests/host/backfill.spec.ts @@ -59,10 +59,19 @@ function captureLogs(ctx: Context): LogLine[] { return lines } -/** The migration-refusal error dsh raises for a legacy log it cannot migrate. */ -function unsupportedMigrationError(): Error { - const error = new Error('subagent/descriptor 0 uses unsupported descriptor version 2; source v0 artifact remains unchanged') - error.name = 'SessionFormatUnsupportedMigrationError' +/** + * One format-refusal face of the persistence seam. The format edge throws + * `SessionFormatUnsupportedMigrationError`, but the seam translates it — with + * the "source vN artifact remains unchanged (raw log: …)" suffix — into + * `SessionFormatUnsupportedError` before the error escapes (issue #75): the + * pass must classify BOTH faces as the same refusal. + */ +function unsupportedFormatError(name: string): Error { + const error = new Error( + 'subagent/descriptor 0 uses unsupported descriptor version 2' + + '; source v0 artifact remains unchanged (raw log: C:\\Users\\u\\.dsh\\sessions\\log.jsonl.zstd)', + ) + error.name = name return error } @@ -435,8 +444,11 @@ describe('watchActivityBackfill', () => { { header: { id: 'legacy-2', cwd: '/repo/legacy-2' } }, { header: { id: 'good', cwd: '/repo/good' } }, ]) - state.failReads.set('legacy-1', unsupportedMigrationError()) - state.failReads.set('legacy-2', unsupportedMigrationError()) + // One refusal per face of the seam's translation: the translated name a + // real dsh delivers, and the untranslated format-edge name (either may + // arrive) — both classify alike, one summary for the pair. + state.failReads.set('legacy-1', unsupportedFormatError('SessionFormatUnsupportedError')) + state.failReads.set('legacy-2', unsupportedFormatError('SessionFormatUnsupportedMigrationError')) const route = arm(ctx, state) const lines = captureLogs(ctx) const dispose = watchActivityBackfill(ctx)