Replies: 3 comments
|
Source-verified both refusals against the release history and current master ( 1.
On master the reader side refuses it exactly as you quote: 2. Flat kind-tagged
On the root cause and the fix shape — agreed on both:
One hardening suggestion beyond the patch: since the release trains are all tagged ( Attaching the complete patch here is the right venue given PRs are disabled — your two-file + tests change is small, policy-shaped, and I'd second posting it inline for the maintainers. |
|
Following the invitation above, here is the complete patch. It is the exact diff of my locally verified commit (
diff --git a/.agents/notes/implemented/bug-fix/2026-09-07-admit-uninventoried-released-v0-payload-members.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-07-admit-uninventoried-released-v0-payload-members.i18n.yaml
new file mode 100644
index 0000000000..b1924f23fa
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-09-07-admit-uninventoried-released-v0-payload-members.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-07-admit-uninventoried-released-v0-payload-members.md
+2026-09-07-admit-uninventoried-released-v0-payload-members.md: b413fe50c681c3e65b4407917f1f881e8d944dfa
+2026-09-07-admit-uninventoried-released-v0-payload-members.zh.md: a9d43866f3f3232c3b7d4762a7deb2d42f241946
diff --git a/.agents/notes/implemented/bug-fix/2026-09-07-admit-uninventoried-released-v0-payload-members.md b/.agents/notes/implemented/bug-fix/2026-09-07-admit-uninventoried-released-v0-payload-members.md
new file mode 100644
index 0000000000..b413fe50c6
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-09-07-admit-uninventoried-released-v0-payload-members.md
@@ -0,0 +1,41 @@
+# Agent Note: Admit released v0 payload members the frozen inventory missed
+
+Status: implemented
+
+English | [中文](2026-09-07-admit-uninventoried-released-v0-payload-members.zh.md)
+
+## Problem
+
+Released writers emitted released-v0 facts that the frozen released-v0 inventory in `@deepseek-ai/dsh-session-format-v0-to-v1` never admitted, so committed sessions failed observation with `SessionFormatUnsupportedMigrationError` and could never load again. Two shapes reached real logs:
+
+- Released 0.1.1-rc.1 wrote `permission/preset` data with an optional `origin` member (`'default' | 'selection' | 'inferred'`). The origin experiment was reverted on master two hours after that release merged, and the frozen inventory was authored from the surviving writer, so it froze `permission/preset` as `preset`-only.
+- Before the wrapped `{ response, blocks? }` replay envelope, the pi-ai adapter recorded a flat kind-tagged `replayState` on `finish` chunks (`{ kind: 'pi-ai', version, api, provider, model, responseId?, responseModel?, stopReason, blocks }`). The inventory froze only the wrapped form, refusing every pre-envelope finish chunk as `replayState has unexpected member "kind"`.
+
+The adjacent-migration policy keeps the source artifact byte- and inode-identical, so a refusal is permanent for that log until the reader changes.
+
+## Decision
+
+The frozen inventory admits payload members a released writer actually emitted, evidenced from that release's shipped source, and validates them at the same altitude as the members it already froze:
+
+- `permission/preset` admits optional `origin` with the released literal domain `'default' | 'selection' | 'inferred'`.
+- A `finish` chunk `replayState` carrying a `kind` member is the pre-envelope adapter-private form. `kind` must be a non-empty string and `blocks`, when present, must be an array; the remaining members and any unknown `kind` stay owner-opaque JSON, matching the inventory's merge-extensible rule for discriminant-tagged objects. A `replayState` without `kind` must still be the exact wrapped `{ response, blocks? }` form.
+
+The identity edge still preserves everything it admits; validation-only admission never rewrites a payload. Discovering further gaps repeats this procedure: evidence the released shape from the release that wrote it, extend the inventory or semantic validator, add fixtures, and rescan.
+
+## Alternatives considered
+
+**Repair the stored logs at their source.** Rejected: the artifacts are valid released v0, not malformed fixtures, and migration never moves, overwrites, or changes committed evidence. The refusals were reader defects.
+
+**Treat every `replayState` as fully owner-opaque JSON.** Rejected: the wrapped form's `{ response, blocks }` structure is the assembler-checked envelope the v2 edge deep-compares against the embedded stream, and it already has an exact local check worth keeping.
+
+**Drop `origin` and flat replay states at the edge.** Rejected: the edge is identity-shaped; silently deleting released payload facts would rewrite history behind a format bump.
+
+## Consequences
+
+- Sessions written by 0.1.1-rc.1 and by pre-envelope pi-ai builds observe, migrate to the current generation, and keep their released `origin` and replay facts intact.
+- Refusals now mean either a genuinely malformed log or a released shape no stored log has evidenced yet; the stored-log scan below is the discovery tool for the second case.
+- The inventory stops being a complete description of every possible released writer; it is complete over the writers evidence has shown, and extends only with that evidence.
+
+## Verification
+
+The v0-to-v1 and v1-to-v2 suites cover the admitted `origin` values, a refused unknown value, the flat kind-tagged envelope, and refused corrupted variants, plus identity preservation of an origin-carrying `permission/preset` through the v0-to-v1 edge. A read-only catalog scan migrated every stored `session*.jsonl[.zstd]` log on the affected machine through the full v0-to-v2 chain in memory: all 20 logs pass, including the four that refused before the fix.
diff --git a/.agents/notes/implemented/bug-fix/2026-09-07-admit-uninventoried-released-v0-payload-members.zh.md b/.agents/notes/implemented/bug-fix/2026-09-07-admit-uninventoried-released-v0-payload-members.zh.md
new file mode 100644
index 0000000000..a9d43866f3
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-09-07-admit-uninventoried-released-v0-payload-members.zh.md
@@ -0,0 +1,41 @@
+# Agent Note: Admit released v0 payload members the frozen inventory missed
+
+Status: implemented
+
+[English](2026-09-07-admit-uninventoried-released-v0-payload-members.md) | 中文
+
+## Problem
+
+已发布的写入端写出过冻结清单从未接纳的 released-v0 事实,导致 `@deepseek-ai/dsh-session-format-v0-to-v1` 的冻结清单在观察已提交会话时抛出 `SessionFormatUnsupportedMigrationError`,这些历史再也无法加载。真实日志中出现了两种形态:
+
+- 已发布的 0.1.1-rc.1 写出的 `permission/preset` data 带有可选的 `origin` 成员(`'default' | 'selection' | 'inferred'`)。该 origin 实验在版本合并两小时后就在 master 上被回退,而冻结清单是照幸存的写入端编写的,于是把 `permission/preset` 冻结成了只有 `preset`。
+- 在包裹式 `{ response, blocks? }` replay 信封出现之前,pi-ai 适配器在 `finish` chunk 上记录的是扁平的、带 `kind` 标记的 `replayState`(`{ kind: 'pi-ai', version, api, provider, model, responseId?, responseModel?, stopReason, blocks }`)。清单只冻结了包裹式形态,把每个信封出现前的 finish chunk 都以 `replayState has unexpected member "kind"` 拒绝。
+
+adjacent-migration 政策要求源文件保持字节与 inode 不变,因此在读取端修复之前,被拒绝对该日志而言是永久性的。
+
+## Decision
+
+冻结清单接纳已发布写入端实际写出、并能从对应发布版本的源码中取证 payload 成员,并按既有成员相同的深度校验它们:
+
+- `permission/preset` 接纳可选的 `origin`,取值域为已发布的字面量 `'default' | 'selection' | 'inferred'`。
+- 带 `kind` 成员的 `finish` chunk `replayState` 属于信封出现前的适配器私有形态。`kind` 必须是非空字符串,`blocks`(如存在)必须是数组;其余成员以及任何未知 `kind` 保持 owner-opaque JSON,与清单对判别式标记对象的 merge-extensible 规则一致。不带 `kind` 的 `replayState` 仍必须是精确的包裹式 `{ response, blocks? }` 形态。
+
+identity 边依旧原样保留它接纳的一切;只做校验层面的接纳,绝不改写 payload。后续再发现同类缺口的流程与此相同:从写出该形态的发布版本取证、扩展清单或语义校验器、补充 fixture,然后重扫存储日志。
+
+## Alternatives considered
+
+**在源头上修复存储日志。** 否决:这些产物是合法的已发布 v0,不是损坏的 fixture;迁移绝不移动、覆盖或更改已提交的证据。拒绝是读取端的缺陷。
+
+**把所有 `replayState` 都当成完全 owner-opaque 的 JSON。** 否决:包裹式形态的 `{ response, blocks }` 结构是 v2 边与内嵌流做深度比对的装配器校验信封,它已有的精确本地校验值得保留。
+
+**在迁移边丢弃 `origin` 与扁平 replay 状态。** 否决:该边是 identity 形态;悄悄删除已发布的 payload 事实等于借版本升级改写历史。
+
+## Consequences
+
+- 0.1.1-rc.1 与信封出现前的 pi-ai 构建写出的会话可以观察、迁移到当前世代,其已发布的 `origin` 与 replay 事实原样保留。
+- 现在的拒绝意味着两种情况之一:日志确实损坏,或者某个已发布形态还没有任何存储日志提供证据;下面的存储日志扫描就是发现第二种情况的工具。
+- 冻结清单不再是"所有可能的已发布写入端"的完整描述;它对已有证据覆盖的写入端是完整的,并且只随证据扩展。
+
+## Verification
+
+v0-to-v1 与 v1-to-v2 的测试覆盖了被接纳的 `origin` 取值、被拒绝的未知取值、扁平 kind 标记信封、被拒绝的损坏变体,以及携带 `origin` 的 `permission/preset` 在 v0-to-v1 边上的 identity 保留。一次只读的目录扫描把受影响机器上每一份 `session*.jsonl[.zstd]` 存储日志在内存中走完 v0 到 v2 的完整迁移链:全部 20 份日志通过,包括修复前被拒绝的 4 份。
diff --git a/packages/session/session-format-v0-to-v1/README.i18n.yaml b/packages/session/session-format-v0-to-v1/README.i18n.yaml
index aacad8ff35..366461470f 100644
--- a/packages/session/session-format-v0-to-v1/README.i18n.yaml
+++ b/packages/session/session-format-v0-to-v1/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session/session-format-v0-to-v1/README.md
-README.md: 829bca816c7770aecfc8cf98fbec34f3ee3a9741
-README.zh.md: 4c5566d28687bb9528d2e6070c7a03bdc52ff611
+README.md: 732c90e2b67efd8e7a10e233befc74b16f4f3166
+README.zh.md: a5f713071abbe1eb17865187db29452897763b2e
diff --git a/packages/session/session-format-v0-to-v1/README.md b/packages/session/session-format-v0-to-v1/README.md
index 829bca816c..732c90e2b6 100644
--- a/packages/session/session-format-v0-to-v1/README.md
+++ b/packages/session/session-format-v0-to-v1/README.md
@@ -38,7 +38,7 @@ const migratedV1 = sessionFormatV0ToV1.migrate(decodedV0)
`releasedV0SessionFormatCodec` reads the exact v0 header and physical rows, including packed assistant deltas and range-encoded provenance. `sessionFormatV0ToV1` normalizes and strictly validates a complete detached artifact. `releasedV1SessionFormatCodec` preserves the v1 physical layout without freezing the ordinary event vocabulary; the catalog restores current events against the installed Session package.
-The alpha edge refuses every event type outside its frozen inventory, including an unknown event marked `ignorable: true`. It also refuses unexpected payload members. `tool/result.meta` and nested PTC `arguments` remain explicit opaque JSON fields and are preserved without Session-sequence interpretation. Unknown content-block `type`, message-source `kind`, assistant finish-reason `kind`, and `turn/end` reason `kind` arms remain owner-opaque JSON while their known arms receive structural validation.
+The alpha edge refuses every event type outside its frozen inventory, including an unknown event marked `ignorable: true`. It also refuses unexpected payload members; the inventory admits the optional members a released writer actually emitted, such as `permission/preset` `origin`. `tool/result.meta` and nested PTC `arguments` remain explicit opaque JSON fields and are preserved without Session-sequence interpretation. Unknown content-block `type`, message-source `kind`, assistant finish-reason `kind`, and `turn/end` reason `kind` arms remain owner-opaque JSON while their known arms receive structural validation. A `finish` chunk `replayState` recorded before the wrapped envelope keeps its kind-tagged adapter-private form; an unknown `kind` stays owner-opaque.
The bounded historical normalizers convert `steering/message` to `user/message`, remove `turn/start.trigger`, convert retired `turn/end` reasons, add the current message wrappers and deterministic legacy message ids, and remove the obsolete `request/header.header.messagePrefix` duplicate. Retired `request/header-delta`, `mode/set`, and the `request/header` fallback reason refuse migration. No other event, reference, source, or payload fact may change.
diff --git a/packages/session/session-format-v0-to-v1/README.zh.md b/packages/session/session-format-v0-to-v1/README.zh.md
index 4c5566d286..a5f713071a 100644
--- a/packages/session/session-format-v0-to-v1/README.zh.md
+++ b/packages/session/session-format-v0-to-v1/README.zh.md
@@ -38,7 +38,7 @@ const migratedV1 = sessionFormatV0ToV1.migrate(decodedV0)
`releasedV0SessionFormatCodec` 读取精确的 v0 标头与物理行,包括打包的 Assistant 增量和范围编码的来源序号。`sessionFormatV0ToV1` 规范化并严格校验一个完整且分离的产物。`releasedV1SessionFormatCodec` 在不冻结普通事件词表的前提下保留 v1 物理布局;目录会根据已安装的 Session 包还原当前事件。
-Alpha 迁移边会拒绝冻结清单之外的所有事件类型,包括带有 `ignorable: true` 标记的未知事件。它也会拒绝意外的 payload 成员。`tool/result.meta` 与嵌套 PTC `arguments` 是显式的不透明 JSON 字段;迁移会原样保留它们,不把其中的数字解释为 Session 序号。未知 content-block `type`、message-source `kind`、assistant finish-reason `kind` 与 `turn/end` reason `kind` 分支保持 owner-opaque JSON,已知分支则接受结构校验。
+Alpha 迁移边会拒绝冻结清单之外的所有事件类型,包括带有 `ignorable: true` 标记的未知事件。它也会拒绝意外的 payload 成员;冻结清单会接纳已发布写入端实际写出的可选成员,例如 `permission/preset` 的 `origin`。`tool/result.meta` 与嵌套 PTC `arguments` 是显式的不透明 JSON 字段;迁移会原样保留它们,不把其中的数字解释为 Session 序号。未知 content-block `type`、message-source `kind`、assistant finish-reason `kind` 与 `turn/end` reason `kind` 分支保持 owner-opaque JSON,已知分支则接受结构校验。包裹式信封出现之前的 `finish` chunk `replayState` 保留其带 `kind` 标记的适配器私有形态;未知 `kind` 保持 owner-opaque。
有限的历史规范化会把 `steering/message` 转换为 `user/message`、移除 `turn/start.trigger`、转换已停用的 `turn/end` reason、添加当前消息包装层与确定性的旧消息 id,并移除已停用且重复的 `request/header.header.messagePrefix`。已停用的 `request/header-delta`、`mode/set` 和 `request/header` fallback reason 会使迁移失败。除此之外,任何事件、引用、来源或 payload 事实都不得改变。
diff --git a/packages/session/session-format-v0-to-v1/src/dispositions.ts b/packages/session/session-format-v0-to-v1/src/dispositions.ts
index 24702e8bb1..2e850c307f 100644
--- a/packages/session/session-format-v0-to-v1/src/dispositions.ts
+++ b/packages/session/session-format-v0-to-v1/src/dispositions.ts
@@ -73,7 +73,7 @@ export const RELEASED_V0_EVENT_DISPOSITIONS: Readonly<Record<string, ReleasedV0P
),
'llm/retry-started': disposition(['retryId', 'turn', 'step', 'retry']),
'model/selection': disposition(['provider', 'model'], ['reasoningEffort']),
- 'permission/preset': disposition(['preset']),
+ 'permission/preset': disposition(['preset'], ['origin']),
'plan/mode': disposition(['active']),
'request/context': disposition(['provider', 'model'], ['contextWindow']),
'request/header': disposition(['header', 'reason'], ['startsSeries']),
diff --git a/packages/session/session-format-v0-to-v1/src/payload-validation.ts b/packages/session/session-format-v0-to-v1/src/payload-validation.ts
index 0a69ca4d1f..9115d47f7d 100644
--- a/packages/session/session-format-v0-to-v1/src/payload-validation.ts
+++ b/packages/session/session-format-v0-to-v1/src/payload-validation.ts
@@ -147,6 +147,10 @@ export function assertReleasedPayloadSemantics(event: SessionFormatEvent, versio
return
case 'permission/preset':
nonEmptyString(data['preset'], `${label} preset`)
+ /* Released 0.1.1-rc.1 recorded where the selection came from before the member was retired. */
+ if (data['origin'] !== undefined) {
+ literalValue(data['origin'], ['default', 'selection', 'inferred'], `${label} origin`)
+ }
return
case 'plan/mode':
booleanValue(data['active'], `${label} active`)
@@ -725,7 +729,16 @@ function finishReasonValue(value: SessionFormatJsonValue | undefined, label: str
}
function replayEnvelopeValue(value: SessionFormatJsonValue | undefined, label: string): void {
- const replay = exactRecord(value, label, ['response'], ['blocks'])
+ const replay = releasedV0Record(value, label)
+ /* Pre-envelope writers recorded a kind-tagged adapter-private object; unknown kinds stay owner-opaque. */
+ if (replay['kind'] !== undefined) {
+ nonEmptyString(replay['kind'], `${label} kind`)
+ if (replay['blocks'] !== undefined && !Array.isArray(replay['blocks'])) {
+ throw new SessionFormatError(`${label} blocks must be an array`)
+ }
+ return
+ }
+ exactRecord(value, label, ['response'], ['blocks'])
if (replay['blocks'] !== undefined && !Array.isArray(replay['blocks'])) {
throw new SessionFormatError(`${label} blocks must be an array`)
}
diff --git a/packages/session/session-format-v0-to-v1/tests/migration.spec.ts b/packages/session/session-format-v0-to-v1/tests/migration.spec.ts
index 33bb1376e6..89617eaf45 100644
--- a/packages/session/session-format-v0-to-v1/tests/migration.spec.ts
+++ b/packages/session/session-format-v0-to-v1/tests/migration.spec.ts
@@ -128,6 +128,19 @@ describe('released Session format v0 to v1', () => {
expect(releasedV1SessionFormatCodec.decodeArtifact(v1, [prefix, event]).events).toEqual([prefix, event])
})
+ it('preserves the released permission-preset origin member through the identity edge', () => {
+ const rows = [
+ { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
+ {
+ type: 'permission/preset', seq: 1, time: 2,
+ data: { preset: 'workspace-write', origin: 'default' },
+ },
+ ]
+ const header = { type: 'session', version: 0, id: 'preset-origin', createdAt: 1, delegationDepth: 0 }
+ const migrated = sessionFormatV0ToV1.migrate(releasedV0SessionFormatCodec.decodeArtifact(header, rows))
+ expect(migrated.events).toEqual(rows)
+ })
+
it('preserves a complete canonical multi-owner log except for header.version', () => {
const physicalHeader = {
type: 'session', version: 0, id: 'full-identity', createdAt: 1, cwd: '/work', delegationDepth: 0,
diff --git a/packages/session/session-format-v0-to-v1/tests/validation.spec.ts b/packages/session/session-format-v0-to-v1/tests/validation.spec.ts
index 6c26a510e5..8effb4bcf8 100644
--- a/packages/session/session-format-v0-to-v1/tests/validation.spec.ts
+++ b/packages/session/session-format-v0-to-v1/tests/validation.spec.ts
@@ -426,11 +426,33 @@ describe('released event and payload inventory', () => {
{ type: 'finish', reason: { kind: 'max-tokens' } },
{ type: 'finish', reason: { kind: 'aborted', failure: { message: 'abort', code: 'ABORT' } } },
{ type: 'finish', reason: { kind: 'error', failure: { message: 'error', code: 'ERROR' } } },
+ {
+ type: 'finish', reason: { kind: 'tool-calls' },
+ replayState: {
+ kind: 'pi-ai', version: 1, api: 'openai-completions', provider: 'mock', model: 'mock',
+ responseId: 'response', stopReason: 'toolUse',
+ blocks: [{ type: 'reasoning', thinkingSignature: 'sig' }, { type: 'tool-call' }],
+ },
+ },
+ { type: 'finish', reason: { kind: 'stop' }, replayState: { kind: 'future-adapter', response: 'opaque' } },
]
for (const chunk of chunks) {
expect(() => { assertPayload('assistant/chunk', { turn: 1, step: 0, chunk }) }).not.toThrow()
}
+ const invalidReplayStates: SessionFormatJsonValue[] = [
+ { response: 'missing nothing else', extra: true },
+ { kind: 'pi-ai', blocks: {} },
+ { kind: 7 },
+ ]
+ for (const replayState of invalidReplayStates) {
+ expect(() => {
+ assertPayload('assistant/chunk', {
+ turn: 1, step: 0, chunk: { type: 'finish', reason: { kind: 'stop' }, replayState },
+ })
+ }).toThrow()
+ }
+
const turnReasons: SessionFormatJsonValue[] = [
{ kind: 'blocked' }, { kind: 'max-tokens' }, { kind: 'interrupted' },
{ kind: 'aborted', reason: { kind: 'user' } },
@@ -559,6 +581,9 @@ describe('released event and payload inventory', () => {
turn: 1, point: 'Stop', handlerId: 'h', decision: 'pass', exitCode: -1,
stderrSummary: 'stderr', durationMs: 1,
}],
+ ['permission/preset', { preset: 'workspace-write', origin: 'default' }],
+ ['permission/preset', { preset: 'danger-full-access', origin: 'selection' }],
+ ['permission/preset', { preset: 'workspace-write', origin: 'inferred' }],
['request/header', {
header: {
config: { provider: 'p', model: 'm', temperature: 0.5, maxTokens: 5, stop: ['stop'] },
@@ -575,6 +600,7 @@ describe('released event and payload inventory', () => {
it('refuses every relationship-specific invalid payload branch', () => {
const cases: Array<[string, SessionFormatJsonValue]> = [
['command/done', { commandId: 'c', kind: 'success', sourceEventSeq: 3 }],
+ ['permission/preset', { preset: 'workspace-write', origin: 'released-later' }],
['session/title-llm-request', {
titleProvider: 'p', messageSeqs: [], route: { provider: 'p', model: 'm' },
system: 's', messages: [userMessage], maxTokens: 1,
diff --git a/packages/session/session-format-v1-to-v2/tests/validation.spec.ts b/packages/session/session-format-v1-to-v2/tests/validation.spec.ts
index 891d4788a1..1d6a10ffba 100644
--- a/packages/session/session-format-v1-to-v2/tests/validation.spec.ts
+++ b/packages/session/session-format-v1-to-v2/tests/validation.spec.ts
@@ -284,6 +284,15 @@ describe('released v2 seed and surface relationships', () => {
])) }).not.toThrow()
})
+ it('admits the released permission-preset origin member and refuses other values', () => {
+ expect(() => { assertReleasedV2Artifact(artifact([
+ event('permission/preset', 0, { preset: 'workspace-write', origin: 'default' }),
+ ])) }).not.toThrow()
+ expect(() => { assertReleasedV2Artifact(artifact([
+ event('permission/preset', 0, { preset: 'workspace-write', origin: 'refreshable' }),
+ ])) }).toThrow(/origin/)
+ })
+
it.each([
['seeded marker cut mismatch', artifact([event('session/end-seed', 0, { inherited: true })], {
header: { version: 2, id: 'seeded', createdAt: 1, isSeeded: true, delegationDepth: 0 }, |
|
The branch is now published as a fetchable reference implementation:
Apply it with the commit message and authorship preserved — no manual diffing needed: # fetch the branch
git fetch https://github.com/terryso/deepseek-harness fix/session-admit-released-v0-members
git cherry-pick FETCH_HEAD # or: git checkout FETCH_HEAD
# … or apply the single-file mbox that GitHub generates for the commit:
curl -LO https://github.com/terryso/deepseek-harness/commit/ae9ae8be2767f5cb4bb07c194e693d157cfbbe50.patch
git am ae9ae8be2767f5cb4bb07c194e693d157cfbbe50.patchThe fetched commit is byte-identical to the diff posted above ( |
Uh oh!
There was an error while loading. Please reload this page.
Summary
After upgrading from an early-0.1.1-era build to a current master build, several historical sessions can no longer be opened. The Web UI (and any resume/query path) refuses them with:
The refusal is permanent for an affected log: the adjacent-migration policy keeps the source artifact byte-identical, so the session can neither load, resume, nor be queried until the reader changes.
Root cause
The frozen released-v0 payload inventory (
RELEASED_V0_EVENT_DISPOSITIONSinpackages/session/session-format-v0-to-v1/src/dispositions.ts) describes the writers that survive on master, not every writer that actually shipped. Two released shapes are missing from it:1.
permission/presetwith anoriginmember — shipped in 0.1.1-rc.1.The origin-tracking experiment (PR #2608) merged before the 0.1.1-rc.1 release merge on 2026-08-21 and was reverted on master the same day, after the release had already been cut. Sessions written by that release carry:
{"type":"permission/preset","seq":0,"data":{"preset":"workspace-write","origin":"default"}}The inventory freezes
permission/presetaspreset-only, so every such log refuses observation.2. Flat kind-tagged
replayStateonfinishchunks — shipped before the wrapped replay envelope.Before the envelope became
{ response, blocks? }(master change on 2026-08-15, "align replay state with assembled content…"), the pi-ai adapter recorded a flat, adapter-private object:{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"kind":"pi-ai","version":1, "api":"openai-completions","provider":"…","model":"…","responseId":"…","stopReason":"toolUse", "blocks":[{"type":"reasoning","thinkingSignature":"…"},{"type":"tool-call"}]}}Any log containing such a chunk refuses with
assistant/chunk N chunk replayState has unexpected member "kind".Reproduction
Open (in the Web UI or via resume) a session that was written by 0.1.1-rc.1 and pinned a permission preset — or any session recorded through the pi-ai provider by a build older than the wrapped-envelope change.
A read-only confirmation that does not touch disk:
Scanning every stored log on my machine (20 logs across 8 project trees): 5 refused before the fix — one with the
originrefusal, four with thereplayStaterefusal — and all 20 pass after it.Proposed solution
Admit payload members that a released writer actually emitted, evidenced from the source of the release that wrote them, and validate them at the same altitude as the already-frozen members:
dispositions.ts— admit the optional member; the released-v2 table inherits it automatically because it retains v0 entries:payload-validation.ts— validateoriginagainst the released literal domain:replayEnvelopeValue— treat areplayStatecarrying akindmember as the pre-envelope adapter-private form: require a non-empty stringkind, requireblocksto be an array when present, and preserve the remaining members owner-opaque, consistent with the inventory's existing merge-extensible rule for discriminant-tagged objects. AreplayStatewithoutkindkeeps the exact wrapped{ response, blocks? }check:Admission stays validation-only: the identity edge still preserves payloads exactly, so released facts such as
originand replay states survive migration into the current generation unchanged. Since the repo does not accept external pull requests, I'm happy to attach the complete patch (two source files plus tests for both adjacent edges) here if that helps.Environment
All reactions