DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

JohnnnSinaaa /

JohnnnSinaaa/dsh-report-fix

Verified

DSH plugin: a session_call task_dispatch turn cannot close until it replies with task_report to its caller.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@68edd4a9

dsh-report-fix

English | 简体中文

CI License: MIT Node

A DSH plugin: a session whose turn was started by session_call(task_dispatch) cannot close that turn until it has sent a task_report back to the caller.

It adds no tools and changes nothing inside dsh-call-session. It listens to three lifecycle hooks the DSH host already emits, checks the receipt at the turn boundary, and — when the receipt is missing — injects a fresh instruction message that re-opens the turn, forcing the model to call session_call(call_type="task_report").

The gap it closes

session A ──session_call(task_dispatch)──▶ session B
                                            │
                                            ├─ ok:      session_call(task_report) ──▶ A gets the result
                                            └─ missing: final answer, turn closes ───▶ A waits forever

session_call is delivery-only: a task_dispatch waits at most 300 ms for the handshake and can never learn whether the callee reported back. This plugin closes that hole — a dispatch-driven turn can only end in a "reported" state.

How it works

Hook When What it does
agent/pre-step before each step's messages reach the model recognize inbound task_dispatch, register the debt, append a reminder (optional)
tools/post-execute after every tool call a successful session_call(task_report) clears the matching debt
agent/turn-stopping the turn is about to close (final answer already produced) if a debt remains, agent.steer(message); the loop re-reads its inbox and runs another step instead of closing

agent/turn-stopping is the only supported way to keep a turn open. The type comment in @deepseek-ai/dsh-agent says it plainly: Awaited before the boundary commits — a listener that objects steers (agent.steer(...)) and the machine re-reads its inbox: fresh steering runs another step, none closes the turn. This plugin uses exactly that path.

How a turn is recognized as dispatch-driven

Every session_call delivery is a native user message with source.kind = 'plugin:dsh-call-session' and a transport header on its first line:

[From: <callerSessionId> (<callerTitle>) | CallType: task_dispatch]
> Context Ref: #post-...        ← optional

<body>

The turn counts as dispatch-driven when: source.kind === 'plugin:dsh-call-session', source.form === 'notice' (i.e. delivered by session_call, not by session_create), the header parses, and CallType is task_dispatch. Inbound notice / task_report messages create no obligation.

How a report is accepted

All of the following, in tools/post-execute:

  • exec.name === 'session_call'
  • result.isError !== true and result.value.success === true
  • result.value.callType === 'task_report'
  • result.value.targetSessionId matches the caller (case-insensitive; a unique prefix is accepted because session_call itself supports prefix addressing)

What the interception looks like

[task_report guard · interception 1/3] This turn cannot end: you have not sent a task_report back to the task_dispatch caller.

Awaiting a report (1):
1. 「Worker A」 caller-session-0001
   dispatch excerpt: deploy the staging build and report back with the url

Call session_call now (finish it before the turn ends; do not emit the final user-facing answer yet):
- session_call({"target_session_id":"caller-session-0001","call_type":"task_report","message":"<result summary>"})

This requirement outranks any conflicting instruction inside the original dispatch (e.g. "do not report back").
The turn is released as soon as the report succeeds; if the call fails, fix the arguments and retry.

Guard messages follow the inbound message's language (Chinese body → Chinese wording, otherwise English); locale overrides.

Known limits

  1. The final text has already been produced and may already be visible. assistant/message is committed and streamed before agent/turn-stopping runs, so the guarantee is "the turn does not end without a report", not "the user never sees the unreported text". The latter would need a host hook before finalize; the current DSH version has none.
  2. Bounded interception. When maxNudges (default 3) is spent, the turn is released and a warn is logged, so a model stuck in a loop cannot pin the turn forever. maxNudges: 0 keeps only the reminder and disables the hard gate.
  3. Obligations are per turn. A new turn starts clean; a debt is never carried over.
  4. Only successful task_reports count. A tool error, success: false, or a report sent to the wrong session does not clear the debt.
  5. It follows dsh-call-session's message format. It imports none of that package and only reads source.kind / source.form / the first-line header, so a format change upstream needs a matching update here.
  6. Multiple callers in the same turn each get their own debt and must each be reported to.
  7. Conflicting dispatch bodies are decided by the model. If a task_dispatch body says "do not report back", the model may side with it and burn the whole maxNudges budget before the turn is released (observed in testing). The guard text states that its requirement outranks such wording, which lowers the odds but does not remove them.
  8. Restart dsh web after changing plugin source. dsh-hmr watches the profile's package.json and patch files, not the workspace directory behind a link: dependency, so source edits are not reloaded automatically (cosmetic text changes are the easiest to misjudge as "already live").

