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)