Repository navigation
Replies: 2 comments 9 replies
|
I traced the
The persistence qualification matters: appending is not the same as flushing. The standard top-level tool checkpoint is in A focused P1 test would hold that approval unresolved, explicitly flush the session, kill the process, then assert both the recovered code and a provider invocation count of zero. It would isolate the transcript gap without adding cross-process continuation. I'm contributing from Mission, where we're testing similar restart boundaries. Your distinction between truthful history and continuing the old turn is useful here; the source trace above is only addressing P1. |
|
For the budget-owner case, one approach is to keep the approval out of the runtime entirely. The agent writes a durable request with its own id: amount, payee, reason. The owner approves it in their own tool or wallet, hours later. A signed webhook resumes the right task after any number of restarts, because the state lives in the request, not the process. I'm with send21, which does this for payments: the agent's key can only create the request, and a person signs it. Does the harness let a webhook rehydrate a paused tool call? send21 |
Uh oh!
There was an error while loading. Please reload this page.
What we are building
We run DeepSeek Harness server-side: one long-lived runtime process per tenant, many human users behind it. The agent does business work, and that work regularly needs a decision from a person who is not the person in the thread: a budget owner, a data owner, a manager. Those decisions come back in hours or days, and the process restarts on every deploy in between.
This is not the sandbox-escalation case. That one is correctly synchronous and should stay that way. This is about a business decision whose answer is slow by nature.
Everything below was checked against
0.1.5-rc.2(latest).What we need
Two properties. The first is the one we are actually asking about.
P1. A resumed transcript should tell the truth about a gated action.
A tool call that was waiting for a human decision did not perform its action. If a resumed session cannot distinguish "was gated, never ran" from "ran, outcome not recorded", then a model reading its own history cannot tell whether a payment was sent or an email went out. That ambiguity is the opposite of what the gate exists to produce.
P1 alone unblocks us. With a truthful transcript we can deliver the late decision ourselves: resume the session, state that the decision arrived, and let the agent proceed.
P2. A deployment should be able to deliver a decision after the asking process is gone.
Ideally without the agent having to re-derive the step. We are not requesting this one. We can see that it implies continuing an interrupted turn, and that is explicitly out of scope today (quoted below). We mention it only so the shape of the need is visible.
What we found
Both human-interaction seams are bounded by the turn, and the turn is bounded by the process.
ctx.userQuestions(@deepseek-ai/dsh-user-questions0.1.5-rc.2) exposesask(request): Promise<AskUserQuestionAnswer>, described as the service used "when it needs to pause work and ask the human for a decision".ask_user_question(@deepseek-ai/dsh-tool-ask-user0.1.5-rc.2), Known Limitations:approval/request(@deepseek-ai/dsh-user-approval0.1.5-rc.2) is a waterfall returningPromise<ApprovalOutcome>, Known Limitations:While a request is pending, this is what exists and where:
approval/askedPromise<ApprovalOutcome>AbortSignalbounding itapproval/decidedNothing bounds the wait:
@deepseek-ai/dsh-agent-loop0.1.5-rc.2 lists "No built-in turn budget".ctx.jobsis not an escape either,@deepseek-ai/dsh-jobs0.1.5-alpha.1:And the durable half we would build on is deliberately limited,
@deepseek-ai/dsh-session-persistence0.1.5-rc.2:We read that as a real boundary, which is why P2 is not a request.
What we can do ourselves
We want to be clear about which half is ours, because most of it is.
Routing a question to the right approver, holding it durably, rendering it, surviving a browser reload, and letting a different person answer are all ours. We compose our own terminal answerer and our own surface, and we have to: the shipped Web answerer follows the session's live client scope (
dsh-user-approval: "agent-scoped listeners receive only that agent";dsh-user-questions: "Remote Events route the shipped Web answerer only when the request carries a live Agent scope"), and a delegated approver is not in that scope.What we have today is an answerer that refuses to wait: it records the request in PostgreSQL with an assignee, returns a terminal outcome immediately, and lets the turn close. A worker resumes the session once a human answers. This works, and we are not blocked. What it costs is that the recorded outcome is not the real one, and the resumed agent has to be told out of band what actually happened, which pollutes the transcript it later reasons over.
What we could not determine
@deepseek-ai/dsh-session/repairexports two recovery codes:TOOL_NOT_STARTED, "an assistant tool request that never reached a recorded call start"TOOL_OUTCOME_UNKNOWN, "a recorded tool call whose completed outcome was not durably recorded"@deepseek-ai/dsh-session-checkpoint-policy0.1.5-rc.2 says "A top-level tool call is flushed before the tool body runs", anddsh-user-approvaldecides "before a sensitive tool action proceeds". From the published types we could not tell where the gate sits relative to the recorded call start, and therefore which code a gated call receives after a crash.If a gated call already receives
TOOL_NOT_STARTED, then P1 is satisfied today and we would simply like to know that, and how a deployment is meant to read it. If it receivesTOOL_OUTCOME_UNKNOWN, then a provably unexecuted action is reported as possibly executed, and that is the gap we are asking about.Shapes we considered, and are not requesting
Recording these only to show what we weighed, not to prescribe. Each has an objection we can see ourselves.
ApprovalOutcomemeaning "answered later". Rejected on compatibility:ApprovalOutcomeis consumed by the tools pipeline and by every composed answerer, and widening it would silently reclassify a new value as a denial in existing plugins.We would rather build against whatever shape you consider right than around it.
This is not only our problem
From
dsh-plugin-catalog2026.917.3416 (3727 plugins, snapshot 2026-09-16), plugins touching approvals fall into exactly two families:dsh-approval-llm,dsh-auto-approve,dsh-yolo-mode,dsh-approve-for-me,dsh-perm-guard, ...).notifyandremoteforward the card to Telegram, Feishu, WeChat, Slack, Discord or a phone app so the human answers faster (dsh-im-gateway,dsh-reach,dsh-lark-bridge, ...).Both families work around the same limit from opposite sides: the human must answer now, so either remove them or hurry them. Nobody solved it, which suggests it is not solvable from a plugin.
Related and distinct: #5236 asks which answerer owns a request, not how long one may live, and raises the deadline question. #4708 carries a request across a process boundary to an embedding host, but the host long-polls and "the answer releases the tool call, an approved
exit_plan_modecontinues the same turn", so it remains one live turn in one live process. #5147 reports an agent stuck when the policy changes while an escalation is pending, which is the same in-memory pending state seen from another side.Questions
Happy to prototype whichever shape you prefer, or to run and report a reproduction if that would help answer the first question.
All reactions