DeepSeek Harness Gemini 400: $defs $ref replay

TroubleshootingPublished 2026-10-20Author: DeepSeek Plugin Market
DeepSeek HarnessGeminiHTTP 400$defscordis_inspect_query
Gemini 400s every turn after cordis_inspect_query in DeepSeek Harness: stored $defs make replay read $ref as display_name. Inline, strip, or redesign.

The tool call itself succeeds; the bad part is the JSON it writes into history: cordis_inspect_query returns a config schema containing $defs, and on the next turn, when the result is replayed as a function_response, Gemini reads $ref as a reference to a multimodal attachment display_name, fails to find one, and returns 400. Every turn after that fails too. According to the discussion, the reporter reproduced this in the desktop app, and community members reviewed the tool-cordis source and return shape (discussion).

Which DeepSeek Harness turn starts returning the Gemini 400

The decisive pattern is "the tool succeeded, the next turn returns 400, a new session recovers"; when all three hold, history contamination is the likely cause. Reproduction and observation order:

  1. Switch the model to Gemini. Expected: requests use the native structured function_response path rather than plain-text tool results.
  2. Have the agent call cordis_inspect_query for an item such as Config.listConfigs that returns a schema with a loaderExpression reference. Expected: the tool result contains a $defs object.
  3. Observe that turn. Expected: the tool call succeeds with no error.
  4. Continue to the next turn. Expected:
400: {
  "message": "The referenced name `#/$defs/loaderExpression` in function_response.response does not match to a display_name in the function_response.parts.",
  "type": "upstream_error",
  "code": 400
}
  1. Switch models or retry in the same session. Expected: still 400. Create a new session. Expected: normal behavior.

Why DeepSeek Harness hands Gemini a $ref named display_name

Gemini scans function_response.response for $ref values and looks them up as attachment names in parts; #/$defs/loaderExpression is a document-local pointer, not an attachment name, so the lookup cannot succeed. The source reviewed by the community is packages/extensions/tool-cordis/src/config.ts:117:

ts
return toJson({
  ...listed,
  acceptsMissing,
  limitations,
  schema: {
    $schema: 'https://json-schema.org/draft/2020-12/schema',
    $defs: { ...definitions, loaderExpression: LOADER_EXPRESSION_SCHEMA },
    ...schema,
  },
})

Three steps explain it:

  1. The tool returns the full projected schema as its result, with $defs containing both the config projector's definitions and loaderExpression.
  2. The result is written into session history with no validation, because it is valid JSON.
  3. On the next turn the $ref is replayed verbatim inside function_response; Gemini fails to resolve it as an attachment name and returns 400. History only grows, so the error persists.

LOADER_EXPRESSION_SCHEMA comes from @deepseek-ai/dsh-app-boot, but inlining must not handle only that one: entries in definitions can also contain references. This is not the same trigger as Gemini thought signature 400; that one breaks on signature replay, while this one breaks on $ref inside a tool result.

DSH plugin fixes: inline, strip, or change the contract

All three paths stop the next turn from returning 400; they differ in whether you change the returned data, the tool contract, or just the history path. If you wrap this query in your own DSH plugin, the same fixes apply; prefer inlining for same-day relief and contract changes for team-level cleanup.

Path 1: inline $ref before toJson

This is the fastest stopgap with the least semantic change: replace each $ref with the object it points to. The output grows slightly, and Gemini still receives the full schema.

  1. Before the toJson() call, walk the schema deeply. Expected: every node in the schema is visited.
  2. When a $ref starts with #/$defs/, substitute the matching definition and enforce a recursion limit. Expected: references are replaced and recursion stops at the configured depth.
  3. After inlining, delete the top-level $defs. Expected: no $ref beginning with #/ remains in the returned object.
  4. Rerun the same query. Expected: the next turn no longer returns 400.

