{"_id":"@argszero/cordis-plugin-steer-preempt","_rev":"4-401d389db0e809340a2f8a3feed6cdd5","name":"@argszero/cordis-plugin-steer-preempt","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@argszero/cordis-plugin-steer-preempt","version":"0.1.0","keywords":["cordis","deepseek-harness","dsh","plugin","steer","preempt","jobs","wait"],"license":"MIT","_id":"@argszero/cordis-plugin-steer-preempt@0.1.0","maintainers":[{"name":"argszero","email":"argszero.reg@gmail.com"}],"dsh":{"bundle":{"patch":"./cordis.patch.yml"}},"dist":{"shasum":"f11164b0230c860e272fe6f9b4dbd4b46ce77863","tarball":"https://registry.npmjs.org/@argszero/cordis-plugin-steer-preempt/-/cordis-plugin-steer-preempt-0.1.0.tgz","fileCount":6,"integrity":"sha512-T4Dt4ohlZ+MWcuTeIsrN6rjXKNzwm/aQ3LHxdgtbzBotL6arwvJ1/6QAd0T9lHzOBo8ENaBlQ+yt7Ic7HZz9CQ==","signatures":[{"sig":"MEUCIQDP0cn4N21rNxjU18dHOFJAGcWTAp9v1S7rrXP2SziYqQIgbtoV7kLU2e5+E36LrZ1TgIGo/oSK6WXn39js0lq8iP8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":21165},"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"},"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":"Steer preemption for dsh: a new user input (next-step steer) interrupts a blocking job_output(wait: true) read immediately instead of letting it block up to the job-wait cap, so the agent loop reaches its step boundary and honors the steer. The background","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-agent":"0.1.5-alpha.1","@deepseek-ai/dsh-tools":"0.1.5-alpha.1"},"peerDependencies":{"@deepseek-ai/cordis":"^4.0.2","@deepseek-ai/dsh-tools":">=0.1.2"},"_npmOperationalInternal":{"tmp":"tmp/cordis-plugin-steer-preempt_0.1.0_1788957573178_0.5202749437338414","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@argszero/cordis-plugin-steer-preempt","version":"0.1.1","keywords":["cordis","deepseek-harness","dsh","plugin","steer","preempt","jobs","wait"],"license":"MIT","_id":"@argszero/cordis-plugin-steer-preempt@0.1.1","maintainers":[{"name":"argszero","email":"argszero.reg@gmail.com"}],"dsh":{"bundle":{"patch":"./cordis.patch.yml"}},"dist":{"shasum":"855d3286056764b621601a1b40ea2476cb69ddd2","tarball":"https://registry.npmjs.org/@argszero/cordis-plugin-steer-preempt/-/cordis-plugin-steer-preempt-0.1.1.tgz","fileCount":6,"integrity":"sha512-RG5hOowtGl0MF87yzYd3Z8yIcfcMs8FTjK83trp1q2IKWgh62cO90y8SDtE+I+WzNWx+5R+pAEb+JAGCcb0t7Q==","signatures":[{"sig":"MEUCIQClpp4qeSCwPjooB+slSC3eCmTPgF+2gyZq8oAQ54qBLQIgWfiIIqu2uBhCo1gbUqAABKBSY371lxZtBFKyDjSTqj4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIH6z22rZxCcB+xIXz9ICj4r9szIfeGEUQIC/bAJg/ZnOAiEAhIzBpSLJum3wCr8dJOMf9EUI1/mA8Pgm4Z0E8sWePjQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":24473},"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":"1c7d226113698c44e1981f67c562f88754196a36","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":"Steer preemption for dsh: a new user input (next-step steer) interrupts a blocking job_output(wait: true) read immediately instead of letting it block up to the job-wait cap, so the agent loop reaches its step boundary and honors the steer. The background","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-agent":"0.1.5-alpha.1","@deepseek-ai/dsh-tools":"0.1.5-alpha.1"},"peerDependencies":{"@deepseek-ai/cordis":"^4.0.2","@deepseek-ai/dsh-tools":">=0.1.2"},"_npmOperationalInternal":{"tmp":"tmp/cordis-plugin-steer-preempt_0.1.1_1788969897565_0.3924227506537279","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@argszero/cordis-plugin-steer-preempt","version":"0.1.2","keywords":["cordis","deepseek-harness","dsh","plugin","steer","preempt","jobs","wait"],"license":"MIT","_id":"@argszero/cordis-plugin-steer-preempt@0.1.2","maintainers":[{"name":"argszero","email":"argszero.reg@gmail.com"}],"dsh":{"bundle":{"patch":"./cordis.patch.yml"}},"dist":{"shasum":"65dfd05d7e84ae37500977751c65a7fbfff97cfa","tarball":"https://registry.npmjs.org/@argszero/cordis-plugin-steer-preempt/-/cordis-plugin-steer-preempt-0.1.2.tgz","fileCount":6,"integrity":"sha512-ujT5ipUcEC+JCPTHxBtwF08C5MCr7N2A8Od5Tn3Z1zpmS5koZWMkg4tgBivMvxnYUGwFphF39Esv9/rfp38mlQ==","signatures":[{"sig":"MEUCIQD6RCBs7ALEpJtFaQDB3uVZg+V2W0HSM0D4lGoiULpfFAIgbCzg01dOIMXA5uZTQHQUMB9F3AiMByVJxnz5PBZGkrA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQCuiR2bRlwljfxSTTYkBcuJoGoAg8RWqtwH8ptYXMoH+QIhAM5v5AM3NkH8/pkUYpccF6fbv7GTMMW6rZ0cuuGqAXjr","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":24511},"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":"1c7d226113698c44e1981f67c562f88754196a36","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":"Steer preemption for dsh: a new user input (next-step steer) interrupts a blocking job_output(wait: true) read immediately instead of letting it block up to the job-wait cap, so the agent loop reaches its step boundary and honors the steer. The background","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-agent":"0.1.5-alpha.1","@deepseek-ai/dsh-tools":"0.1.5-alpha.1"},"peerDependencies":{"@deepseek-ai/cordis":"^4.0.2","@deepseek-ai/dsh-tools":">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0"},"_npmOperationalInternal":{"tmp":"tmp/cordis-plugin-steer-preempt_0.1.2_1789010443131_0.2420268420510634","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"_id":"@argszero/cordis-plugin-steer-preempt@0.2.0","dsh":{"bundle":{"patch":"./cordis.patch.yml"}},"dist":{"shasum":"5098dfb30194c82f147df2c3bece66e99f3fa986","tarball":"https://registry.npmjs.org/@argszero/cordis-plugin-steer-preempt/-/cordis-plugin-steer-preempt-0.2.0.tgz","fileCount":6,"integrity":"sha512-CGh6L+uR76zNJKz/7sy4s/4axkkOvINE2bZNWYrpUEjb1lPidTGNja9rkdyjTcwIDZEDe3Byc2VKyDWqAPv5IA==","signatures":[{"sig":"MEYCIQDNcVnUsDt1/nbF33UoEouyP2fUuhAUfWtZ2wPcQT4lqAIhAM3md+AhAjF51rIsWgFGptlokW0jFKZ7U+VQo1Czs97X","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCNbTYV2rX4x/bG/1q9zjgTaaGmCygAaY7MVLsa+nT6EwIgb38+cxJIJz3bIDns4si7Zw8s0/+VUcmnlyE6oSBW6Ok="}],"unpackedSize":42875},"main":"lib/index.js","name":"@argszero/cordis-plugin-steer-preempt","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":"2b1731b157b65c574d6b8832262e80c2f87e14d6","license":"MIT","scripts":{"test":"tsc && node --test \"test/*.test.js\"","build":"tsc","prepublishOnly":"tsc"},"version":"0.2.0","_npmUser":{"name":"argszero","email":"argszero.reg@gmail.com"},"keywords":["cordis","deepseek-harness","dsh","plugin","steer","preempt","jobs","wait","output"],"_npmVersion":"11.17.0","description":"Steer and new-output preemption for dsh: a new user input (next-step steer) or new output from the awaited job interrupts a blocking job_output(wait: true) read immediately instead of letting it block up to the job-wait cap, so the agent loop reaches its ","directories":{},"maintainers":[{"name":"argszero","email":"argszero.reg@gmail.com"}],"_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-agent":"0.1.5-alpha.1","@deepseek-ai/dsh-tools":"0.1.5-alpha.1"},"peerDependencies":{"@deepseek-ai/cordis":"^4.0.2","@deepseek-ai/dsh-tools":">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/cordis-plugin-steer-preempt_0.2.0_1789121127575_0.6792636341323837"}}},"time":{"created":"2026-09-09T12:39:32.990Z","modified":"2026-09-11T10:05:27.816Z","0.1.0":"2026-09-09T12:39:33.324Z","0.1.1":"2026-09-09T16:04:57.664Z","0.1.2":"2026-09-10T03:20:43.219Z","0.2.0":"2026-09-11T10:05:27.652Z"},"license":"MIT","keywords":["cordis","deepseek-harness","dsh","plugin","steer","preempt","jobs","wait","output"],"description":"Steer and new-output preemption for dsh: a new user input (next-step steer) or new output from the awaited job interrupts a blocking job_output(wait: true) read immediately instead of letting it block up to the job-wait cap, so the agent loop reaches its ","maintainers":[{"name":"argszero","email":"argszero.reg@gmail.com"}],"readme":"# @argszero/cordis-plugin-steer-preempt\n\nSteer preemption for the DeepSeek Harness (`dsh`). When the agent blocks in a\n**`job_output(wait: true)`** read, two things a model routinely waits for are\n*not* wake sources for the wait:\n\n1. **a new user message** — a *steer* queued as a next-step input — is normally\n   ignored until the wait expires;\n2. **new job output** — a streaming task that prints a progress line still leaves\n   the call blocked for the whole timeout.\n\nThe tool-jobs wait cap is **600,000 ms**, so a user's \"stop, switch tasks\" can\nsit unread for ten minutes, and a \"check the progress\" read returns nothing until\nthe job ends. This plugin makes both **interrupt the blocking wait early** so the\nloop reaches its step boundary and honors the steer — and so the model sees the\noutput it was waiting for. The background job is **never killed**.\n\n## The gaps it closes\n\n`Agent.steer()` (and the session-controller's insert path) sends the input to\n`inbox.nextStep`. The running step never looks at that queue — it is consumed\nonly at the **next step boundary** (`preStep` claim in the agent loop). A\nblocking job read holds the step for up to the wait cap, so the steer is not\nprocessed until the wait expires on its own. See\n[deepseek-ai/deepseek-harness discussion #6030](https://github.com/deepseek-ai/deepseek-harness/discussions/6030).\n\nThe wait's only wake sources are **job settlement** and **its own timeout**, while\nthe call is most often used for exactly the opposite: a mid-run progress check.\nDiscussion\n[#6285](https://github.com/deepseek-ai/deepseek-harness/discussions/6285)\nreports output appearing 0.44 s into a wait whose call still blocks the full\ntimeout.\n\n| Mechanism | Behavior |\n|---|---|\n| `Agent.steer()` / next-step input | only *queues* into `inbox.nextStep`; consumed at the next step boundary |\n| `job_output(wait: true)` | blocks up to the configured cap (max 600,000 ms), honoring `exec.signal` |\n| `Agent.cancel()` | aborts the phase the same tick (too heavy: kills the turn) |\n| **this plugin** | aborts **only the tool-visible derived signal** when a steer is pending or the job has produced output → wait returns early → step boundary arrives → steer is honored / output is read |\n\n## How it works\n\nThis plugin is a **community-side fix** that needs no harness patch. It registers\na `tools/execute` around-wrapper (the same seam the in-tree\n`guard/timeout-policy` uses):\n\n1. Only **blocking** reads (`job_output` with `wait: true`) are wrapped; every\n   other tool call delegates with zero overhead.\n2. For a blocking read, the wrapper swaps in a **derived `AbortSignal`** for the\n   duration of the dispatch (the registry fuses it with the caller's signal, so\n   a real turn cancel keeps its semantics — the caller signal is never touched).\n3. A monitor polls the agent's public `inbox.nextStep` queue (read-only — it\n   **never consumes**; the loop's `preStep` claim owns consumption). When a\n   **user** steer has been pending for a short **settle grace** (default 1 s), it\n   aborts the derived signal. The trigger is a per-message\n   `message.source.kind === 'user'` test, **not** a bare queue-length check: the\n   `nextStep` queue also carries plugin-injected notifications (`agent.inject`,\n   e.g. a job-B completion notice while the model waits on job A, with\n   `source.kind === 'plugin'`). A plugin notice is not user input, so it must\n   not fire a user-steer preemption.\n4. The same monitor watches the **job's output**: the target job's read returning\n   text — held for the same settle grace — arms the output trigger.\n5. The abort makes `jobs.wait` reject (`'wait aborted'`); the wait logic\n   **uncounts the waiter but leaves the job running**. The registry converts the\n   tool throw into an error result.\n6. The wrapper then replaces that error result with a structured\n   `STEER_PREEMPTED` / `JOB_OUTPUT_RESUMED` result telling the model the job is\n   still running — so it finishes its turn and the steer is consumed at the step\n   boundary, or it acts on the output it just received.\n\nA job that settles inside the grace window still returns its **real result**\nthrough the normal path — only an actual abort is ever replaced.\n\n### The output trigger's one real constraint\n\nThe job registry's output cursor is **one-way**: `read` returns whatever was\nproduced since the previous read and moves the cursor, and `onJobsChanged` fires\nonly on visible-set changes (registration, `stopping`, settlement, removal) —\nnever on an append. There is **no non-consuming way to ask whether new output\nexists**.\n\nSo the output trigger *is* the consuming read, which has one consequence worth\nknowing: the delta it observes is **handed to the model** as part of the resumed\nresult. That is not a workaround, it is the property that makes the trigger\nworth having — the moment the plugin notices output, that output is on its way to\nthe model, and nothing is discarded. A wait that observes nothing never reads.\n\nBecause the observation is what fires the trigger, an early return on the **very\nfirst line** of output is normal and cannot be tuned down: there is no\n\"how interesting is this output\" knob, since discarding a delta to keep waiting\nwould throw away the only copy of it.\n\nVerified against `packages/core/tools/src/index.ts`,\n`packages/jobs/jobs-local/src/index.ts`, `packages/jobs/tool-jobs/src/index.ts`\nand `packages/core/agent/src/runtime-types.ts` on\n**dsh 0.1.5-rc.2 / `c291e7961a`**. (v0.1 was verified on 0.1.5-alpha.1; the\nplatform surface is unchanged.)\n\n## Install\n\nMount the plugin in your profile's bundle (see the dsh bundle/preset docs), or\ndrop it into your plugins directory:\n\n```sh\nnpm install @argszero/cordis-plugin-steer-preempt\n```\n\n```yaml\n# cordis.patch.yml overlay\n- insert:\n    - id: steer-preempt\n      name: '@argszero/cordis-plugin-steer-preempt'\n```\n\nOr use the shipped patch by adding the package to your `bundle` dependencies.\n\n## Configuration\n\nNo config is required. Optional tuning (via a `set` layer):\n\n| Field | Default | Meaning |\n|---|---|---|\n| `pollMs` | `200` | Poll interval for the pending next-step queue and the observed output while a blocking read is active |\n| `graceMs` | `1000` | Settle grace: after a trigger is observed, how long to let a nearly-done job finish before aborting |\n| `deliver` | `'wrap'` | How the output trigger delivers its early return: `'wrap'` (structured `JOB_OUTPUT_RESUMED` carrying the observed delta), `'cut'` (leave the tool's own early-return result), `'off'` (disable the output trigger) |\n| `resumeTimeoutMs` | `5000` | Bounded wait re-armed in `'wrap'` mode after an output resume, so the model resumes with a bounded wait instead of the full original budget. Never exceeds the caller's own `timeout_ms` |\n\nThe **steer** trigger is unaffected by `deliver`: a user message always wins and\nalways produces `STEER_PREEMPTED`.\n\n## Changelog\n\n- **v0.2.0**: output trigger (#6285). A blocking `job_output(wait: true)` now\n  returns early when the job produces output, not only when it settles or times\n  out. The observed delta is handed to the model in a structured\n  `JOB_OUTPUT_RESUMED` result (delivery mode is explicit — see `deliver`), the\n  job keeps running, and a settle inside the grace window still wins. There is\n  deliberately no \"how interesting is this output\" knob: a sensitivity threshold\n  could only be honored by consuming a delta and discarding it, which is strictly\n  worse than handing the model the text it was waiting for.\n- **v0.1.2**: peer range fix. `>=0.1.2` matched **no published version** — every dsh\n  release is a prerelease and a semver comparator only admits prereleases sharing its\n  own `major.minor.patch` tuple, so installation failed outright with `ETARGET`. The\n  range is now `>=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0` (one comparator per\n  supported tuple line — the single-comparator form still rejects the `0.1.5` line with\n  `ERESOLVE`). A `test/peer-range.test.js` regression prevents both forms from\n  returning.\n- **v0.1.1**: preemption trigger narrowed from `nextStep.length > 0` to a\n  per-message `message.source.kind === 'user'` test. Fixes a false preemption\n  when a plugin-injected notification (`agent.inject`, e.g. a job-B completion\n  notice with `source.kind === 'plugin'`) sits in the same `nextStep` queue as a\n  real user steer. Regression tests added. (Community feedback, discussion\n  #6030.)\n- **v0.1.0**: initial release — preempt a blocking `job_output(wait: true)` read\n  when a user steer is queued.\n\n## What it does not do\n\n- It does **not** kill or cancel the background job — the job keeps running and\n  a later `job_output` read returns its state.\n- It does **not** consume the steer — the loop's step boundary still owns that.\n- It does **not** replace a successful wait result — only an abort-shaped error\n  that this plugin's own trigger caused.\n- It does **not** re-arm the tool's own wait after a resume — a `'wrap'` resume\n  reports the wait's remaining budget so the model can decide whether to keep\n  waiting, and the job's completion notice still arrives on its own.\n- It targets `job_output(wait: true)` only (the reported gaps). Other tools that\n  honor `exec.signal` can be added later.\n\n## Relationship to in-tree guards\n\n| Guard | Hook | Catches |\n|---|---|---|\n| `guard/timeout-policy` | `tools/execute` | a tool call exceeding a declared `timeoutMs` |\n| `guard/repeat-tool-reminder` | `tools/post-execute` | the model repeating the same tool-call chain |\n| **this plugin** | `tools/execute` | a **blocking wait** that a user steer (or new job output) should preempt |\n\n## License\n\nMIT\n","readmeFilename":"README.md"}