Skip to content

cordis-plugin-content-exists-risk

Verified

@argszero/cordis-plugin-content-exists-risk Β· v0.1.0 Β· MIT

Content-moderation rescue for dsh: a provider's content moderation (DeepSeek HTTP 400 'Content Exists Risk' / INVALID_REQUEST) rejects an entire request, and because the loop re-derives the whole conversation from the session log on every turn, one stored

Install

dsh plugin add @argszero/cordis-plugin-content-exists-risk

Confirm the layer applied with dsh --profile default --dump-config β€” see the install guide.

Source

Tags

Readme

@argszero/cordis-plugin-content-exists-risk

A rescue for a dsh session the provider refuses to receive.

DeepSeek's content moderation can reject an entire request:

HTTP 400  {"message":"Content Exists Risk","code":"INVALID_REQUEST"}

The rejection is a property of content, and the content that trips it is already durable. Because dsh re-derives the whole conversation from the session log on every turn (GenerateOptions.messages β€” "exactly as the provider sees them"), one stored record that trips moderation makes every later turn of that session fail, whatever you now type. Reported cases: a long session whose history holds symbols fetched from a web page β€” a two-character new message failed identically (#7181); a brand-new session that failed on its first turn (#6495); a session whose failure is inherited by every session that reads it (#6476).

The log's content is not editable, and the built-in retry policy is right not to retry a deterministic 400. So the only lever left is what is sent, not what is stored β€” and that is what this plugin moves.

What it does

Mounted on the public llm/stream waterfall, it inserts a space between every pair of adjacent characters when neither of them is a space. Moderation matches character sequences; a spaced-out sequence is not that sequence.

FLAG-TOKEN-1234   ->   F L A G - T O K E N - 1 2 3 4

Three properties make that usable rather than reckless:

A healthy session pays nothing The rewrite is reactive. An untouched request takes the identity path β€” next() with your own object β€” so a session that never trips moderation pays no tokens, no cache invalidation, and no re-dispatch.
Cost is spent newest-first, only as far as needed The scope is a ladder (ladder, default [4, 32, 0]): a rung's rejection is the evidence it was not enough, so the next rung rewrites more. Nothing is paid for messages the retry proved irrelevant. Spacing roughly doubles the characters it touches, so scope is the only real currency.
A refused attempt is never shown A rejection arrives before the provider streams anything, so the terminal frame of a refused attempt is held back while another rung is available, and only the rung that was actually accepted is streamed. When every rung is refused, the provider's own failure is surfaced unchanged β€” this plugin never turns a failure into a success it did not observe.

Once a rung is accepted, the session is latched to it (in memory, for the life of the process), so later requests skip the untouched probe and start at the rung already known to work.

Install

npm install @argszero/cordis-plugin-content-exists-risk

Then mount it, by adding to your profile's config:

- insert:
    - id: content-exists-risk
      name: '@argszero/cordis-plugin-content-exists-risk'

The package ships a cordis.patch.yml (dsh.bundle.patch), so a bundle-aware profile picks it up without any further wiring. Mounting needs no configuration.

Configuration

key default meaning
mode auto auto probes untouched and rewrites only after a rejection; always rewrites from the first dispatch; observe reports what it would rewrite and dispatches unchanged; off leaves every request alone.
ladder [4, 32, 0] Rungs, in newest-message counts. 0 means every message and may only be the last rung. Must escalate.
roles ['user'] Roles whose content may be rewritten. user also carries tool results, which is where fetched content lands.
includeAuxiliary false Whether purpose-carrying calls (compaction, session title) are rewritten. Their output is folded back into the session, so a spaced request yields a spaced summary.
maxChars 200000 Characters added per request before a pass stops. 0 disables the budget.
matchPattern content[_ ]?exists[_ ]?risk Regex (case-insensitive) matched against the provider's failure message.
matchCodes [] Extra failure codes treated as a content rejection. Empty by default β€” the generic 400 code INVALID_REQUEST also covers malformed requests and oversized bodies, so it is not a moderation signal on its own. Add it for a gateway that mangles the message.
statuses [400] HTTP statuses a rejection must carry (a failure reporting no status passes, since a gateway may not forward one).
warnLimit 5 Bound on emitted warnings per mount. 0 silences them.

A session that is already dead when you mount this plugin is rescued on its next turn: the first request pays one refused probe, the second pays the rung that works. Set mode: always if you would rather skip the probe and rewrite from the start.

What it does not fix

  • A trigger in assistant or system content β€” out of default scope. Add assistant to roles. Model output is not where fetched content enters, and spacing it costs the most tokens while inviting the model to imitate spaced-out text.
  • A trigger inside tool-call arguments β€” never rewritten. arguments is a raw JSON string; a character-level transform would corrupt the call's syntax, and a JSON-aware one is a different feature with different hazards.
  • A trigger inside a tool schema (options.tools) or a one-shot system field β€” never rewritten. A schema's string leaves include enum, const and pattern, where inserting a space changes the schema's meaning.
  • Persistence β€” the latch is an in-process, bounded map keyed by sessionId. A restarted daemon pays one extra probe per poisoned session; nothing is written to disk or to the session log.
  • Fidelity β€” the rewritten text is what the model reads and answers. It is legible, but it is not the original, and a rung that fires invalidates the provider's prefix cache from the first rewritten message onward. Both are real costs, paid only by a session whose alternative is failing every turn.

How it hooks in

llm/stream is a waterfall, but the last argument is the innermost next and cordis closes over the listener's own argument list β€” next takes no parameters, so a listener cannot substitute the options object for the rest of the chain. A loop-built request additionally arrives deep-frozen (its content is "a pure function of the session log"), so mutation throws outright. A fresh dispatch through ctx.llm.stream() is therefore the only way to change what the adapter receives. The plugin registers with prepend: true, so every other listener sees exactly one dispatch per proposed request β€” the one that is sent β€” and keeps a WeakSet of its own replacements so the re-dispatch terminates.

The consequence is stated plainly in the module doc: a rewritten request is a new object and does not carry dsh-agent-loop's process-local marker, so the loop's request-reconstruction invariant does not inspect it. That invariant exists to keep a sent request equal to the session log's derivation; a request that could not be sent at all is the one case where that equality is deliberately broken, knowingly, and with a log line.

Verification

npm test builds and runs 47 tests against a real @deepseek-ai/cordis context and a real @deepseek-ai/dsh-llm runtime, with a stub provider that refuses a request exactly the way moderation does (a terminal error finish, no content):

  • the unmounted control arm fails on the same request β€” the defect is real, and the plugin is what fixes it;
  • a poisoned session is rescued, in three adapter calls, and the consumer never sees a failure;
  • an attempt that already streamed content is not retried;
  • a rung that cannot reach the trigger does not retry forever, and the provider's own failure is then surfaced unchanged;
  • a healthy request is dispatched as the identical object, with one adapter call;
  • a differently worded 400 is not treated as moderation;
  • mode: off / observe / always, includeAuxiliary, a deep-frozen loop-style request, and the diagnostic bound.

Nine mutations of the implementation were run against this suite (transform disabled, hold-back guard removed, attachment protection removed, rung off-by-one, latch not written, latch not read, rejection gate removed, an undeclared import, a stale dependency) β€” each one turned the suite red.

Source

License

MIT