Illustrative code (not the repository's original):

ts
const deref = (node: any, defs: any): any =>
  Array.isArray(node) ? node.map((n) => deref(n, defs))
  : node?.$ref?.startsWith('#/$defs/') ? deref(defs[node.$ref.slice(8)], defs)
  : node && typeof node === 'object'
    ? Object.fromEntries(Object.entries(node).map(([k, v]) => [k, deref(v, defs)]))
    : node

Path 2: strip the schema or serialize it as a string

If consumers do not need to read the schema as a structure, the simplest option is to keep it from entering function_response as an object.

  1. Confirm which callers actually read the schema field. Expected: you get a list of callers that depend on structural access.
  2. Remove schema from the returned object, or replace it with JSON.stringify(schema) while keeping the $schema version and summary fields. Expected: the result no longer carries a nested schema object.
  3. Rerun the query. Expected: still valid JSON, but no nested $ref inside function_response.response.
  4. Check callers that rely on response.schema.$defs. Expected: those callers parse the string and still work.

The cost is structural access: consumers that need a field must parse it again.

Path 3: stop returning $defs from the query

Moving the schema out of "data" and back into "tool description" is the more correct direction, but it changes the contract, not one line of code.

  1. Make cordis_inspect_query return only a summary of config items plus a handle for follow-up queries. Expected: the result contains no schema.
  2. Put the schema in the tool definition or a separate type-query endpoint, fetched on demand. Expected: the schema has a dedicated lookup path.
  3. Update every caller. Expected: the code compiles without reading schema from the result.
  4. Rerun the full flow with Gemini. Expected: no tool result containing $defs enters history, and other providers still read the summary and handle correctly.

This has the highest correctness and the highest rollout cost. Treat it as follow-up cleanup, not same-day mitigation.

What to do with a poisoned DeepSeek Harness session

There is no official recovery: the bad result is already in persistent history, and a retry will not remove it. Order of operations:

  1. Start a new session and continue there. Expected: no more 400.
  2. If the old session has content you must keep, export the text and move it by hand. Expected: the new session contains only the text you chose to carry over.
  3. Do not hand-edit session.v4.jsonl.zstd. Expected: the compressed log stays valid and checksum-verifiable, which the edit would break.
  4. If session rollback is available, roll back to before the cordis_inspect_query call. Expected: the session becomes usable again.

Retest and boundaries for the DeepSeek Harness fix

Testing "this turn does not error" is not enough; test that "no result containing $ref can enter history again". Steps:

  1. Call cordis_inspect_query twice in a row under Gemini. Expected: both turns succeed.
  2. Inspect that tool result in the session log. Expected: no $ref beginning with #/.
  3. Use an ordinary query without $defs as a control. Expected: behavior matches the pre-fix baseline; run the same query under a non-Gemini provider and the tool result is still readable.

There is one boundary: only the native structured function_response path is sensitive. If results were ever passed as plain text the problem would vanish, but so would structured access.

FAQ

Why does DeepSeek Harness return Gemini 400 every turn after cordis_inspect_query says $defs is not a display_name?

DeepSeek Harness writes a JSON Schema containing $defs into session history as a tool result. On the next turn it is replayed as a function_response; Gemini reads $ref as a reference to a multimodal attachment display_name, cannot find it, and returns 400. History only grows, so every later turn fails.

In DeepSeek Harness, what is the difference between inlining $ref and deleting the schema?

Inlining $ref in DeepSeek Harness preserves the full structure and merely removes references, so consumers read the same schema. Deleting the schema turns the result into a summary or handle, reduces semantic information, and forces every caller that depends on the field to change.

Can a DeepSeek Harness session that already returns 400 be recovered?

DeepSeek Harness has no official recovery: the bad result is already in persistent history, and neither a retry nor a model switch removes it. Start a new session or roll back to history before the poisoning; hand-editing the session file is high risk.

Related Terms

JSON Schema $defs
`$defs` is the container for reusable subschemas in JSON Schema 2020-12, referenced elsewhere through `$ref`. Inside data it is ordinary JSON, but a specific API may attach extra semantics to it.— https://json-schema.org/draft/2020-12/json-schema-core
function_response
`function_response` is the structure that returns a tool result in Gemini's native function-calling protocol, paired with `function_call`. Its `response` field is a structured object parsed by the server according to protocol rules.— https://ai.google.dev/gemini-api/docs/function-calling
display_name
`display_name` names an attachment in Gemini multimodal parts so the model or server can refer to it. The protocol allows a `function_response` to reference an attachment by name, which explains why `$ref` is misread.— https://ai.google.dev/gemini-api/docs/function-calling

Sources