feat(dsh): add DeepSeek Harness (dsh) agent support - #1682
Conversation
Bridges cc-connect to the DeepSeek Harness CLI (dsh) in headless profile: every user message spawns `dsh --profile headless` as a one-shot run that resumes the same persisted dsh session (context carries across turns), then prints the final assistant text back to the chat. The installed dsh must provide the --session-id / --model / --mode headless runner flags (companion change to the harness repo). Implements: - core.Agent / core.AgentSession (one-shot headless per Send) - core.ModelSwitcher (/model): reads dsh settings.yaml model catalog, passes --model to the next run - core.ModeSwitcher (/mode): read-only | workspace-write | danger-full-access | confirm, pinned via --mode - WorkspaceAgentOptionSnapshotter, MemoryFileProvider, SkillProvider, AgentDoctorInfo, session listing - plugin_agent_dsh.go (build tag !no_dsh), Makefile ALL_AGENTS, config example
- Send now drives dsh headless in --jsonl mode: text/thinking deltas, tool calls and results stream to the chat, and approval requests (confirm mode write/execute interception + workspace-write sandbox escalations) render as permission cards answered via the runner's stdin. - RespondPermission writes the human's decision back to the dsh process. - /session shows dsh's real session titles (last session/title event, fallback first user message) parsed from the zstd-compressed session logs (new dep: klauspost/compress).
- errcheck: check s.Close() in tests - unused: drop the unused forceKillCmd helpers and the resultSent struct field (Send uses a local flag)
cmd.Wait() closes the stdout pipe; waiting on it before the reader goroutine finishes could discard the JSONL lines still buffered in the pipe (tool calls, result/done envelopes), silently dropping events under race-detector scheduling. The reader now drains to EOF first (the process closing its write end), then Wait reaps the child.
|
CI is fully green ✅ (lint, unit-test with One extra fix landed during review: Local verification summary:
|
After the turn's done envelope, the dsh process waits for the driver to close stdin before its approval readline sees EOF and the one-shot exits. Send held stdin open until cmd.Wait returned, so a completed turn left the session busy forever (second messages were queued until /stop killed the process). Close stdin right after the reader drains stdout, before Wait.
|
One more fix landed after real-world testing: close stdin before Symptom: after the first question, the AI finished and replied, but a second message was queued ("will be processed after the current task completes") until Fix (with the companion dsh-side change): the reader drains stdout to EOF, then cc-connect closes stdin before |
Summary
Adds a first-class DeepSeek Harness (dsh) agent to cc-connect. Users can chat with dsh through Feishu/Telegram/... with full multi-turn context, live tool-call visibility, and interactive permission approval cards.
How it works
dsh --profile headlessas a one-shot run that resumes the same persisted dsh session (--session-id, create-or-resume), so conversation context carries across turns.--model/--modeare passed on every run, making/modeland/modework like other agents:/modelreads dsh's settings.yaml model catalog (fallback: deepseek-v4-flash / deepseek-v4-pro)/modeexposesread-only/workspace-write/danger-full-access/confirm--jsonlmode (the dsh headless driver protocol this feature builds on):RespondPermission/sessionshows dsh's real session titles (lastsession/titleevent, fallback first user message), parsed from the zstd-compressed session logs (new dep:klauspost/compress).Companion change (not in this PR)
The dsh side needs
--session-id/--model/--mode/--jsonlheadless-runner flags. Those live indeepseek-ai/deepseek-harness, which currently does not accept external pull requests (per its CONTRIBUTING.md). The headless runner changes are small and self-contained (startup flag parsing + runner streaming/approval wiring + confirm-mode ask interception + regression tests) — the patch is documented in the config example.Testing
go test -tags no_web ./...— all packages pass (LANG=C; one pre-existing locale-dependent core test fails under zh_CN only, unrelated to this change)go test ./core/ -run TestCUJ— passes--jsonlevent mapping (text/thinking/tool call/result), approval request relay + stdin response (allow & reject), session-title parsing from zstd logs, mode/model switchingError: the user rejected tool "write"and the file is not createdNotes
dshis additive.Msg*keys).