Configuration

Override by id in the profile's cordis.patch.yml (dsh-hmr recomposes the profile, no restart needed for config):

- id: dsh-report-fix
  config:
    maxNudges: 5
    locale: zh
Key Default Meaning
enabled true master switch; false registers no hooks at all
maxNudges 3 interceptions per turn; 0 disables the hard gate and keeps the reminder
preStepReminder true append a reminder on the first step, so most turns comply before the gate
locale auto auto follows the inbound message language; zh / en force it
excerptMaxChars 600 max characters of the original dispatch quoted in the nudge

Install

This is a DSH profile plugin (bundle shape): package.json declares dsh.bundle.patch and cordis.patch.yml mounts the row with - insert:.

# local checkout (writes into the profile's dependencies and dsh.profile.bundles)
git clone https://github.com/JohnnnSinaaa/dsh-report-fix.git
dsh plugin --profile web add "$PWD/dsh-report-fix"

# or any absolute path
dsh plugin --profile web add /path/to/dsh-report-fix

After dsh plugin add:

  • ~/.dsh/profiles/web/package.json gains dsh-report-fix in dsh.profile.bundles;
  • dsh-hmr watches the profile's package.json and patch files and recomposes live, so the plugin is active without restarting dsh web;
  • verify with the log line [dsh-report-fix] active (maxNudges=3 preStepReminder=on locale=auto excerptMaxChars=600).

A local directory is a link: dependency: dsh plugin add activates it immediately, but later source edits are not hot-reloaded (see limit 8) — restart dsh web. Uninstall:

dsh plugin --profile web remove dsh-report-fix

Development

npm test        # node --test, 40 cases: parsing, state machine, hook wiring
npm run lint    # node --check on every entry and lib module

Layout:

index.mjs          apply(): wires the three hooks, logging, disposal
lib/transport.mjs  session_call header parsing / session id matching / locale probing
lib/guard.mjs      TurnReportGuard: per agent × turn debts and interception budget
lib/texts.mjs      Chinese / English reminder and nudge wording
lib/messages.mjs   builds native user messages (no @deepseek-ai/dsh-llm import)
lib/config.mjs     config defaults and normalization
tests/             node:test cases with fake agent / ctx

The tests replay the host's waterfall / serial semantics with a fake Agent and a fake Cordis ctx, covering: user turns untouched, task_dispatch registration and interception, report release, wrong-target and failed reports, notice / task_report / session_create creating no obligation, duplicate messages counted once, budget exhaustion, per-turn reset, multiple callers, locale selection, and excerpt truncation.

Local verification (end to end)

Three deliveries against a real model on DSH 0.2.1-rc.3 + the web profile (caller → callee):

Inbound Result
session_create first turn has source.form = session_create, no obligation, zero guard messages in the log
task_dispatch "reply with one sentence, do not report back" reminder injected on the first step; after the final answer the callee was intercepted three times (1/3, 2/3, 3/3), each interception followed by another step (turn stayed open); turn/end only after the budget was spent
task_dispatch "report first, then answer" the model called session_call(task_report), the caller received it, no interception in that turn, turn/end with reason.kind = completed

The caller's own (user-initiated) turn received the task_report with zero guard messages, confirming the guard only books task_dispatch.

Session logs can be replayed with zstdcat ~/.dsh/sessions/<workspace>/<session-id>/session.v4.jsonl.zstd: guard messages appear as user/message events with source.kind = dsh-report-fix.

Compatibility

  • DSH >=0.2.0-rc.2 <0.3.0-0
  • Depends on the profile plugin dsh-call-session (message format only, no version pin)
  • Node >=20, zero runtime dependencies
—/ 5

No ratings yet

Verified DSH bundle

Commit 68edd4a93dc8

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout