mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 14:39:42 +02:00
feat(deepseek): read dsh session transcripts for last-response
`GET /api/sessions/:id/last-response` is how an agent (and the Response Viewer) reads what a worker said. DeepSeek was falling through to the pane segmenter with the other external CLIs, which for this mode is not merely coarse but wrong: dsh-TUI paints a full-screen splash, so a `last-response` call on a fresh dsh session answered with its ASCII-art logo -- and anything polling for a worker's first reply reads that as a reply. dsh does not belong in that group. It writes a structured JSONL transcript per session, so read it. Four things in that file shaped the reader, all measured against real transcripts on disk: 1. dsh appends ONE ZSTD FRAME PER WRITE, and Node's zlib zstd decoder (one-shot and streaming alike) stops at the first frame end: a real 56-line transcript decoded as 1 line / 158 bytes -- the session header alone, i.e. a silent truncation that reads as "nothing said yet" forever. `zstdFrameRanges()` walks frame and block headers to find exact boundaries; splitting on the 4-byte magic would corrupt everything after a magic sequence occurring inside compressed data. zstd is resolved at RUNTIME because it landed in Node 22.15 while the project floor is 22.0, so an older Node keeps the pane behaviour. 2. Every turn also records a plugin-sourced `user/message` (the runtime context snapshot), which must not render as the user's own words. 3. A turn that ends in an error carries the provider's message; it is surfaced as `Turn error: …` (and a non-error early stop as `Turn ended: …`) rather than as an empty string, which an agent reads as "still thinking" through fifteen polls. 4. Reply text is assembled per (turn, step): a finalized message wins and the streamed deltas fill in only for a step that never finalized, so a partial answer is readable mid-turn and never doubled. "Finalized" is tracked as a set of steps rather than as non-empty text, because a step whose whole reply was reasoning strips to '' at the `</think>` boundary and would otherwise resurrect the raw deltas in its place. Session-to-transcript pairing is by the transcript's own header `cwd` plus a boot window against the session's createdAt, never by reproducing dsh's directory mangling (already two forms on disk) and never by newest-mtime alone -- mtime alone handed a freshly spawned worker its predecessor's answer in the same case directory. An empty result still wins over the pane; only a Node that cannot decode zstd falls back to it.
This commit is contained in:
@@ -141,6 +141,7 @@ import {
|
||||
isExternalCliTranscriptMode,
|
||||
parseExternalCliTranscript,
|
||||
} from '../response-viewer-transcript.js';
|
||||
import { readDeepSeekLastResponse } from '../../deepseek-transcript.js';
|
||||
|
||||
// Path to linked-cases registry (same file used by case-routes resolveCasePath)
|
||||
const LINKED_CASES_FILE = dataPath('linked-cases.json');
|
||||
@@ -2042,6 +2043,28 @@ export function registerSessionRoutes(
|
||||
return await readCodexLastResponse(session, codexQuery.context === 'full');
|
||||
}
|
||||
|
||||
// DeepSeek Harness writes a real structured transcript under
|
||||
// `$DSH_HOME/sessions/**`, so read that rather than segmenting the pane.
|
||||
// ⚠️ For dsh the pane fallback is not merely coarse, it is WRONG: dsh-TUI
|
||||
// paints a full-screen splash, and the segmenter served its ASCII-art logo
|
||||
// back as the worker's answer (measured), which an agent polling for a
|
||||
// reply reads as a reply. So an EMPTY transcript result still wins over the
|
||||
// pane — "nothing said yet" is the honest answer. Only `null`, meaning a
|
||||
// Node too old to decode zstd, falls through to the segmenter below.
|
||||
if (session.mode === 'deepseek') {
|
||||
const deepSeekQuery = req.query as { context?: string };
|
||||
const full = deepSeekQuery.context === 'full';
|
||||
const transcript = await readDeepSeekLastResponse(session, { blocks: full });
|
||||
if (transcript) {
|
||||
return {
|
||||
text: transcript.text,
|
||||
timestamp: transcript.timestamp,
|
||||
hasContext: transcript.text.length > 0 || transcript.blocks.length > 0,
|
||||
messages: full ? transcript.blocks : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// OpenCode / Gemini / Antigravity / Pi render their own TUIs and write no
|
||||
// Claude transcript, so the scan below finds nothing and the response viewer
|
||||
// renders permanently empty for them. Segment the terminal buffer instead —
|
||||
|
||||
Reference in New Issue
Block a user