Repository navigation
Multi-model runtime
- Native Kimi and GLM profiles share provider routing, model budgets and scoped
credentials across the CLI and app-server clients. Unknown prices are shown
as unavailable. See multi-model configuration. - Claude Agent SDK 0.3.310 is an explicit experimental engine, with controlled
DeepCode tools, approvals, snapshots and isolated sessions. Native remains the
default. To roll back, selectengine: "native"and start a new session;
existing SDK records are retained. - Truncated responses and exhausted turn budgets report incomplete execution.
Summaries inherit the selected model and cancellation, credentials follow the
documented precedence, and user prompts can be cancelled. - File mutations recheck permissions after approval; review restore respects
file restrictions, and SDK rollback records point to the pre-edit snapshot.
Hook processes and SDK resources are cleaned up when work stops. Cancellation
during the final path check or file preparation no longer starts a later write.
Additional writable directories work with the default sandbox mode; SDK
session forks are rejected before copying incomplete history. - Real SDK executable tests run on Ubuntu and macOS against local fixtures;
Linux bwrap/netns, desktop protocol journeys and release gates also pass.
Real Kimi/GLM endpoints and long-task quality still need live acceptance. - The VSIX uses native by default and does not bundle the optional SDK
executable. Signed macOS installers and npm publication require the release
credentials described in Releasing DeepCode.
The following changes accumulated on main since v0.3.0 are also included.
⚠️ Breaking
deepcode mcp servenow applies your permission settings. It executed
Read/Write/Edit/Bash for any connected MCP peer with no mode, no permission
rules, no file contract and noPreToolUsehooks — the same shape as the
runAgentbypass fixed in #181, in a different entry point. Every call now
goes through the central gate, a call that would need approval is refused
(nobody is attached to that pipe to approve it), and a permissive
permissions.defaultModeis clamped todefaultexactly as a scheduled job's
is. A peer can now do whatpermissions.allowsays it can and nothing else,
so anyone relying on the old behaviour must add rules — or start the server
with an explicit--mode.--sandboxalso applies now; it did not before.
✨ Added
-
Grep and Glob results in the desktop are now clickable locations. Both
tools attach the places they found as structured data — resolved from their
own parsed rows, where a path containing:is still unambiguous — and the
desktop renders a search's card as a list of openable paths (with line and
matched text in content mode) instead of a grey text blob. Clicking one opens
it in the file panel. This is thelocationsrender intent the DSH round
specified and deferred: re-deriving entries by parsing the formatted result
text was the rejected design, and attaching them at the source is what makes
the entries exact. A session restored from its log has only the text the
model saw, so replayed cards degrade to today's plain-text body. -
/planenters plan mode directly (/plan offleaves it) — the same
switch/mode planalready threw, one word shorter and matching what Claude
Code users type. A trailing prompt (/plan do X) is refused with a pointer
rather than silently dropped, because half-obeying a command teaches the
wrong lesson about what it did. -
Persistent shells now last a whole CLI session, and
/shellsshows them.
The registry landed in #273 owned by a singlerunAgentcall, which meant a
shell opened in one turn was gone by the next — a slowerBashwith extra
steps. The REPL now owns one registry for the session and threads it through
every turn, socd,export, and an activated virtualenv survive from one
message to the next./shellslists what is open and where each started;/shells close <id>
closes one and whatever is still running in it. Worth having because the
change is what makes these processes outlive a turn: without a view of them,
the user has no way to see or stop something the agent left running.Everything closes when the session ends, including when it ends by throwing —
these are real processes in their own process group, so they do not die with
the CLI. Background tasks and sub-agents deliberately do not share the
session's shells: two agents interleaving commands in one shell would each be
wrong about its state. -
A shell that survives between tool calls (#273) —
ShellOpen/ShellRun/
ShellClose/ShellList. EveryBashcall is a fresh process, socd,
export, andsource venv/bin/activatewere forgotten the moment they
returned; the workaround was re-pasting the whole prefix into every command,
which still could not hold a running dev server.Not a PTY. That would mean
node-pty, a native dependency needing a build for
every platform the desktop ships to, bought for full-screen programs that are a
small share of the value. This runs over pipes with a per-session random
sentinel, and says plainly in its description thatvimandtopdo not work.
Commands run with stdin closed socatcannot swallow the sentinel, and stderr
is merged at the shell so the streams interleave in true order.Interrupting an overrunning command signals the shell's children, not its
process group — signalling the group kills a non-interactive bash, which would
lose the session every time. Lifetime is guaranteed rather than remembered: a
run that opens its own registry closes every shell before returning, including
when the loop throws. Idle shells close themselves and there is a cap on how
many can be open. -
The agent can search its own past sessions (#272) —
SessionSearchfinds
text across previous sessions andSessionReadfollows a hit into the
surrounding conversation. Every session was already on disk as JSONL and
nothing could read it back, so "how did we fix this last month" was
unanswerable while the answer sat in a file the agent wrote.Scoped to the current workspace by default, matched on a resolved path boundary
so/a/projectcannot capture/a/project-two. Neither tool takes a scope
argument: widening the search is a privacy decision, and a parameter would let
the model consent to reading another project's history on the user's behalf.
SessionReadenforces the same rule, so knowing an id is not authorisation. No
index — a scan is fast at this volume, and an index is a second copy of the
truth that can disagree with it. -
Tools declare how they should be drawn (#271). Every tool call rendered as
the same grey blob: anEditshowed its result sentence but never the change,
aBashshowed output with no sign of which command produced it. A tool now
declares a render intent beside its schema —Bashisterminal,
Edit/Write/NotebookEditarediff— and the desktop draws real coloured
+/-lines and a shell transcript. The mapping lives in core so the CLI and
the extension read one answer instead of each hardcoding tool names.Presentation is a pure function of the call's arguments, never the result or
the filesystem, so a replayed session renders as it did live. That is why
Writerenders as wholly added: its arguments genuinely do not say what the
file held before. -
A backstop deadline on every tool call (#270). Only
Bashbounded itself;
aGrepagainst a stalled mount could hang the turn forever with nothing on
screen but a blinking cursor. Deliberately generous (10 min default) so each
tool's own limit fires first and produces the more specific error — this only
covers the case where the inner limit never fires. A caller-requestedtimeout
always wins when longer. On a side-effecting tool the message says the effect is
unknown rather than implying nothing happened. -
The loop notices when the model is repeating itself (#269). The most
expensive agent failure is the loop that neither errors nor terminates: same
tool, same arguments, same answer, quietly spending the budget. At 3, 5, and 8
consecutive identical calls an escalating reminder is injected. It has no veto,
which is what makes false positives affordable. Excluded tools are transparent
to the chain rather than resets, so a bookkeeping call interleaved into a loop
cannot launder it, and calls the gate refused still count. -
thread/delete— the protocol could list, fork and archive threads but not
delete one, so the desktop deleted session files through Tauri instead. The
app-server is the single owner of thread storage; a client removing files
behind it can pull the ground out from under an open writer. Served under the
existingthreadManagementcapability, with the local writer kept as the
fallback for a sidecar too old to know the method. -
Change ledger records provenance. Each entry now carries
derivedFrom—
the files the turn read before making that change — answering the question
after "what changed" and "how do I undo it": what was it derived from. That
is what you ask when a generated file is wrong and you need to know which
input to fix. Shown bydeepcode ledger show. Built on the existing ledger
rather than a second store.It is observed, not declared: only
Readcounts (Grep/Globtake a search
root and return many paths — calling the root an input claims a derivation the
turn did not make), a failed read is not an input, and the file being written
is excluded so anEditdoes not look self-derived. Absent rather than empty
when there is nothing to say. -
Trigger sources for scheduled jobs — a job can now fire from a calendar
file or a file change, not only a clock.{ "kind": "ics", "path": "team.ics", "match": "release" }fires when a matching event starts;
{ "kind": "file", "paths": ["schema.json"] }fires when a watched path
changes.schedulestill means cron and existing jobs need no migration.
Everything is polled by the existingscheduler run, so there is no
daemon and no way for a trigger to fire while nothing is listening. See
docs/triggers.md.Standard iCalendar text is the only calendar input — no vendor SDK, no OAuth
to a calendar service. The reader handlesDTSTART, foldedSUMMARYlines and
RRULE FREQ=DAILY/WEEKLYwithINTERVAL/BYDAY/UNTIL/COUNT, and
reports anything it cannot express rather than dropping it: a silently
ignoredRRULEis a job that never fires, and that failure is
indistinguishable from "nothing was scheduled". All-day entries never fire —
they name a day, not a moment. A trigger decides when, never what may happen:
every scheduled run still goes through the unattended clamp. -
The in-app updater has a feed.
tauri.conf.jsonhas had
updater.active: trueand a committed public key since the desktop app
shipped, pointing at alatest.jsonthat nothing ever produced — so the app
polled, 404ed, and silently never updated. The release pipeline now enables
updater artifacts, signs them, writes the manifest and attaches it, all gated
on a signing key being present so a credential-less release still builds. When
the key is absent the release body says the feed is missing, because an
updater polling a 404 forever looks identical to one that has found no update.
Generating the key pair remains yours — seedocs/RELEASING.md.
🔒 Security
- A sub-agent did not inherit the file contract. The
Taskdelegation
forwarded mode, permission rules, hooks, sandbox config and auto-mode — every
gate except the contract. So "never readsecrets/**" bound the main agent
and said nothing to the sub-agent it spawned to do the reading, and since a
contractdenyis deliberately not waivable, this was the one gate that was
supposed to hold no matter what. A regression test asserts the secret never
reaches the provider. GrepandGlobreturned results the contract denies reading. Both take
a search root, so the pre-call verdict only ever covered where the search
started; a search rooted at the workspace was allowed and then handed back
matches from denied paths, with the matched line attached. Results are now
filtered through the sameevaluatePaththe gate uses — no second glob
dialect to drift — and the output ends with a count of what was withheld,
never with the paths.askis not filtered: mid-search there is nobody to
ask, and a hit is not yet a read.- The plugin capability bridge passed no contract into the tools it executed, so
a plugin'sGrepskipped the same filter. --sandbox read-onlywas not read-only on Linux.buildLinuxBwrapArgs
ended with an unconditional--bind <cwd> <cwd>, and bwrap applies binds in
order with the last one winning — so the read-only bind that the mode had
correctly asked for was overwritten a few arguments later, and a command could
write to the workspace. macOS never had this: its profile grants writes only
fromallowWrite, which read-only leaves empty. #226 introduced the mode axis
and verified it on macOS; this is the half nobody looked at. Callers using the
legacyenabled: trueshape are unaffected.
🐛 Fixed
-
A rethrown error keeps the one that caused it. Five places wrapped a
caught error in a newErrorcarrying only its.message— settings and
trust-store loading, the hook trust file, and the MCPheadersHelper— so a
parse failure or a spawn error arrived with the original stack,errnoand
pathdiscarded. They now pass{ cause }, and ESLint'spreserve-caught-error
keeps the next one from being written. -
Tool output could flood the model's context, or vanish (#268). Two defects,
one cause: nothing central bounded what a tool result put in front of the
model, so each tool improvised.WebFetchimprovised by not bounding at all — it returned the whole response
body, capped only at 5 MiB of bytes. A 5 MiB page is roughly 1.5M tokens; one
call ended the session.Bashimprovised by destroying evidence: it sliced at
30 KB and wrote the remainder nowhere, and the tail of a failing test run is
exactly the part worth reading.A spill policy now runs on every tool result. Anything at or under the
threshold passes through untouched; anything over becomes a head-and-tail
preview naming the file holding the full text, saved beside that session's
snapshots and retrievable withRead. Both ends, weighted toward the tail,
because stack traces and exit codes live at the end. A host with no filesystem
still gets the bound and is told the output was not saved rather than shown a
path that does not exist. -
Release notes come from the CHANGELOG.
gen-release-notes.tswalked the
commit range, and with no preceding tag it fell back to the root commit — which
is how v0.3.0's release page came to say "0 commits." after #250 fixed the
shallow clone. It now takes the tagged version's CHANGELOG entry, which is
written for humans and groups changes by what they mean rather than by the verb
the commit happened to start with. Repo-relative links are rewritten to
absolute URLs pinned at the tag, since a release body does not render inside
the repository. Falling back to commits still works and says so in the body. -
The desktop sidebar was a second reader of the session directory. Archive
and delete went through Tauri while the protocol served the same threads, and
the list did too —window.deepcode.sessions.list()had preferred the
protocol since #231, butSidebar.tsxbypassed the shim and called
listSessions()directly. All three now go through the owner, and deleting a
thread removes both its protocol snapshot and its canonical session
projection:listreads both, so removing one left the row reappearing on the
next refresh as an empty session that could not be opened. -
deleteSessionrefuses a session id that is not a single path segment, before
removing anything. It ends in a recursive delete of<root>/<id>, and..—
the id that resolves to the directory above the sessions root — is spelled
entirely in characters an id may legitimately contain, so the character-class
check both it and the thread store relied on admitted it. Every in-tree caller
validates first; a delete this destructive should not depend on that. -
Grepover a single file no longer prefixes every line with a colon.
ripgrep omits the filename when the search path is one file — there is
nothing to disambiguate — so its--nulloutput carries no NUL, and rejoining
the record aspath:textwith an absent path emitted:1:hit. Parsing now
distinguishes "rg printed no path" from "rg printed an empty field", and the
separator is written back only where rg wrote one. Such a row is attributed to
the search root for contract filtering, so the result filter does not depend on
the pre-call gate having adjudicated that call correctly. -
CI installs ripgrep and sets
DC_REQUIRE_RIPGREP=1. TheGrepsuite
self-skips whenrgis absent, so it may never have run in CI — and it now
covers ripgrep's--nulloutput format, which the tool parses byte for byte. -
The test suite could re-initialise your own repository.
gitreads
GIT_DIRfrom the environment and a git hook sets it, so a fixture calling
git initon a temp directory from inside the pre-commit gate did not
initialise the temp directory — it re-initialised the developer's checkout as
bare and wrote the test identity into its config, after which every git
command there failed with "this operation must be run in a work tree".
apps/server/src/workspace-diff.test.tswas the fixture; the code it tests
scrubs the environment, the fixture did not. Three other fixtures had each
grown their own copy of the scrub, two carrying a comment describing this
precise failure — a convention passed by word of mouth that had stopped being
enforced. They now sharegitSpawnEnv, and a check fails the build if a test
spawnsgitwithout it. -
The CLI is published as
@oratis/deepcode. 0.3.0 renamed it away from
deepcode-clibecause that name belongs to an unrelated project — but
@deepcode/cliwas not ours either. The leaf name is unpublished, which is
what made it look free; the@deepcodescope holds@deepcode/tscand
@deepcode/dcignore, and npm rejects a publish into a scope you do not own. A
scope is not claimable by publishing into it, sopnpm publishwould have
returned the same 403 the rename was meant to fix. The new name is the
repository owner's personal scope, which needs no organisation to exist first.
Nothing was ever published under either old name, so no installed package
changes. The binary is stilldeepcode. -
A test now asserts that every
npm i -g …in a current document or in CLI
source names the packageapps/cli/package.jsonpublishes. Both renames so far
moved some install strings and left others behind. It scans the whole
repository minus an explicit list of historical snapshots, rather than an
allowlist of the documents somebody thought of — an allowlist has to be
extended by whoever adds the next document, and stays silent when they forget,
which is the same shape as the bug it is there to catch. -
apps/cli/README.md— the npm landing page — still described the CLI as an
"M0 骨架,命令入口存在但不能用" and pointed at milestone numbers for when
features would arrive. It shipped in the packagefileslist.
📄 Documentation
- DeepSeek Harness research and adoption plan (#267) —
docs/research/deepseek-harness.mdgrades
every claim A/B/C (source read / their docs say so / hearsay, inadmissible),
anddocs/DSH_ADOPTION_PLAN.mdargues each
candidate proponent / opponent / verdict so the implementation PRs execute a
decision rather than reopen one. dsh is the closest comparison DeepCode has:
another DeepSeek-powered coding agent under the same constraints.
🚫 Deliberately not adopted
A Cordis-style "everything is a plugin" rewrite: dsh spends 219 packages
expressing what DeepCode expresses in 4, and that ratio is what shipping a
third-party plugin ecosystem costs — we do not ship one, so we would pay the cost
and collect none of the benefit. It also self-describes as a developer preview
that will break compatibility, while 0.3.0 is out with npm, VSIX, DMG and an
update feed downstream. Take the discipline, not the framework.
Also rejected: its workflow engine, which introduces model-written code executed
behind a boundary its own docs say is not a security boundary, with no use case
our sub-agents fail to cover. Deferred pending a product decision: the goal
domain and the Ralph loop, which change when an agent stops rather than what it
can do. Reasoning in the adoption plan §2.
No macOS build in this release. Apple signing credentials were not configured when it was cut; see
docs/RELEASING.md.
Not published to npm.
NPM_TOKENwas not configured when this release was cut; install from source or the VSIX.