Repository navigation
Replies: 6 comments
|
Sanitized minimal fixture attached: https://gist.github.com/rusliksu/4495d81e38654aed73e972129ddec218 The failing log — header + 13 events, synthetic text only, {"type":"session","version":4,"id":"session-11111111-1111-4111-8111-111111111111","createdAt":1700000000000,"isSeeded":false,"delegationDepth":0}
{"type":"turn/start","seq":0,"time":1700000000000,"data":{"turn":1}}
{"type":"step/start","seq":1,"time":1700000000000,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":2,"time":1700000000001,"data":{"content":[{"type":"text","text":"hello"}],"source":{"kind":"user"},"role":"user","id":"u-0001"},"surfaceOp":"append"}
{"type":"assistant/message","seq":3,"time":1700000000002,"data":{"turn":1,"step":1,"message":{"id":"a-0001","role":"assistant","content":[{"type":"text","text":"hi"}],"source":{"kind":"model","provider":"test","model":"test"}},"usage":{"inputTokens":0,"outputTokens":0,"cacheReadTokens":0,"reasoningTokens":0},"stream":[]},"surfaceOp":"append"}
{"type":"step/end","seq":4,"time":1700000000002,"data":{"turn":1,"step":1}}
{"type":"turn/end","seq":5,"time":1700000000002,"data":{"turn":1,"reason":{"kind":"completed"}}}
{"type":"session/end-seed","seq":6,"time":1700000000003,"data":{}}
{"type":"turn/start","seq":7,"time":1700000000003,"data":{"turn":2}}
{"type":"step/start","seq":8,"time":1700000000003,"data":{"turn":2,"step":1}}
{"type":"system/message","seq":9,"time":1700000000004,"data":{"turn":2,"step":1,"message":{"id":"s-0001","role":"system","content":[{"type":"text","text":"You are a test assistant."}],"source":{"kind":"system-prompt"}}},"surfaceOp":"append"}
{"type":"user/message","seq":10,"time":1700000000005,"data":{"content":[{"type":"text","text":"again"}],"source":{"kind":"user"},"role":"user","id":"u-0002"},"surfaceOp":"append"}
{"type":"step/end","seq":11,"time":1700000000005,"data":{"turn":2,"step":1}}
{"type":"turn/end","seq":12,"time":1700000000005,"data":{"turn":2,"reason":{"kind":"completed"}}}The surface starts with Headless reproduction (run in a DSH checkout with installed workspace deps, Node 22+/24): node validate-fixture.mjs repro-session.v4.jsonl
# SessionFormatError: system/message requires a protected first surface head
node validate-fixture.mjs control-session.v4.jsonl
# OK: 12 events validate (control-session.v4.jsonl)
In-app reproduction: Note: the fixture reproduces the unreadable-log part only. The second part of the report — the startup rewrite truncating such a log to a frame prefix without backup — needs a real (or long synthetic) log and is described with the observed sizes/timeline in the opening post. Keep a copy if you reproduce in-app. |
|
Follow-up: tried to reproduce the destructive part (startup truncation) with the fixture — it did not reproduce; correction to the "app start" claim. What I did (isolated environment, no real user data):
Results:
So the "rewritten to a frame prefix at app start" part of my first post is not reproduced by a clean start or by opening the corrupt session; that claim was inferred from mtime/size evidence in the affected installation (log replaced by exactly its first two complete frames), and the responsible path is still unknown. Context from the affected installation: dozens of session logs received synthetic closers in the same second while the app was already running, so some background path does restore sessions at runtime — but our isolated client only restored sessions it opened. Questions for maintainers:
Repro (same as the gist README): node make-fixture.mjs repro-session.v4.jsonl
DSH_HOME=<iso> dsh --profile web --no-open --port 0 # passive start: file unchanged
# open printed URL, click the session: read error above; file still unchanged |
|
Code search: is there a canonical-rewrite / checksum-upgrade / recovery pass, and does it keep a backup? I audited the shipped 1. No canonical rewrite / checksum upgrade for existing logs
2. The only in-place truncation is torn-tail repair
The codebase does have a backup convention — but not for logs: JSON state documents are moved aside as 3. No startup pass writes session logs
4. Third-party context for the affected installationThe affected desktop profile previously contained I checked its vendored persistence too: same 5. Consequence for the incidentNothing in the shipped code (nor in the previously installed forked persistence) can produce the observed artifact — a complete 729,460-byte / 428-frame log replaced by exactly its first two complete frames (4,129 bytes):
So the responsible path is still unidentified. Concrete asks:
I can provide the surviving 4,129-byte artifact, the decoded 1,058-event log, hashes and the ISO timeline if useful. |
|
Follow-up: checked the third-party relocation bundle — it cannot rewrite or truncate a session log To close the loop from my previous comment, I extracted and audited the forked persistence layer that one of the affected profile's third-party bundles ( 1. It never mounted on 0.2.1-rc.5. The profile's plugin-manager log records 2. Relocation is metadata-only. It writes exactly those four JSON artifacts and nothing else: the commit path re-checks the log file revision and publishes 3. It refuses damaged logs on purpose. Relocation requires a current generation with 4. Truncation in that fork is unchanged from core: only the torn-tail repair ( Other writers in the affected profile (audited, host side):
Conclusion for the incident. No shipped path and no previously installed profile code replaces a complete log with a prefix of itself. The only code that emits exactly "header frame + one event batch" (the two-frame shape of the 4,129-byte survivor) is Happy to share the formatted sources and exact function-level references if useful. |
|
Patch: torn-tail repair safety (backup + refuse an invalid committed prefix) I prepared a minimal core patch for the recovery side of this report: https://gist.github.com/rusliksu/8bebaeb0023ec1529bf3d8d1cce667b1 It is based on What it changes (
This does not fix the writer bug reported in this thread ( Verification:
The gist contains the Branch: the commit is also available on a fork branch if the team wants to pull or cherry-pick it — https://github.com/rusliksu/deepseek-harness/tree/fix/torn-tail-repair-safety (compare view: master...rusliksu:fix/torn-tail-repair-safety). I did not open a pull request because CONTRIBUTING.md states that external pull requests are not accepted at the moment. |
|
Request: would a maintainer consider taking this patch? The commit It is based on If the direction is useful, I am happy to reshape the patch to fit the team's process: split it, drop the README change, rename the backup artifact, or rebase it onto a release branch. |
Uh oh!
There was an error while loading. Please reload this page.
Component:
@deepseek-ai/dsh-session-persistence-jsonl(recovery/rewrite path),@deepseek-ai/dsh-agent-loop(SystemPromptProjection),@deepseek-ai/dsh-session-format-v3-to-v4(Relationships.foldSurface)Version: DeepSeek Harness
0.2.1-rc.5desktop (Electron 44.7.0, Node 24.21.0, Windows 11 x64); session format v4, zstd multi-frame logRelated: #10619 (same write-path invariant violation on
0.2.1-rc.2— still reproduces on rc.5), #12007, #12008, #8125, #8888Summary
The write-path invariant violation from #10619 still exists on 0.2.1-rc.5: a session whose surface starts with
user/message(imported/forked history, nosystem/message) getssystem/messageappended bySystemPromptProjection.project()on the first live turn, and the log becomes unreadable.What is new here: on this build the next app start silently replaced the log with a prefix of complete zstd frames — 729,460 bytes / 1,058 events became 4,129 bytes / 34 events — and kept no backup. The rest of the history was only recoverable because a full decode had been captured before the rewrite.
Evidence
Timeline (local time, Windows):
00:21:23— log intact: 729,460 bytes, 428 complete zstd frames, 1,058 events, denseseq0..1057, every record parses, every frame decodes cleanly.~00:20— GUI reportsstored session "<id>" is corrupt: ... SessionFormatError: system/message requires a protected first surface head.00:21:52— the file is replaced by 4,129 bytes: exactly its first two complete frames (frame boundaries 0–152–4,129 preserved), decoding to the header + 34 events (seq0..33). At the same second the mtimes of ~15 other session logs changed too, i.e. this happened during an app-start pass, not from a manual edit..bakand no copy of the original bytes remained anywhere under the DSH data directory (only the session projection cache survived); 1,024 of 1,058 events were gone from the artifact.The kept prefix is exactly the complete frames preceding the frame that contains the first invalid event — some recovery/rewrite path appears to commit a validated frame prefix and never preserves the rest.
Event shape that produces the invalid log (sanitized):
The session then kept appending events for ~35 minutes (two more turns completed) — the writer path never validates this relationship, only the loader does:
Expected
.bak/copy) and the loss reported. The documented contract only allows discarding an incomplete final frame — committed complete frames must reject as corruption, not be silently dropped.system/messagehead at the first step and converts the first realsystem/messageinto a replacement over that head (renumberingseq,surfaceOp,sourceEventSeqs,messageSeqs). I verified that a log repaired this way loads and continues normally.Impact
Silent, irreversible loss of committed chat/tool history on startup for any affected session — the GUI shows only the "corrupt log" error, nothing about truncation or data loss. In this incident the session was created by the seeded/forked-history path, and 1,024 of 1,058 events were discarded without warning.
I can provide a sanitized minimal log fixture and the surviving prefix artifact if that helps.
All reactions