{"_id":"@argszero/cordis-plugin-llm-tool-call-guard","_rev":"6-3d0b7d4e54bf4cc0e883e793f0048649","name":"@argszero/cordis-plugin-llm-tool-call-guard","dist-tags":{"latest":"0.1.5"},"versions":{"0.1.0":{"name":"@argszero/cordis-plugin-llm-tool-call-guard","version":"0.1.0","keywords":["cordis","deepseek-harness","dsh","plugin","llm","stream","tool-call","runaway","guard"],"license":"MIT","_id":"@argszero/cordis-plugin-llm-tool-call-guard@0.1.0","maintainers":[{"name":"argszero","email":"argszero.reg@gmail.com"}],"dsh":{"bundle":{"patch":"./cordis.patch.yml"}},"dist":{"shasum":"38912721246ca5b24603f3216ecfa300f8f19cc6","tarball":"https://registry.npmjs.org/@argszero/cordis-plugin-llm-tool-call-guard/-/cordis-plugin-llm-tool-call-guard-0.1.0.tgz","fileCount":6,"integrity":"sha512-V3+XItMWyioHYMiL95wEfdj7RDwbU0OSvzYcZ7FZfKYe93lOtqMWc1GiDo1+7LWJyKBjGM5O0OFuHGOq5+ty0g==","signatures":[{"sig":"MEQCID54fI3dHwRgsWpRXdgIvKyNxERQlsZCH/54sz9w1loIAiBnLA/ZEJS269ct2P+MxQJ9kHEtVZLoP2qKXtvuXfupNA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":17824},"main":"lib/index.js","type":"module","types":"lib/types/index.d.ts","exports":{".":{"types":"./lib/types/index.d.ts","default":"./lib/index.js"},"./src/*":"./src/*","./package.json":"./package.json"},"gitHead":"1d90c90fc8db20a4d3c58f05c9183be7aa0be348","scripts":{"test":"tsc && node --test \"test/*.test.js\"","build":"tsc","prepublishOnly":"tsc"},"_npmUser":{"name":"argszero","email":"argszero.reg@gmail.com"},"_npmVersion":"11.17.0","description":"Runaway tool-call argument guard for dsh: wrap the llm/stream waterfall and cut a tool call that streams more than a configurable number of arguments bytes, terminating with a routed TOOL_CALL_ARGUMENTS_TOO_LARGE error finish so the oversized call is neve","directories":{},"_nodeVersion":"26.5.0","dependencies":{"@deepseek-ai/schemastery":"^3.18.1"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.5.0","@types/node":"^26.5.0","@deepseek-ai/cordis":"^4.0.2","@deepseek-ai/dsh-llm":"0.1.5-alpha.1"},"peerDependencies":{"@deepseek-ai/cordis":"^4.0.2","@deepseek-ai/dsh-llm":">=0.1.2"},"_npmOperationalInternal":{"tmp":"tmp/cordis-plugin-llm-tool-call-guard_0.1.0_1788969495453_0.33904744842880996","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@argszero/cordis-plugin-llm-tool-call-guard","version":"0.1.1","keywords":["cordis","deepseek-harness","dsh","plugin","llm","stream","tool-call","runaway","guard"],"license":"MIT","_id":"@argszero/cordis-plugin-llm-tool-call-guard@0.1.1","maintainers":[{"name":"argszero","email":"argszero.reg@gmail.com"}],"dsh":{"bundle":{"patch":"./cordis.patch.yml"}},"dist":{"shasum":"c535e7ec2a7d7a9c2ea224ea335725a88ad53761","tarball":"https://registry.npmjs.org/@argszero/cordis-plugin-llm-tool-call-guard/-/cordis-plugin-llm-tool-call-guard-0.1.1.tgz","fileCount":6,"integrity":"sha512-fIzsa5EXf4EPZD8RxI9SK6xVntNBN2QRAe/zel+weHXCFJyg5cKkYNg3eQjwmFzV0f5woz36M6Bj6W1H3KG1rw==","signatures":[{"sig":"MEUCIQDbXHOVqZFCizqFJmFPDaIRU8DqWFs1PxB9KQKbpbeMmAIgY7CqXkYK0CXEoGjN7f+Uh3WSxpDQE/WJnPNlE5qfw/A=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":22721},"main":"lib/index.js","type":"module","types":"lib/types/index.d.ts","exports":{".":{"types":"./lib/types/index.d.ts","default":"./lib/index.js"},"./src/*":"./src/*","./package.json":"./package.json"},"gitHead":"1d90c90fc8db20a4d3c58f05c9183be7aa0be348","scripts":{"test":"tsc && node --test \"test/*.test.js\"","build":"tsc","prepublishOnly":"tsc"},"_npmUser":{"name":"argszero","email":"argszero.reg@gmail.com"},"_npmVersion":"11.17.0","description":"Runaway tool-call argument guard for dsh: wrap the llm/stream waterfall and cut a tool call that streams more than a configurable number of arguments bytes, terminating with a routed TOOL_CALL_ARGUMENTS_TOO_LARGE error finish so the oversized call is neve","directories":{},"_nodeVersion":"26.5.0","dependencies":{"@deepseek-ai/schemastery":"^3.18.1"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.5.0","@types/node":"^26.5.0","@deepseek-ai/cordis":"^4.0.2","@deepseek-ai/dsh-llm":"0.1.5-alpha.1"},"peerDependencies":{"@deepseek-ai/cordis":"^4.0.2","@deepseek-ai/dsh-llm":">=0.1.2"},"_npmOperationalInternal":{"tmp":"tmp/cordis-plugin-llm-tool-call-guard_0.1.1_1788976617030_0.9097527323076087","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@argszero/cordis-plugin-llm-tool-call-guard","version":"0.1.2","keywords":["cordis","deepseek-harness","dsh","plugin","llm","stream","tool-call","runaway","guard","fragments","utf-8"],"license":"MIT","_id":"@argszero/cordis-plugin-llm-tool-call-guard@0.1.2","maintainers":[{"name":"argszero","email":"argszero.reg@gmail.com"}],"dsh":{"bundle":{"patch":"./cordis.patch.yml"}},"dist":{"shasum":"92a19d6337cfee9a8da072609231c708b8845e6b","tarball":"https://registry.npmjs.org/@argszero/cordis-plugin-llm-tool-call-guard/-/cordis-plugin-llm-tool-call-guard-0.1.2.tgz","fileCount":6,"integrity":"sha512-Mzg+9Zq4Iqgba2SmQgePLbdVKvwGCtALM/n69mtV9BA8/yIk0KuBh9L/iQR5mQT9hM490fP24iGq4scZczc4fg==","signatures":[{"sig":"MEUCIF84FGTgIPrPN8H0iHZ7hPLq3o0Y1cbXukAOXoj3ehsZAiEAi8cEiaTmb9crWkaZg6SICZ4Puv1f8VNhWUOUo6hzU3s=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":30264},"main":"lib/index.js","type":"module","types":"lib/types/index.d.ts","exports":{".":{"types":"./lib/types/index.d.ts","default":"./lib/index.js"},"./src/*":"./src/*","./package.json":"./package.json"},"gitHead":"b8fa87c50e29d42f095b431325cc70c02c1e2cfc","scripts":{"test":"tsc && node --test \"test/*.test.js\"","build":"tsc","prepublishOnly":"tsc"},"_npmUser":{"name":"argszero","email":"argszero.reg@gmail.com"},"_npmVersion":"11.17.0","description":"Runaway tool-call argument guard for dsh: wrap the llm/stream waterfall and cut a tool call that streams more than a configurable number of arguments bytes (UTF-8, maxArgsBytes) or fragments (maxArgsFragments), terminating with a routed error finish so th","directories":{},"_nodeVersion":"26.5.0","dependencies":{"@deepseek-ai/schemastery":"^3.18.1"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.5.0","@types/node":"^26.5.0","@deepseek-ai/cordis":"^4.0.2","@deepseek-ai/dsh-llm":"0.1.5-alpha.1"},"peerDependencies":{"@deepseek-ai/cordis":"^4.0.2","@deepseek-ai/dsh-llm":">=0.1.2"},"_npmOperationalInternal":{"tmp":"tmp/cordis-plugin-llm-tool-call-guard_0.1.2_1788993765246_0.5893577547566693","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@argszero/cordis-plugin-llm-tool-call-guard","version":"0.1.3","keywords":["cordis","deepseek-harness","dsh","plugin","llm","stream","tool-call","runaway","guard","fragments","utf-8"],"license":"MIT","_id":"@argszero/cordis-plugin-llm-tool-call-guard@0.1.3","maintainers":[{"name":"argszero","email":"argszero.reg@gmail.com"}],"dsh":{"bundle":{"patch":"./cordis.patch.yml"}},"dist":{"shasum":"a880651c578549800d077ce94866adb0bad00d2a","tarball":"https://registry.npmjs.org/@argszero/cordis-plugin-llm-tool-call-guard/-/cordis-plugin-llm-tool-call-guard-0.1.3.tgz","fileCount":6,"integrity":"sha512-/w5s53/u3FFHhL6yjUNngI6i3zUC2o09QHxi579woozjN1Vk4q5wXMtsgKAaP1X73mBSPwYrxkZ1RNjJp+fsDQ==","signatures":[{"sig":"MEUCIQDUYpAYdT1qRAeKdvobh8yTGmKmMnEpWehvcbIipGfC6QIgFeCkgrsjj7qT/I1yGuRSI+qEZyvTazcDa6hl/iiTUmY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":30302},"main":"lib/index.js","type":"module","types":"lib/types/index.d.ts","exports":{".":{"types":"./lib/types/index.d.ts","default":"./lib/index.js"},"./src/*":"./src/*","./package.json":"./package.json"},"gitHead":"b8fa87c50e29d42f095b431325cc70c02c1e2cfc","scripts":{"test":"tsc && node --test \"test/*.test.js\"","build":"tsc","prepublishOnly":"tsc"},"_npmUser":{"name":"argszero","email":"argszero.reg@gmail.com"},"_npmVersion":"11.17.0","description":"Runaway tool-call argument guard for dsh: wrap the llm/stream waterfall and cut a tool call that streams more than a configurable number of arguments bytes (UTF-8, maxArgsBytes) or fragments (maxArgsFragments), terminating with a routed error finish so th","directories":{},"_nodeVersion":"26.5.0","dependencies":{"@deepseek-ai/schemastery":"^3.18.1"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.5.0","@types/node":"^26.5.0","@deepseek-ai/cordis":"^4.0.2","@deepseek-ai/dsh-llm":"0.1.5-alpha.1"},"peerDependencies":{"@deepseek-ai/cordis":"^4.0.2","@deepseek-ai/dsh-llm":">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0"},"_npmOperationalInternal":{"tmp":"tmp/cordis-plugin-llm-tool-call-guard_0.1.3_1789010252179_0.5797275128289407","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@argszero/cordis-plugin-llm-tool-call-guard","version":"0.1.4","keywords":["cordis","corrupt-session","deepseek-harness","dsh","fragments","guard","identity","llm","plugin","repair","runaway","stream","tool-call","utf-8"],"license":"MIT","_id":"@argszero/cordis-plugin-llm-tool-call-guard@0.1.4","maintainers":[{"name":"argszero","email":"argszero.reg@gmail.com"}],"dsh":{"bundle":{"patch":"./cordis.patch.yml"}},"dist":{"shasum":"cb49559d573f1f5a074a86e6f4d393843979e1e5","tarball":"https://registry.npmjs.org/@argszero/cordis-plugin-llm-tool-call-guard/-/cordis-plugin-llm-tool-call-guard-0.1.4.tgz","fileCount":6,"integrity":"sha512-oZ2kzF15Wm6v5MIBwzRwg4I97k1E3KUydEQ1qXxRkZpv98j4cyike4r4N6AIIv3Dp1FFJhx8dkWfWD4gQv3b2A==","signatures":[{"sig":"MEUCIQCF8ouZvWvNqOsCiBIqLLdltRqMHeuImGjAwZMpNQQOLQIgcOHZwUNRzLCKrI0cMe0dLm3Y9v55xS0spiGUGXU35BU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":48884},"main":"lib/index.js","type":"module","types":"lib/types/index.d.ts","exports":{".":{"types":"./lib/types/index.d.ts","default":"./lib/index.js"},"./src/*":"./src/*","./package.json":"./package.json"},"gitHead":"d751220d8e69996ba8a0dcb8b3fc535518a7d06d","scripts":{"test":"tsc && node --test \"test/*.test.js\"","build":"tsc","prepublishOnly":"tsc"},"_npmUser":{"name":"argszero","email":"argszero.reg@gmail.com"},"_npmVersion":"11.17.0","description":"Runaway tool-call argument guard for dsh: wrap the llm/stream waterfall and cut a tool call that streams more than a configurable number of arguments bytes (UTF-8, maxArgsBytes) or fragments (maxArgsFragments), terminating with a routed error finish so th","directories":{},"_nodeVersion":"26.5.0","dependencies":{"@deepseek-ai/schemastery":"^3.18.1"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.5.0","@types/node":"^26.5.0","@deepseek-ai/cordis":"^4.0.2","@deepseek-ai/dsh-llm":"0.1.5-alpha.1"},"peerDependencies":{"@deepseek-ai/cordis":"^4.0.2","@deepseek-ai/dsh-llm":">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0"},"_npmOperationalInternal":{"tmp":"tmp/cordis-plugin-llm-tool-call-guard_0.1.4_1789052361500_0.704404993352282","host":"s3://npm-registry-packages-npm-production"}},"0.1.5":{"_id":"@argszero/cordis-plugin-llm-tool-call-guard@0.1.5","dsh":{"bundle":{"patch":"./cordis.patch.yml"}},"dist":{"shasum":"9b90a4ce719954566e5869dac90ec31c9a271582","tarball":"https://registry.npmjs.org/@argszero/cordis-plugin-llm-tool-call-guard/-/cordis-plugin-llm-tool-call-guard-0.1.5.tgz","fileCount":6,"integrity":"sha512-dOSH+whK/Q4DP6AAT3Yn9tEZhRqxyI+1TV7+ZYVuIuOnoMbqQcOM4vIZQcgEE13skp6HmhepV61kq/hRAGOXmQ==","signatures":[{"sig":"MEUCIQDlIWS9l9FT/ec5oMtchOwp2bz/BLjg8tba+9Vri7D7VwIgfzVfzO6pZot735+lZ8b6fM4KoICsokua3ck3CBJN3Mg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCp2lfZbSrk+LZGYvLPTJzYRKgTnrF2+Wg0o9vdlyJo9QIhAN+yWQ+Pm4Q3pP4FmlsL/XJW7/h3Mo3MrUcX/EucXg8m"}],"unpackedSize":50815},"main":"lib/index.js","name":"@argszero/cordis-plugin-llm-tool-call-guard","type":"module","types":"lib/types/index.d.ts","exports":{".":{"types":"./lib/types/index.d.ts","default":"./lib/index.js"},"./src/*":"./src/*","./package.json":"./package.json"},"gitHead":"b22bd6bd9b293d0fb6da571d3d18c8938178fed3","license":"MIT","scripts":{"test":"tsc && node --test \"test/*.test.js\"","build":"tsc","prepublishOnly":"tsc"},"version":"0.1.5","_npmUser":{"name":"argszero","email":"argszero.reg@gmail.com"},"keywords":["cordis","corrupt-session","deepseek-harness","dsh","fragments","guard","identity","llm","plugin","repair","runaway","stream","tool-call","utf-8"],"_npmVersion":"11.17.0","description":"Runaway tool-call argument guard for dsh: wrap the llm/stream waterfall and cut a tool call that streams more than a configurable number of arguments bytes (UTF-8, maxArgsBytes) or fragments (maxArgsFragments), terminating with a routed error finish so th","directories":{},"maintainers":[{"name":"argszero","email":"argszero.reg@gmail.com"}],"_nodeVersion":"26.5.0","dependencies":{"@deepseek-ai/schemastery":"^3.18.1"},"_hasShrinkwrap":false,"devDependencies":{"semver":"^7.6.0","typescript":"^5.5.0","@types/node":"^26.5.0","@deepseek-ai/cordis":"^4.0.2","@deepseek-ai/dsh-llm":"0.1.6-alpha.2"},"peerDependencies":{"@deepseek-ai/cordis":"^4.0.2","@deepseek-ai/dsh-llm":">=0.1.2-rc.1 <0.1.3 || >=0.1.3-alpha.2 <0.1.4 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-alpha.1 <0.2.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/cordis-plugin-llm-tool-call-guard_0.1.5_1789715565201_0.33006699461762934"}}},"time":{"created":"2026-09-09T15:58:15.303Z","modified":"2026-09-18T07:12:45.547Z","0.1.0":"2026-09-09T15:58:15.587Z","0.1.1":"2026-09-09T17:56:57.167Z","0.1.2":"2026-09-09T22:42:45.391Z","0.1.3":"2026-09-10T03:17:32.321Z","0.1.4":"2026-09-10T14:59:21.614Z","0.1.5":"2026-09-18T07:12:45.303Z"},"license":"MIT","keywords":["cordis","corrupt-session","deepseek-harness","dsh","fragments","guard","identity","llm","plugin","repair","runaway","stream","tool-call","utf-8"],"description":"Runaway tool-call argument guard for dsh: wrap the llm/stream waterfall and cut a tool call that streams more than a configurable number of arguments bytes (UTF-8, maxArgsBytes) or fragments (maxArgsFragments), terminating with a routed error finish so th","maintainers":[{"name":"argszero","email":"argszero.reg@gmail.com"}],"readme":"# @argszero/cordis-plugin-llm-tool-call-guard\n\nRunaway tool-call argument guard for the **dsh** harness (deepseek-harness). It wraps\nthe [`llm/stream`](https://deepseek-ai.github.io/deepseek-harness/) waterfall and cuts a\nsingle tool call that streams **more than a configurable number of arguments bytes** (or\n**fragments**), terminating the stream with a routed error finish so the oversized call is\n**never executed**.\n\n## Why\n\nA model can start a tool call and stream its JSON arguments until it exhausts the entire\nresponse output-token budget. The assembled tool-call `arguments` grows unboundedly, the\nprovider returns a terminal `max-tokens` finish, and the user only sees the generic\n\"Output token limit reached\" — the model's reasoning is invisible and the oversized (often\nmalformed) call is either executed or silently discarded. This is discussion **#6059**.\n\n## What it does\n\n- Counts the accumulated `argumentsDelta` **UTF-8 bytes** per tool-call block index (a model\n  can interleave several calls, so the budget applies to each call independently).\n- When one call's arguments exceed `maxArgsBytes` (default **24576** = 24 KiB), it stops\n  consuming the upstream generator (releasing the provider connection and the still-growing\n  arguments stream) and emits a **synthesized terminal `error` finish** with the stable\n  failure code `TOOL_CALL_ARGUMENTS_TOO_LARGE`.\n- Optionally counts the **number of `tool-call-delta` fragments** per call index\n  (`maxArgsFragments`, default `0` = off). Bytes and fragments measure *different* runaway\n  shapes: the recorded #6059 stream is 4,074 deltas but only 4,658 bytes — a **fragment-count\n  runaway** that a byte budget alone never fires. When an index exceeds `maxArgsFragments` it\n  cuts the source and emits the distinct code `TOOL_CALL_ARGUMENTS_TOO_MANY_FRAGMENTS`.\n- Optionally counts the **whole-request aggregate** `argumentsDelta` across *every* tool-call\n  block in one stream (`maxTotalArgsBytes`, default `0` = off). When the sum exceeds that\n  budget it cuts the source and emits the distinct code `TOOL_CALL_ARGUMENTS_TOTAL_TOO_LARGE`\n  — a different failure shape from the per-call guard (one call too big vs. too many calls).\n- The agent loop branches on the finish reason **before** it filters assistant content for\n  tool-call blocks, so an `error` finish routes to `agent/request-error` and **never executes**\n  the oversized call. `llm-invariant` explicitly allows an error/aborted finish to carry open\n  block indexes, so the finish is protocol-legal.\n- **Repairs a tool call that streams an empty identity** (`repair`, default `'repair'`) — the\n  `{\"id\":\"\",\"name\":\"\"}` shape behind discussion **#6152**. See [Empty tool-call identity](#empty-tool-call-identity-6152) below.\n- Every chunk under the budget passes through **byte-for-byte**; only the breaching call is cut.\n\n## Install / mount\n\n```sh\nnpm install @argszero/cordis-plugin-llm-tool-call-guard\n```\n\nThen mount it in a dsh profile (the bundle patch exposes the plugin id `llm-tool-call-guard`):\n\n```yaml\n# cordis.patch.yml (or an overlay)\n- insert:\n    - id: llm-tool-call-guard\n      name: '@argszero/cordis-plugin-llm-tool-call-guard'\n```\n\nTune via config:\n\n```yaml\n- set:\n    - id: llm-tool-call-guard\n      config:\n        maxArgsBytes: 8192        # per-call argument budget (byte); 0 disables\n        maxArgsFragments: 1024    # per-call fragment budget (count); 0 disables\n        maxTotalArgsBytes: 32768  # whole-request aggregate budget (byte); 0 disables\n        fail: true                 # emit error finish (never execute the oversized call)\n```\n\n## Config\n\n| Field | Default | Description |\n|-------|---------|-------------|\n| `maxArgsBytes` | `24576` | Max accumulated `argumentsDelta` **UTF-8 bytes** per tool-call block index. `0` disables the byte guard. |\n| `maxArgsFragments` | `0` | Max number of `tool-call-delta` **fragments** per tool-call block index. `0` disables the fragment guard. Catches the #6059 fragment-count runaway that a byte budget alone misses. |\n| `maxTotalArgsBytes` | `0` | Max *cumulative* `argumentsDelta` **UTF-8 bytes** across all tool-call blocks in one stream (whole-request budget). `0` disables the aggregate guard. |\n| `fail` | `true` | On breach, emit a terminal `error` finish (routes to `agent/request-error`, call not executed). `false` = observe-only: cut the source but emit a normal `stop` finish (partial call treated normally). |\n| `repair` | `'repair'` | What to do with an **empty tool-call identity** (#6152): `'repair'` substitutes a deterministic synthetic call id, `'error'` cuts the stream with `TOOL_CALL_EMPTY_IDENTITY`, `'off'` preserves v0.1.3 exactly. |\n| `repairBytes` | `true` | With `repair: 'repair'`, also substitute `{}` for an empty `argumentsDelta` on the repaired call. Only the block's first delta is inspected, so this can only affect a genuinely identity-less block. |\n\n## Empty tool-call identity (#6152)\n\nA model — observed with an OpenAI-completions-style provider — can emit a tool call whose\nidentity is blank:\n\n```json\n{\"type\": \"tool-call\", \"id\": \"\", \"name\": \"\", \"arguments\": \"{}\"}\n```\n\nThe harness persists that as-is, and the **next** load rejects the stored `tool/result`\n(`assertMessageEventShape` requires a non-empty `source.callId`). The whole conversation is\nthen marked corrupt and the session can no longer be started or resumed; recovery means\nhand-patching the `.jsonl.zstd` frame container.\n\nThe asymmetry is worth stating precisely, because it explains why no existing layer caught\nit: the validator that rejects the event **already exists** and is called from the *read*\nboundary and the seed path — but `Session.append` never calls it. The OpenAI-completions\nadapter emits `''` as a deliberate \"id not yet seen\" sentinel, and the assembler's\n`?? 'call-N'` fallback never fires on it because `''` is not `undefined`.\n\nThis plugin sits on the producer-side seam (`llm/stream`), so it can close the gap before the\nevent is persisted:\n\n- **`repair: 'repair'`** (default) rewrites the empty id to a deterministic\n  `repaired-call-<index>` on **both** the `tool-call-delta` chunks and the `block-end` block.\n  Repairing only the deltas would be a silent no-op, because the core assembler treats\n  `block-end` as authoritative and overwrites the accumulated values.\n- An empty **`name`** is deliberately left alone. The harness already turns it into a\n  `ToolNotFoundError` / `UNKNOWN_TOOL` error result, which is an honest and *resumable*\n  outcome. Blanking the name would make the call vanish; inventing a plausible one would\n  fabricate a call the model never requested.\n- **`repair: 'error'`** cuts the stream with a terminal `error` finish (code\n  `TOOL_CALL_EMPTY_IDENTITY`), so the degenerate call is never executed at all.\n- **`repair: 'off'`** preserves v0.1.3 behaviour byte-for-byte.\n\nEvery repaired block is reported **once** at `warn` level, naming the field(s) that were\nempty. That is deliberate: \"nothing was visible\" is the heart of the #6152 report.\n\n> **Scope.** This is a mitigation at the producer seam, not the authoritative fix. A plugin\n> cannot prevent the append — the write boundary needs its own check. Reported upstream in\n> [#6152](https://github.com/deepseek-ai/deepseek-harness/discussions/6152).\n\n## UTF-8 byte counting\n\nThe guard counts `argumentsDelta` in **UTF-8 bytes** (via `TextEncoder`), matching the option\nname and docs. It does **not** use `.length`, which counts UTF-16 code units and would\nunder-count non-ASCII arguments (e.g. `'中'` is 3 bytes but 1 code unit).\n\n## Regression fixture\n\nThe test suite includes a **boundary-exact recorded stream** from discussion #6059: 4,074\n`tool-call-delta` fragments / 4,658 bytes / histogram `{ 1: 3492, 2: 581, 4: 1 }`, supplied by\n`@luisnomad`. The fixture asserts the byte-only guard passes it unchanged (the blind spot) while\nthe fragment guard correctly catches it.\n\n## Peer range\n\nThe plugin declares `@deepseek-ai/dsh-llm` as a peer dependency with range:\n\n```\n>=0.1.2-rc.1 <0.1.3 || >=0.1.3-alpha.2 <0.1.4 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-alpha.1 <0.2.0\n```\n\nEvery dsh release published today is a **prerelease** (`0.1.2-rc.1`, `0.1.5-alpha.1`,\n`0.1.6-alpha.2`, …), and a semver comparator admits a prerelease only when some\ncomparator **in the same group** shares its `major.minor.patch` tuple. Two\nconsequences follow, and both have already bitten this package:\n\n```jsonc\n// Matches nothing: 0.1.2-rc.1 sorts BELOW 0.1.2, and every other prerelease\n// carries a different tuple.  -> ETARGET, the package cannot be installed.\n\">=0.1.2\"\n\n// Only the 0.1.2-rc tuple. The `<0.2.0` upper bound is INERT for prereleases:\n// it excludes no later line, so every other line gets ERESOLVE.\n\">=0.1.2-rc.1 <0.2.0\"\n```\n\nThe second form is the dangerous one, because it *reads* as though it covered\neverything from `0.1.2-rc.1` onward. It does not — `<0.2.0` never excludes\n`0.1.6-alpha.2`, and no comparator names that tuple. Up to **v0.1.4** the\nshipped range was\n\n```\n>=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0\n```\n\nwhich admitted the `0.1.2-rc` and `0.1.5` tuples and **nothing else** — 5 of the\n23 published versions. A user on the newest shipped dsh release\n(`0.1.6-alpha.2`) therefore could not install the plugin at all:\n\n```\nnpm error ERESOLVE unable to resolve dependency tree\nnpm error peer @deepseek-ai/dsh-llm@\">=0.1.2-rc.1 <0.2.0 || ...\" from\nnpm error   @argszero/cordis-plugin-llm-tool-call-guard@0.1.4\n```\n\nThe plugin's own suite passes on that line (51/51). The dsh packages are\n**peers**, so `--legacy-peer-deps` is not something a consumer can reasonably be\nasked to accept: the install simply fails.\n\n**What we ship:** one comparator per supported tuple, each with its own upper\nbound so the intended span is legible rather than implied.\n\n| clause | admits |\n| --- | --- |\n| `>=0.1.2-rc.1 <0.1.3` | `0.1.2-rc.1` |\n| `>=0.1.3-alpha.2 <0.1.4` | `0.1.3-alpha.2` |\n| `>=0.1.5-alpha.1 <0.2.0` | the whole 0.1.5 line (alpha.1, alpha.2, rc.1, rc.2) |\n| `>=0.1.6-alpha.1 <0.2.0` | the whole 0.1.6 line (alpha.1, alpha.2) |\n\n`test/peer-range` **computes** the admitted set with the real `semver` package\nand asserts it equals exactly the set the suite has been run against — 8\nversions — rather than pattern-matching the range string. An earlier guard only\nchecked that the string mentioned `0.1.2-rc.N` and `0.1.5-alpha.N`; that form\ncannot tell a correct range from an incorrect one, which is how the range above\nshipped green. Asserting the set exactly makes both directions loud: dropping a\nsupported line fails, and admitting an unverified line fails too. The same file\nalso asserts that this README quotes the manifest range verbatim.\n## License\n\nMIT\n","readmeFilename":"README.md"}