I read thirteen articles by the same author this week. Seven of them were grounded in something I could check. Six were not. Same byline, same confident tone, same clean formatting, and no way to tell which was which from the first paragraph.
The author is JIN, who runs a publication called JIN System Architect on Medium, and lately has been writing a running series about the DeepSeek Harness, usually shortened to DSH. I triage a stack of Medium articles every week as part of my own reading routine, and JIN keeps landing in the same awkward spot: roughly half of what gets published is useful engineering writing, and the other half is thought leadership wearing an analysis costume. Because of that, I stopped judging the byline a long time ago. I judge each article on its own.
This post walks through the two installments I think are strongest, because they have something real underneath them: one about how Cordis handles plugin lifecycles, and one arguing that DSH’s real contribution is deciding what happened to the agent. Along the way it uses the contrast with JIN’s weaker pieces to talk about how to read AI engineering content at all. It ends with a rule for when any of this machinery is worth its cost, and the honest answer is “less often than the articles suggest.”
How I read a series like this
Before the technical parts, let me be upfront about the reading method, because it shapes every claim below.
A running series has a specific trap. By installment four, you have absorbed the author’s vocabulary, and the vocabulary starts to feel like evidence. “Ownership,” “disposal order,” “the agent’s world state.” These are good words, and they are also free to type.
So my rule is boring. For every installment, I look for the things that cost the author something to include. A version tag costs something, because it can be wrong and anyone can check it. A named tool costs something. A specific failure scenario costs something, because a reader who knows the domain can tell immediately whether it could happen. What is cheap is a claim like “leading teams are adopting this” or a sentence that says the author used “GPT search to collect facts” without telling you which facts, from where, or at what version.
That last one is not hypothetical. In JIN’s earlier piece, “From Codex Harness to DSH Subagents,” the only concrete specific I could find was a mention of DeepSeek Harness v0.1.0-rc.8, as the version where Codex and Claude Code can appear as DSH subagents. No commit, no pull request, no repository link to check it against. The article also says the author used GPT search to collect facts. I am not accusing anyone of anything. Using a search tool for research is normal. But a disclosure like that, with no specifics attached, tells me the article is an opinion built on a summary, and I should weigh it as an opinion.
Keep that in mind, because the two installments I want to talk about are different in kind.
Installment one: Cordis and the part everyone ignores
The first piece is titled “Cordis Solves the Part of Plugin Systems Everyone Ignores: What Happens After a Plugin Leaves?” It was the one that got clapped most this week, and I understand why. The question in the title is the right question, and almost nobody asks it.
Most plugin system tutorials are entirely about arrival. How do you register a plugin, how do you pass it config, how do you let it hook into events. Nobody spends a chapter on departure. But in any process that runs for days, departure is where the bugs live. A plugin that registered a timer, opened a socket, subscribed to an event bus, and claimed a name in a shared registry does not clean all that up by itself just because you removed its entry from a config file.
JIN’s article compares two answers to that problem.
The Pi answer: the extension cleans up after itself
Pi, the minimal coding agent, takes what I would call the small-core position. The core stays tiny, and extensions are trusted to manage their own resources. Pi’s extension documentation is explicit about the contract. Extension factories may run in invocations that never start a session, so the docs tell you not to start background resources such as processes, sockets, file watchers, or timers from the factory. You start them in a session_start handler and you tear them down in a session_shutdown handler. On reload, Pi emits session_shutdown for the current extension instance, then reloads, then emits session_start again with a reason of reload.
I like this design more than I expected to. It is simple, it is legible, and it puts responsibility where the knowledge is. The extension author knows what they opened, so the extension author closes it. There is no framework magic to misunderstand.
The cost is that correctness depends on every extension author being disciplined every time. Pi’s docs even suggest treating reload as terminal for the handler, returning right after await ctx.reload(), because stale in-memory state is a real hazard. That is a sensible guardrail, but it is a guardrail written in prose. Nothing stops an extension from leaking a timer.
The Cordis answer: make the relationships into runtime state
Cordis, the plugin meta-framework that DSH is built on, takes the opposite position. Instead of trusting each plugin to remember what it registered, the framework tracks it. In the Cordis tutorial that ships with the DSH docs, the vocabulary is Context, effect, service, inject, and Loader. A Context is the shared container where plugins mount. A service is a capability exposed on that context that other plugins can depend on, and inject is how a plugin declares the dependency. The Loader reads a cordis.yml and mounts the configured plugins. Cordis-managed registrations, which are the effects, are undone automatically when their plugin unloads.
JIN’s argument, as I read it, is that this changes what kind of thing a plugin relationship is. In the Pi model, “who owns this timer” is a fact in the extension author’s head. In the Cordis model, ownership, dependency, and disposal order become explicit state that the runtime can inspect, and can act on. If plugin B injects a service from plugin A, the runtime knows that, and it knows that when A leaves, B has lost something it needs. Teardown stops being a convention and becomes an operation.
Here is a toy version of the idea. This is not the Cordis API. It is a deliberately small sketch to show what “ownership as runtime state” means, and it runs with plain Node and TypeScript, no packages, no API keys.
// scope.ts - a toy model of ownership as runtime state
type Disposer = () => void | Promise<void>;
export class Scope {
private disposers: Disposer[] = [];
private children: Scope[] = [];
private closed = false;
constructor(public readonly name: string, parent?: Scope) {
parent?.children.push(this);
}
// every registration goes through here, so ownership is recorded
effect(undo: Disposer): void {
if (this.closed) throw new Error(`scope ${this.name} is closed`);
this.disposers.push(undo);
}
fork(name: string): Scope {
return new Scope(name, this);
}
async dispose(): Promise<void> {
if (this.closed) return;
this.closed = true;
// dependents (created later) go first, then our own effects in reverse
for (const child of [...this.children].reverse()) await child.dispose();
for (const undo of [...this.disposers].reverse()) {
try {
await undo();
} catch (e) {
console.error(`[${this.name}] dispose failed`, e);
}
}
}
}
And a usage example you can paste into the same folder. Put {"type":"module"} in a package.json there (for top-level await) and run it with npx tsx demo.ts:
// demo.ts
import { Scope } from "./scope";
const root = new Scope("root");
const db = root.fork("db");
const heartbeat = setInterval(() => {}, 1000);
db.effect(() => {
clearInterval(heartbeat);
console.log("db: heartbeat stopped");
});
const search = root.fork("search"); // conceptually depends on db
search.effect(() => console.log("search: index closed"));
await root.dispose();
// prints "search: index closed" first, then "db: heartbeat stopped"
The output order is the whole point. The thing that depends on the database goes down before the database does. In the Pi model you would encode that order by hand in each session_shutdown handler, and hope the order stays right when someone adds a third extension. In the model above, the order falls out of how the scopes were created.
What I trust and what I could not confirm
This is the part where I try to follow my own rule.
The Cordis concepts I just described are checkable, and I checked them against the DSH documentation and Pi’s extension docs. Those are real, and the contrast between “extension cleans up its own resources” and “the runtime tracks and unwinds registrations” is a fair and useful one.
JIN’s article also cites a specific commit hash and a release tag, dsh@0.1.0-rc.5. I want to be careful here. While writing this, I looked at the DSH releases page and tried the obvious tag URL variants, and I could not confirm that exact tag string myself. The releases page I could read shows the recent tags, which run well past rc.5, and the one URL I guessed returned a 404. That does not mean the tag does not exist. Tag naming conventions in monorepos vary, and older tags may simply be beyond what I could page through. It means I cannot vouch for it, so I am not going to repeat a hash I could not check. If you are going to lean on that reference, click through to the repository yourself.
I think that is worth saying out loud, because it is the exact behavior I wish more writers had. The gap between “the article says it is verifiable” and “I verified it” is where a lot of AI engineering content quietly lives.
Installment two: what happened to the agent?
The second piece, “The DeepSeek Harness Teardown,” is my favorite of the pair, and not because of the code. It is because of the scenario.
The argument goes like this. Everyone talks about agent harnesses as if the hard part were tool calling: getting the model to emit a structured call, running it, feeding back the result. That part is close to solved. It is a loop with a schema. The hard part, JIN argues, is a different question entirely: when things go wrong in the middle, what actually happened to the agent?
The scenario makes it concrete. Picture an agent that has just issued a tool call, say a write to a file. The call is in flight. At that moment, new user input arrives. Now the harness has a decision to make, and every option has a cost. It can cancel the in-flight call, which might leave a half-written file. It can let the call finish and queue the new input, which makes the user wait. It can interleave them, which risks the model reasoning over a world that changed under it.
Now add the second half. The tool call finishes, the file is written, and the process crashes before the write result reaches the session log. On restart, the log says the agent asked for a write and never heard back. The file on disk says the write happened. The agent’s memory, which is the log, and the world, which is the disk, no longer agree. If the harness naively replays from the log, the agent will either repeat the write or reason about a file it believes is unchanged.
That is what I mean by “deciding what happened to the agent.” It is not tool calling. It is being the authority on the agent’s history, including the parts where history is ambiguous.
I find this convincing precisely because it is a failure I can picture without any help from the author. Anyone who has built a job queue has met the same shape: an intent is recorded, an effect happens, the acknowledgment is lost. The scenario is specific, it is falsifiable, and it points at a design question you can go and inspect in a codebase.
Here is a toy journal that handles the intent-and-result split. Again, this is my sketch of the general technique, not DSH’s implementation. It writes an intent entry before the tool runs and a result entry after, so that recovery can find calls that started and never finished.
// journal.ts - write intent before the effect, result after
import { appendFileSync, existsSync, readFileSync } from "node:fs";
type Entry =
| { t: "intent"; id: string; tool: string; args: unknown }
| { t: "result"; id: string; ok: boolean; output?: string };
const LOG = "session.jsonl";
export const record = (e: Entry) =>
appendFileSync(LOG, JSON.stringify(e) + "\n");
// calls that started and never got a result: the ambiguous ones
export function unresolved() {
if (!existsSync(LOG)) return [];
const entries: Entry[] = [];
for (const line of readFileSync(LOG, "utf8").split("\n")) {
if (!line) continue;
try {
entries.push(JSON.parse(line));
} catch {
// a torn final line from a crash mid-write; ignore it
}
}
const done = new Set(
entries.filter((e) => e.t === "result").map((e) => e.id)
);
return entries.filter(
(e): e is Extract<Entry, { t: "intent" }> =>
e.t === "intent" && !done.has(e.id)
);
}
Notice what this does not do. It does not tell you whether the unresolved write actually happened. It tells you there is an unresolved question, which is the honest state of the world. What you do about it, whether that is checking the file, asking the user, or marking the call as unknown to the model, is a policy decision. A harness that makes that decision explicit is doing something a plain tool-calling loop does not.
The version tag, and what it does and does not prove
The teardown piece points at a verifiable version tag, dsh-v0.1.2-alpha.2, and this one I could check. It exists on the DeepSeek Harness repository as a release dated August 30. The release notes cover things like connection failure visibility with retry, plugin organization by conversation scope and global scope, faster handling of long conversation histories, and a restored SessionEvent.ignorable API element. Seven contributors are credited.
Here is the careful reading, and I think it matters. That tag proves the version exists. It does not prove the race scenario. The release notes do not mention in-flight tool calls, crash recovery, or write results at all. So the scenario in the article is JIN’s reasoning about a class of failure, illustrated with a hypothetical, and anchored to a real version so you know which code the reasoning is about. That is a legitimate way to write. It just is not the same as “this bug was found and fixed here,” and a hurried reader could easily merge the two.
There is a real, documented DSH failure nearby that I found while checking. A community discussion, number 4048 on the repository, describes a failed tool call leading to a deadlocked session. In that report, a read_image result with an image format the model provider rejected was turned into a hard abort of the turn. Each attempt to continue replayed the same poisoned context, so the session could never recover. The affected version was v0.1.1-rc.2, and a maintainer decomposed it into three layers, including converting provider errors into failed tool results the model can react to, and evicting poisoned context on resume.
That is a different bug from the one in the teardown piece. I am not claiming it is the same. But it rhymes with it strongly: both are about the session log becoming a source of truth that is wrong or stuck, and both are about the harness, not the model, being the thing that fails. It is the kind of evidence that makes the abstract argument feel less abstract.
What separates the trustworthy pair from the rest
Here is the comparison I ended up writing down for myself, in the plain text table format so you can paste it anywhere.
+---------------------------+--------------------------+--------------------------+
| Signal | Cordis + Teardown pair | Weaker pieces (JIN or |
| | | anyone else) |
+---------------------------+--------------------------+--------------------------+
| Version tags | Named, checkable | "Recent releases", |
| | (dsh-v0.1.2-alpha.2) | "the latest version" |
+---------------------------+--------------------------+--------------------------+
| Named real tools | Pi, Cordis, DSH, with | "Leading frameworks", |
| | docs you can open | "several teams" |
+---------------------------+--------------------------+--------------------------+
| Failure scenario | Specific: call in | Invented company, no |
| | flight, input arrives, | name, dramatic numbers |
| | crash before log write | |
+---------------------------+--------------------------+--------------------------+
| Research disclosure | Claims point at things | "I use GPT search to |
| | you can click | collect facts", nothing |
| | | attached |
+---------------------------+--------------------------+--------------------------+
| Numbers | Few, and tied to a source| Round, tidy, unsourced, |
| | | sometimes hypothetical |
+---------------------------+--------------------------+--------------------------+
| What you can do next | Open a repo, read a doc, | Nod, share, move on |
| | run a toy | |
+---------------------------+--------------------------+--------------------------+
The last row is the one I trust most. A good technical article leaves you with something to do. The weaker ones leave you with a feeling.
A caution about numbers, since this came up again this week. Several benchmark and savings figures I saw were explicitly hypothetical or illustrative in their own source articles, including a monthly savings figure attributed to a company I could not verify. If you cite something like that, say it is illustrative. I have seen “a team might save X” turn into “teams save X” within two retellings.
There is also a fairness point that cuts against my own cynicism. Invented scenarios are not automatically bad, and every example in this article’s toy code is invented. The difference is labeling. A hypothetical that says it is hypothetical is a teaching tool. A hypothetical dressed as an incident report is a small lie. When I say the race scenario in the teardown is JIN’s illustration, I am not marking it down. I am filing it in the right drawer.
The lesson: when is lifecycle management worth it?
Now back to the technical question, because I promised a rule.
Cordis-style lifecycle management is impressive engineering. Tracking ownership, dependencies, and disposal order as runtime state means you can hot swap a model adapter, unload a tool plugin, or reload a workflow engine without leaving dangling state behind. But that capability has a price. You take on a dependency graph, a vocabulary, a framework contract that every plugin must follow, and in the case of DSH, a vendored and patched fork of a pre-1.0 framework. Every one of those is something a team has to learn, debug, and keep current.
So when does it pay for itself? My answer, after reading the series and sitting with it, is that you need all three of the following, not one or two.
First, the process has to be truly long-lived. If the process lives for minutes, or for one request, or for one CLI invocation, then whatever leaks is reclaimed when it exits. The operating system is a very good lifecycle manager for short-lived processes.
Second, its topology has to change at runtime. If the set of plugins is fixed at startup, then teardown only ever happens once, at shutdown, and shutdown can be a plain function that closes things in a known order. You only need a runtime that understands departure if things actually depart while the process keeps running.
Third, the components have to have real interdependencies. If your plugins are independent, Pi’s model is enough: each one cleans up after itself, and nothing else cares. Disposal order only matters when one component’s exit changes what another component is allowed to assume.
Here is that as a table you can copy.
+--------------------+-------------+--------------------+-------------------------+
| Long-lived process | Topology | Real interdepend- | What I would reach for |
| | changes at | encies between | |
| | runtime | components | |
+--------------------+-------------+--------------------+-------------------------+
| No | - | - | Restart the process |
+--------------------+-------------+--------------------+-------------------------+
| Yes | No | - | One shutdown function |
+--------------------+-------------+--------------------+-------------------------+
| Yes | Yes | No | Per-extension cleanup |
| | | | hooks (the Pi model) |
+--------------------+-------------+--------------------+-------------------------+
| Yes | Yes | Yes | Explicit ownership and |
| | | | disposal order (Cordis) |
+--------------------+-------------+--------------------+-------------------------+
Only the last row justifies the framework. Everywhere else, a restart beats a dependency graph. A restart is boring, it is testable, and it returns you to a known state, which is worth more than most people admit. I would add that a restart is also the honest answer to a lot of agent runtime problems: when the state is ambiguous, starting clean from a trustworthy log is often safer than trying to repair a live process in place.
The teardown installment adds a fourth condition that I think is easy to miss. If your long-lived process also acts on the outside world, meaning it writes files, sends messages, or calls other systems, then you need something like the journal above regardless of how you manage plugins. Lifecycle management is about your own process’s resources. The intent-and-result log is about the agent’s relationship with a world you do not control. They are separate problems, and the series is at its best when it keeps them separate.
What I would take away
If you are building an agent runtime, ask three questions before adopting anything that looks like Cordis. Will this process live long enough for leaks to matter? Will the set of components change while it runs? Do the components actually depend on each other? If the answer to any of them is no, write the cleanup hooks by hand and be done.
If you are reading about agent runtimes, ask what it cost the author to include each claim. Prefer the article that hands you a tag, a doc, a scenario you can poke at, and be skeptical of the ones that say they used search to collect facts and then show you none.
And if you follow a series by someone whose reliability is mixed, judge the installment, not the byline. The checkable parts are the parts worth keeping.
Sources
- JIN, “Cordis Solves the Part of Plugin Systems Everyone Ignores: What Happens After a Plugin Leaves?”, JIN System Architect on Medium
- JIN, “The DeepSeek Harness Teardown”, JIN System Architect on Medium
- JIN, “From Codex Harness to DSH Subagents: Are Agents Entering a New Layer of Software Architecture?”, JIN System Architect on Medium, August 2026
- DeepSeek Harness release dsh-v0.1.2-alpha.2, github.com/deepseek-ai/deepseek-harness
- DeepSeek Harness discussion #4048, “Bug: Failed tool call causing session dead lock”
- DeepSeek Harness Cordis tutorial, deepseek-harness.github.io
- Pi coding agent extension documentation, pi.dev
Tags: ai-agents, agent-harness, deepseek, plugin-architecture, software-architecture, llm-engineering, developer-tools
Top comments (0)