Background subagent parks its turn with no completion notice to the parent (parent must poll list_agents)
#5360
Replies: 4 comments
|
Good report — I traced the full mechanism in current master (4e84901 = 0.1.2-alpha.4) and the symptom is real, though the exact wiring drifted since your rc.2 citations. Here is the verified map, then the two gaps I think are actually worth fixing, and a diagnostic note for your reproductions. Verified mechanism (alpha.4)
Two genuine gaps
Diagnostic note for your reproductions If the manager had fabricated the reviewer (tool call never succeeded), its Fix assessment
A PR for the park notice would be very welcome; happy to review the diff. Thanks for the clean write-up — and if you capture the descendant listing next time, it would confirm whether the recursive-park case is the dominant one. |
|
Implemented Option 2 from your analysis — the parent-visible park notice on the Summary:
Verification: From 1358eeaffb7bf242919ab8ac12c2532150a65c81 Mon Sep 17 00:00:00 2001
From: nathanlively <nathan@squarewave.com.au>
Date: Tue, 1 Sep 2026 18:29:23 -0500
Subject: [PATCH] feat(subagent): notify the parent when a continuable child
parks in waiting
A continuable background child that spawns its own continuable
descendants parks in a `waiting` residency state when its turn ends
while those descendants are still running. Nothing fired on the
running->waiting transition, so the parent received no signal between
"child dispatched" and "child settled" and had to poll list_agents.
watchSettlement now observes that transition and emits one deduped
user-role notice (`subagent-waiting` source kind, form `notice`)
via the new notifyParked, sharing the settlement delivery policy
extracted as deliverParentNotice. The notice is reported once per
Activation. Closes the observability gap from discussion #5360.
diff --git a/.agents/notes/implemented/bug-fix/2026-09-01-subagent-waiting-park-notice.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-01-subagent-waiting-park-notice.i18n.yaml
new file mode 100644
index 0000000..047275a
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-09-01-subagent-waiting-park-notice.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-01-subagent-waiting-park-notice.md
+2026-09-01-subagent-waiting-park-notice.md: d3d564c04d8a2b53345a97d9cec64a4b9a1adbfd
+2026-09-01-subagent-waiting-park-notice.zh.md: 3567f3367798a05897850842cf2c6c31ab9ce5f2
diff --git a/.agents/notes/implemented/bug-fix/2026-09-01-subagent-waiting-park-notice.md b/.agents/notes/implemented/bug-fix/2026-09-01-subagent-waiting-park-notice.md
new file mode 100644
index 0000000..d3d564c
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-09-01-subagent-waiting-park-notice.md
@@ -0,0 +1,34 @@
+# Agent Note: Report a continuable child parking in `waiting` to its parent
+
+Status: implemented
+
+English | [中文](2026-09-01-subagent-waiting-park-notice.zh.md)
+
+## Problem
+
+A continuable background child that spawns its own continuable descendants enters a `waiting` residency state when its turn ends while those descendants are still running. Nothing fired on the running→waiting transition, so the parent received no signal between "child dispatched" and "child settled". `list_agents` collapses the state to `idle`, whose own contract admits the ambiguity (a resident Agent between turns, possibly waiting on agents it started), so a supervisor could not distinguish a child productively waiting on its descendants from a stalled one without polling.
+
+## Decision
+
+`SubagentContinuationManager` tells the durable direct parent when a child first parks in `waiting`. `watchSettlement` observes the running→waiting transition and calls the new `notifyParked`, which emits one user-role notice — `Background subagent <id> is waiting on its subagents (<ids>) and will report when they settle.` — sourced as a new `subagent-waiting` message source kind with form `notice`, parallel to the existing `subagent-settled` kind. The notice is reported once per Activation: `waitingNotified` is set on the first observation and never cleared, so a child that wakes and parks again is not re-reported.
+
+Delivery reuses the settlement path, extracted as `deliverParentNotice`: a parent whose teardown began is injected without a wake, an idle parent is woken, a busy parent is steered, a parent that left the registry is skipped, and a rejected send is logged without blocking disposal. The message construction stays in each `notify*` method because the two notices differ in content; only the delivery policy is shared.
+
+## Alternatives considered
+
+**Hold the turn open until the descendants settle.** Not implementable as stated: the agent loop cannot force the model to keep producing. Its useful variant — auto-resume the parked manager with each descendant's settlement summary — is a larger behavior change with loop risk and belongs in its own discussion.
+
+**Re-report every running→waiting transition.** Clearing `waitingNotified` on each waking delivery would emit a second notice after every interim turn. The first park is the signal the parent needs; later parks add no information.
+
+**Expose the state through `list_agents` only.** Fixing the gap at the transition benefits every consumer and keeps the ambiguous `idle` reading unchanged rather than growing a new vocabulary in the list entry.
+
+## Consequences
+
+- The parent now receives one signal per child lifetime that the child is working through its descendants, closing the gap that forced polling. The eventual settlement notice still closes the loop.
+- `subagent-waiting` is a merge-extensible `MessageSourceMap` entry, so consumers that do not recognize it fall through the documented default and present it as opaque content; no switch on the closed set of notice kinds breaks.
+- A park summary naming many children is bounded by the existing notice summary cap (`boundContextSummary`); the full child list remains in the message content.
+- `deliverParentNotice` consolidates the two parent-facing runtime accounts under one delivery policy. Its branches were already covered by the settlement-delivery tests; the new park wording and the dedup are pinned by a unit test that parks a child, wakes it, and asserts the parent still has exactly one notice.
+
+## Related
+
+The shared delivery policy and its ordering rules come from [settlement delivery](../feature/2026-08-06-manager-owned-subagent-settlement-delivery.md).
diff --git a/.agents/notes/implemented/bug-fix/2026-09-01-subagent-waiting-park-notice.zh.md b/.agents/notes/implemented/bug-fix/2026-09-01-subagent-waiting-park-notice.zh.md
new file mode 100644
index 0000000..3567f33
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-09-01-subagent-waiting-park-notice.zh.md
@@ -0,0 +1,34 @@
+# Agent Note: 向父代理报告 continuable 子代理进入 waiting 停车状态
+
+Status: implemented
+
+[English](2026-09-01-subagent-waiting-park-notice.md) | 中文
+
+## 问题
+
+当一个 continuable 后台子代理派生出自己的 continuable 后代时,它会在自己的回合结束、而这些后代仍在运行的那一刻进入 `waiting` 常驻状态。running→waiting 的转变没有触发任何信号,因此父代理在「子代理已派出」与「子代理已结算」之间收不到任何通知。`list_agents` 把这个状态折叠为 `idle`,而该字段自身的契约也承认这种歧义(一个处于回合之间的常驻 Agent,可能正在等待它派生的子代理),于是监督者无法在不轮询的情况下区分「正在通过子代理正常工作」与「停滞」。
+
+## 决策
+
+`SubagentContinuationManager` 会在子代理首次停入 `waiting` 时告知其持久化的直接父代理。`watchSettlement` 观察到 running→waiting 的转变后调用新增的 `notifyParked`,它发出一个 user 角色通知——`Background subagent <id> is waiting on its subagents (<ids>) and will report when they settle.`——其来源是新增的 `subagent-waiting` 消息来源 kind,form 为 `notice`,与已有的 `subagent-settled` kind 并列。每个 Activation 只报告一次:`waitingNotified` 在首次观察到时置位且永不清除,因此被唤醒后再次停车的子代理不会被重复报告。
+
+投递复用结算路径,抽取为 `deliverParentNotice`:已经开始销毁的父代理以注入方式送达且不唤醒,空闲的父代理被唤醒,忙碌的父代理被 steer,已离开注册表的父代理被跳过,投递失败则记录日志而不阻塞销毁。消息构造保留在各个 `notify*` 方法中,因为两种通知的内容不同;只有投递策略是共享的。
+
+## 考虑过的替代方案
+
+**保持回合开启直到后代结算。** 按现有表述无法实现:agent loop 不能强迫模型持续产出。其有用的变体——在每个后代的结算摘要到达时自动恢复被停车的管理者——是更大的行为变更,带有循环风险,应当单独讨论。
+
+**每次 running→waiting 转变都重新报告。** 在每次唤醒投递时清除 `waitingNotified` 会在每次临时回合后发出第二个通知。首次停车才是父代理需要的信号;之后的停车不带来任何新信息。
+
+**只通过 `list_agents` 暴露状态。** 在转变处修复该缺口能让每个消费方受益,并且保持含糊的 `idle` 读取不变,而不是为列表条目增加一套新词汇。
+
+## 后果
+
+- 父代理在子代理的整个生命周期中会收到一个信号,表明该子代理正在通过其后代工作,从而弥补了迫使轮询的缺口。最终的结算通知仍会闭合整个循环。
+- `subagent-waiting` 是 merge-extensible 的 `MessageSourceMap` 条目,因此不认识它的消费方会落入已文档化的默认路径,将其呈现为不透明内容;没有任何针对通知 kind 封闭集合的 switch 会被破坏。
+- 命名多个子代理的停车摘要受既有通知摘要上限约束(`boundContextSummary`);完整的子代理列表仍保留在消息内容中。
+- `deliverParentNotice` 把两种面向父代理的运行时说明统一到同一个投递策略下。它的分支已由结算投递测试覆盖;新的停车措辞与去重由一个单元测试锁定——让子代理停车、唤醒它,再断言父代理仍然只有一条通知。
+
+## 相关
+
+共享的投递策略及其顺序规则来自[结算投递](../feature/2026-08-06-manager-owned-subagent-settlement-delivery.zh.md)。
diff --git a/packages/subagent/subagent/src/continuation.ts b/packages/subagent/subagent/src/continuation.ts
index 2103ce2..8315405 100644
--- a/packages/subagent/subagent/src/continuation.ts
+++ b/packages/subagent/subagent/src/continuation.ts
@@ -81,10 +81,27 @@ export interface SubagentSettledMessageSource {
readonly senderSessionId: SessionId
}
+/**
+ * Durable attribution for the runtime's account of a continuable child parking
+ * in `waiting`: its turn ended while descendants it owns are still running.
+ * A different kind from {@link SubagentSettledMessageSource} so a consumer can
+ * tell "still working through its children" from "finished" without polling.
+ */
+export interface SubagentWaitingMessageSource {
+ readonly kind: 'subagent-waiting'
+ /** A runtime account shown without expanding the row (`notice` context form). */
+ readonly form: 'notice'
+ /** One-line account of what the child is waiting on. */
+ readonly summary: string
+ /** Session id of the child that parked. */
+ readonly senderSessionId: SessionId
+}
+
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'agent-message': AgentMessageSource
'subagent-settled': SubagentSettledMessageSource
+ 'subagent-waiting': SubagentWaitingMessageSource
}
}
@@ -224,6 +241,12 @@ interface Activation {
* not exist, so its teardown owes the parent no settlement account.
*/
announced: boolean
+ /**
+ * Whether this Activation already told the parent it parked in `waiting`.
+ * Set on the first running→waiting observation and never cleared, so a child
+ * that wakes and parks again is not re-reported on every ownership wake.
+ */
+ waitingNotified: boolean
/** Renewed whenever a settlement watcher must re-observe quiescence. */
poke: PromiseWithResolvers<void>
}
@@ -338,9 +361,21 @@ function settlementSummary(childId: SessionId, stopReason: SubagentResult['stopR
}
}
+/**
+ * One line telling a parent that a background child is parked waiting on the
+ * descendants it spawned, in the parent's own task vocabulary.
+ * @param childId - the durable child the parent knows by id.
+ * @param children - the owned child ids this child is still waiting on.
+ * @returns the model-facing line of the waiting notice.
+ */
+function parkSummary(childId: SessionId, children: readonly SessionId[]): string {
+ const subject = `Background subagent ${childId}`
+ return `${subject} is waiting on its subagents (${children.join(', ')}) and will report when they settle.`
+}
+
/** Whether one settlement attempt opened the disposal transaction. */
type SettlementAttempt =
- | { readonly settling: false }
+ | { readonly settling: false; readonly state: ActivationState | undefined }
| { readonly settling: true; readonly done: Promise<void> }
/** Serialize each durable child's delivery, release, and disposal. */
@@ -1169,6 +1204,7 @@ export class SubagentContinuationManager {
disposal: undefined,
accepted: new Set(),
announced: false,
+ waitingNotified: false,
poke: Promise.withResolvers<void>(),
}
// After transfer, any failure must dispose the created handle, remove the
@@ -1377,8 +1413,12 @@ export class SubagentContinuationManager {
// Deciding outside the lock would let a delivery observe a not-yet
// resident handle that this watcher is already about to tear down.
const settling = await this.locks.run<SettlementAttempt>(activation.childId, () => {
- if (disposalOf(activation) !== undefined || this.stateOf(activation) !== 'settled') {
- return Promise.resolve({ settling: false })
+ if (disposalOf(activation) !== undefined) {
+ return Promise.resolve({ settling: false, state: undefined })
+ }
+ const state = this.stateOf(activation)
+ if (state !== 'settled') {
+ return Promise.resolve({ settling: false, state })
}
// `dispose()` assigns its memoized transaction synchronously, so
// admission is closed before this critical section releases.
@@ -1387,6 +1427,7 @@ export class SubagentContinuationManager {
if (!settling.settling) {
// Still running, or waiting on descendants: re-observe after the next
// accepted message or ownership release.
+ if (settling.state === 'waiting') this.notifyParked(activation)
if (activation.handle.agent.status !== 'running') await poked
continue
}
@@ -1521,37 +1562,77 @@ export class SubagentContinuationManager {
* need it — a token ceiling, a model failure, cancellation, teardown — are
* exactly the ones where the child never got to choose. A materialization
* rolled back before its first acceptance stays silent, since the caller was
- * told that child was not established. A parent that is no longer live is not
- * an error; the child's own Session remains the durable record either way.
- * A parent whose own lineage is already closing receives the notice without a
- * wake, because teardown is not a reason to start a turn.
- *
- * Never blocks disposal. A delivery failure is logged and dropped, because
- * retaining a child to retry a notice would pin its whole ancestry in
- * `waiting` forever.
+ * told that child was not established.
* @param activation - the settling Activation, still owned by its parent.
* @param terminal - how this epoch ended, as the terminal edge will report it.
*/
private notifySettlement(activation: Activation, terminal: ActivationTerminal): void {
if (!activation.announced) return
+ const summary = settlementSummary(activation.childId, terminal.stopReason)
+ const message = createUserMessage({
+ content: [
+ { type: 'text' as const, text: summary },
+ ...terminal.output === undefined
+ ? [{ type: 'text' as const, text: 'It left no closing message.' }]
+ : [{ type: 'text' as const, text: 'Its closing message:' }, ...terminal.output],
+ ],
+ source: {
+ kind: 'subagent-settled' as const,
+ form: 'notice' as const,
+ summary: boundContextSummary(summary),
+ senderSessionId: activation.childId,
+ },
+ })
+ this.deliverParentNotice(activation, message, 'settlement notice')
+ }
+
+ /**
+ * Tell the durable direct parent that this child's turn ended while the
+ * descendants it spawned are still running. Reported once per Activation:
+ * the first running→waiting observation is the signal the parent needs to
+ * distinguish "working through its children" from "stalled", and a later
+ * park after a waking turn adds no new information. Reached only after the
+ * child ran a turn, so no `announced` guard is needed.
+ * @param activation - the Activation that just entered `waiting`.
+ */
+ private notifyParked(activation: Activation): void {
+ if (activation.waitingNotified) return
+ activation.waitingNotified = true
+ const summary = parkSummary(activation.childId, [...activation.ownedChildren])
+ const message = createUserMessage({
+ content: [{ type: 'text' as const, text: summary }],
+ source: {
+ kind: 'subagent-waiting' as const,
+ form: 'notice' as const,
+ summary: boundContextSummary(summary),
+ senderSessionId: activation.childId,
+ },
+ })
+ this.deliverParentNotice(activation, message, 'waiting notice')
+ }
+
+ /**
+ * Deliver one runtime account to the durable direct parent. A parent that is
+ * no longer live is not an error; the child's own Session remains the durable
+ * record either way. A parent whose own lineage is already closing receives
+ * the notice without a wake, because teardown is not a reason to start a
+ * turn.
+ *
+ * Never blocks disposal. A delivery failure is logged and dropped, because
+ * retaining a child to retry a notice would pin its whole ancestry in
+ * `waiting` forever.
+ * @param activation - the child whose parent receives the account.
+ * @param message - the built user-role notice to deliver.
+ * @param label - the notice noun used in the non-delivery warning.
+ */
+ private deliverParentNotice(
+ activation: Activation,
+ message: ReturnType<typeof createUserMessage>,
+ label: string,
+ ): void {
try {
const parent = this.ctx.agents.get(activation.parentSession)
if (parent === undefined) return
- const summary = settlementSummary(activation.childId, terminal.stopReason)
- const message = createUserMessage({
- content: [
- { type: 'text' as const, text: summary },
- ...terminal.output === undefined
- ? [{ type: 'text' as const, text: 'It left no closing message.' }]
- : [{ type: 'text' as const, text: 'Its closing message:' }, ...terminal.output],
- ],
- source: {
- kind: 'subagent-settled' as const,
- form: 'notice' as const,
- summary: boundContextSummary(summary),
- senderSessionId: activation.childId,
- },
- })
// A parent whose own teardown already began must not be woken. Waking is
// not a queue operation: `followup()` on a quiescent Agent starts a turn,
// and `cancel()` does not arm against a later one, so a notice arriving
@@ -1577,7 +1658,7 @@ export class SubagentContinuationManager {
})
} catch (error: unknown) {
this.ctx.logger.warn(
- `subagent "${activation.childId}" settlement notice was not delivered to its parent: `
+ `subagent "${activation.childId}" ${label} was not delivered to its parent: `
+ errorChain(error),
)
}
diff --git a/packages/subagent/subagent/tests/continuation.spec.ts b/packages/subagent/subagent/tests/continuation.spec.ts
index 851720f..19cc5f9 100644
--- a/packages/subagent/subagent/tests/continuation.spec.ts
+++ b/packages/subagent/subagent/tests/continuation.spec.ts
@@ -13,7 +13,7 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
import type { ContentBlock, GenerateOptions, MessageId, StreamChunk } from '@deepseek-ai/dsh-llm'
-import { ToolCallId, createUserMessage, LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
+import { ToolCallId, boundContextSummary, createUserMessage, LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import { defineTool } from '@deepseek-ai/dsh-tools'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -915,6 +915,52 @@ describe('continuable child ownership', () => {
// The top-level parent remains independently registered after its child settles.
expect(ctx.agents.get(parent.id)).toBe(parent)
})
+
+ it('tells the parent once when a child parks waiting on its descendants', async () => {
+ const releaseGrandchild = Promise.withResolvers<undefined>()
+ const adapter = new GatedAdapter([
+ // The child delegates, then finishes its own turn while the grandchild runs.
+ { chunks: textResponse('child done') },
+ { chunks: textResponse('grandchild'), gate: releaseGrandchild.promise },
+ { chunks: textResponse('woken') },
+ ])
+ const { ctx, parent } = await setupWith(adapter)
+ const started = await ctx.subagents.startContinuable(startSpec(parent))
+ const child = await vi.waitFor(() => {
+ const found = ctx.agents.get(started.childId)
+ expect(found).toBeDefined()
+ return found!
+ })
+ const grandchild = await ctx.subagents.startContinuable(startSpec(child))
+ await vi.waitFor(() => {
+ expect(child.status).toBe('idle')
+ expect(ctx.agents.get(started.childId)).toBe(child)
+ }, { timeout: 5_000 })
+
+ // The child is now waiting on its grandchild: the parent is told once.
+ await vi.waitFor(() => { expect(waitingNotices(parent)).toHaveLength(1) })
+ const expected = `Background subagent ${started.childId} is waiting on its subagents `
+ + `(${grandchild.childId}) and will report when they settle.`
+ const notice = waitingNotices(parent)[0]!
+ expect(notice.sender).toBe(started.childId)
+ expect(notice.text).toBe(expected)
+ // The collapsed-row account bounds the child list to the notice summary cap.
+ expect(notice.summary).toBe(boundContextSummary(expected))
+
+ // A waking delivery returns the child to running, and it parks again: the
+ // second park is not re-reported.
+ await queuePrompt(ctx, parent, started.childId, message('while waiting'))
+ await vi.waitFor(() => {
+ expect(adapter.requests.filter(request => request.sessionId === started.childId)).toHaveLength(2)
+ })
+ await vi.waitFor(() => { expect(child.status).toBe('idle') }, { timeout: 5_000 })
+ expect(waitingNotices(parent)).toHaveLength(1)
+
+ releaseGrandchild.resolve(undefined)
+ await waitNoActivation(ctx, grandchild.childId)
+ await waitNoActivation(ctx, started.childId)
+ expect(waitingNotices(parent)).toHaveLength(1)
+ })
})
describe('continuable durability and teardown', () => {
@@ -1845,6 +1891,19 @@ function settlementNotices(agent: Agent): { sender: string; text: string; summar
})
}
+/** Every waiting (park) notice this agent received, in order, as flat text. */
+function waitingNotices(agent: Agent): { sender: string; text: string; summary: string }[] {
+ const logged = agent.session.snapshotEvents().flatMap(event => event.type === 'user/message' ? [event.data] : [])
+ return [...logged, ...agent.inbox.nextStep, ...agent.inbox.nextTurn].flatMap((message) => {
+ if (message.source.kind !== 'subagent-waiting') return []
+ return [{
+ sender: message.source.senderSessionId,
+ summary: message.source.summary,
+ text: message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n'),
+ }]
+ })
+}
+
describe('continuable adjacent-Agent delivery', () => {
it('rejects a stale sender before resolving either adjacent target', async () => {
const { ctx, parent } = await setup([])
--
2.50.1 (Apple Git-155)
|
|
Confirmed the patch matches the Option-2 shape exactly: One design note for the record: set-once dedup means a re-parked child (wakes, does an interim turn, parks again) is silent after its first notice — for a long chain this matches "first park is the signal"; if a downstream case shows the parent needs a re-signal after some elapsed time, a time-based re-notice can be layered later without touching the dedup. I'd take this patch as-is. Thanks for the |
|
Related request from the opposite direction, in case it helps shape a single delivery-policy design: I've opened #7508 asking for an opt-out — a way for the creator of a continuable child to suppress the settlement notice entirely. The scenario there is a host that already owns the child's reporting channel (chat surface, ticket update, webhook), where the parent is a long-lived session a human is actively working in and owns several such children. I don't think it conflicts with what's proposed here — this thread is about making sure a parent learns about a child it would otherwise lose track of, mine is about letting a caller that already reports elsewhere decline a notice it doesn't need. But they do meet at the same underlying point: settlement delivery currently has exactly one policy, and whether that policy is right depends on who owns the child's reporting channel. Worth noting for this thread specifically: adding One detail from digging through the 0.1.2-rc.1 and 0.1.5-rc.2 artifacts that may be relevant here too: |
Uh oh!
There was an error while loading. Please reload this page.
Version:
@deepseek-ai/dsh0.1.1-rc.2Symptom
A continuable background child dispatched with the plain
subagenttool (the "manager" seat) reaches a point where it spawns a grandchild (e.g. asubagent_reviewreviewer) and then ends its turn with a line like "the reviewer is running, I'll continue when it reports."At that moment:
runningand notcompleted;list_agents/psto discover the child has stalled;Impact
In a supervisor → manager → reviewer delegation tree, a manager that parks this way silently drops off the supervisor's radar; the supervisor's only signal is polling. It has reproduced reliably (twice) and costs a poll-and-resume cycle each time.
Why it happens (from the packaged source)
@deepseek-ai/dsh-tool-subagent-reportREADME). Reports and the settlement notice are authored explicitly by the child/manager, not emitted by the runtime on turn-end.stopReason(completed/aborted/error/max-tokens/refusal—@deepseek-ai/dsh-subagentrun-settlement.jsrunOutcome). A turn that ends while the child expects to continue (a non-terminal "park") never reaches a terminal reason, so no completion notice is ever produced.reportDelivery'snext-stepwake only fires on an accepted report; with neither a report nor a settlement there is nothing to wake the parent.Suggested fix (either)
Either would remove the poll-and-resume cycle the current behavior forces on every supervisor in a delegation tree.
All reactions