fix(subagent): scope drains and soften final flush
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# 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 packages/subagent/subagent/README.md
|
||||
README.md: a27c64ad3b7c4260d769f030db5dacd20e772ac6
|
||||
README.zh.md: d18e1159336515e9184dbc8f07dc92811a93d4e9
|
||||
README.md: 5b7c0376367a942d91700739ad445cf2b7a4455a
|
||||
README.zh.md: 30c5e0b501d0cf3cac749a788bb92a0353b466e9
|
||||
|
||||
@@ -77,7 +77,7 @@ The manager derives three internal residency conditions from Agent quiescence an
|
||||
|
||||
The manager reserves the child identity, resolves the durable descriptor, calls `ctx.agents.create()` (or `ctx.agents.resume()` for cold resume) through a private activation-owner scope, installs the returned `AgentHandle` in the Activation, establishes any continuable-parent ownership, and then submits the prompt. Cold resume never dispatches through a provider because the persisted Session already holds the initial prefix and the folded descriptor is the whole reconstruction input.
|
||||
|
||||
A continuation-managed parent Activation records each child Session id in an `ownedChildren` set before the child can run and disposes only after every owned child Activation completes `AgentHandle` disposal (child-first). Teardown propagates Agent cancellation top-down before awaiting slow descendants, while handle release remains child-first. Top-level and other non-continuation Agents have no Activation and stay outside this waiting graph. Final settlement treats only `ctx.sessions.flush(child.session) === true` as durability confirmation; `false` or rejection reports `DURABILITY_FAILED` and still disposes the handle and releases ownership, because retaining a failed child would permanently pin its ancestors in `waiting`.
|
||||
A continuation-managed parent Activation records each child Session id in an `ownedChildren` set before the child can run and disposes only after every owned child Activation completes `AgentHandle` disposal (child-first). Teardown propagates Agent cancellation top-down before awaiting slow descendants, while handle release remains child-first. Top-level and other non-continuation Agents have no Activation and stay outside this waiting graph. Final settlement awaits a best-effort `ctx.sessions.flush(child.session)` before handle disposal. A listener rejection is logged without failing the Activation because listener participation does not identify a persistence backend; the persisted state may therefore be missing or stale on resume.
|
||||
|
||||
## Lifecycle events
|
||||
|
||||
@@ -91,7 +91,7 @@ Provider additions and removals also emit `subagent/provider-added` and `subagen
|
||||
|
||||
The model-facing tool collects synchronously by default: it awaits the child result and disposes the run before returning. One-shot background delegation registers a plain Task in the tool, whose generic status, collection, and cancellation tools own later interaction, and persists its model-supplied `description` as the optional display label. Continuable background delegation calls `ctx.subagents.startContinuable()` and returns only the durable child id; the child owns its own turns from inbox acceptance, so there is no Task, no result promise, and no public subagent cancellation — a caller sends later work with the `send_message` follow-up tool, and the durable child Session remains the source of the child's detailed output. The continuation manager exists only while `ctx.agents` is available, and session persistence is resolved per continuation operation. Independently, `listChildren()` resolves session query and dynamically imports its optional runtime only when called, then interprets a read-only live-preferred scan of all descriptor-bearing direct children without consulting the continuation manager, Agent registrations, Activations, or providers. Service consumers such as a UI can retain both modes and choose a fallback for an unlabeled one-shot child; the model-facing `list_agents` tool projects only `continuable` entries and maps service activity to its existing `running`/`complete` vocabulary. The scan forwards the caller's signal to cancellable trace and exact-read operations, checks cancellation around the remaining event-list read, and reports every observed abort as `SubagentError` code `CANCELLED`. See the [background subagent tasks Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md), the [continuable background subagents Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md), the [durable catalog Agent Note](../../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md), the [merged-service Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md), the [capability-seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), and `src/types.ts` for the complete contracts.
|
||||
|
||||
Continuable Activations require final durability confirmation. One-shot runs retain best-effort session checkpointing, so a completed one-shot child is discoverable after disposal only when its session actually reached persistence; the service does not invent a catalog entry from Task history when that checkpoint is absent.
|
||||
Continuable Activations await a best-effort final session flush without treating listener participation as durability confirmation. One-shot runs retain best-effort session checkpointing, so a completed one-shot child is discoverable after disposal only when its session actually reached persistence; the service does not invent a catalog entry from Task history when that checkpoint is absent.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
|
||||
|
||||
管理器预留子 agent 身份、解析持久化描述符,通过私有的 activation-owner 作用域调用 `ctx.agents.create()`(冷恢复时为 `ctx.agents.resume()`),把返回的 `AgentHandle` 安装到 Activation 中,建立任何可继续父级所有权,然后提交提示词。冷恢复绝不通过提供方分发,因为持久化 Session 已持有初始前缀,折叠后的描述符即是全部重建输入。
|
||||
|
||||
受继续执行管理的父级 Activation 会在子 agent 能够运行之前,把每个子 agent 的 Session id 记录到 `ownedChildren` 集合中,并且只有在每个所拥有的子 agent Activation 完成 `AgentHandle` dispose 之后才会 dispose(子先于父)。拆卸会先自顶向下传播 Agent 取消,再等待缓慢的后代,而 handle 释放仍保持 child-first。顶层及其他非继续执行的 Agent 没有 Activation,处于该等待图之外。最终结算只把 `ctx.sessions.flush(child.session) === true` 视为持久性确认;`false` 或拒绝会报告 `DURABILITY_FAILED`,但仍会 dispose 句柄并释放所有权,因为保留失败的子 agent 会使其祖先永久停留在 `waiting`。
|
||||
受继续执行管理的父级 Activation 会在子 agent 能够运行之前,把每个子 agent 的 Session id 记录到 `ownedChildren` 集合中,并且只有在每个所拥有的子 agent Activation 完成 `AgentHandle` dispose 之后才会 dispose(子先于父)。拆卸会先自顶向下传播 Agent 取消,再等待缓慢的后代,而 handle 释放仍保持 child-first。顶层及其他非继续执行的 Agent 没有 Activation,处于该等待图之外。最终结算会在 dispose handle 前等待 best-effort 的 `ctx.sessions.flush(child.session)`。listener rejection 会被记录,但不会使 Activation 失败,因为 listener 是否参与无法标识持久化后端;因此,恢复时持久化状态可能缺失或陈旧。
|
||||
|
||||
## 生命周期事件
|
||||
|
||||
@@ -91,7 +91,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
|
||||
|
||||
面向模型的工具默认同步收集:先等待子 agent 结果,再 dispose 运行,然后才返回。一次性后台委派会在工具中注册普通 Task,其通用状态、收集和取消工具负责后续交互,并将模型提供的 `description` 持久化为可选显示标签。可继续后台委派会调用 `ctx.subagents.startContinuable()`,只返回持久化子 agent id;子 agent 自 inbox 接受起就拥有自己的轮次,因此没有 Task、没有结果 promise,也没有公开的子 agent 取消操作——调用方通过 `send_message` 后续操作工具发送后续工作,而持久化子 agent Session 仍是子 agent 详细输出的来源。只有 `ctx.agents` 可用时,继续执行管理器才会存在,而会话持久化按每项继续执行操作解析。与此独立,`listChildren()` 只在被调用时解析会话查询并动态导入其可选运行时,然后解释对所有带描述符的直接 child 所作的只读、实时优先扫描,且不查询继续执行管理器、Agent 注册信息、Activation 或提供方。UI 等服务消费方可以保留两种模式,并为无标签的一次性 child 选择回退展示;面向模型的 `list_agents` 工具只投影 `continuable` 条目,并将服务活动状态映射到现有的 `running`/`complete` 词汇。扫描会把调用方的取消信号转发到可取消的追踪与精确读取操作,在其余事件列表读取的前后检查取消,并将每次检测到的中止报告为 `SubagentError` 错误码 `CANCELLED`。完整契约见[后台 subagent 任务 Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md)、[可继续后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)、[持久化目录 Agent Note](../../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md)、[服务合并 Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)、[能力 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)和 `src/types.ts`。
|
||||
|
||||
可继续 Activation 要求最终持久性确认。一次性运行保留尽力执行的会话检查点,因此已完成的一次性 child 只有在其会话确实进入持久化存储时,才可在 dispose 后继续被发现;如果该检查点缺失,服务不会根据 Task 历史虚构目录条目。
|
||||
可继续 Activation 会等待 best-effort 的最终会话 flush,但不会把 listener 参与视为持久性确认。一次性运行保留尽力执行的会话检查点,因此已完成的一次性 child 只有在其会话确实进入持久化存储时,才可在 dispose 后继续被发现;如果该检查点缺失,服务不会根据 Task 历史虚构目录条目。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -917,9 +917,9 @@ export class SubagentContinuationManager {
|
||||
* transaction is installed before cancellation or recursive callbacks, so
|
||||
* admission and reentrant teardown converge on the same owner.
|
||||
*
|
||||
* A failed final checkpoint is reported but never prevents handle disposal or
|
||||
* ownership release, because retaining a failed child would permanently pin
|
||||
* its ancestors in `waiting`.
|
||||
* The final session flush is best effort and never prevents handle disposal
|
||||
* or ownership release, because retaining a child would permanently pin its
|
||||
* ancestors in `waiting`.
|
||||
* @param activation - the residency epoch to stop and release.
|
||||
* @returns the one disposal transaction owned by this Activation.
|
||||
*/
|
||||
@@ -951,7 +951,7 @@ export class SubagentContinuationManager {
|
||||
.filter((child): child is Activation => child !== undefined)
|
||||
const childDisposals = children.map(child => this.dispose(child))
|
||||
|
||||
let failure: Error | undefined
|
||||
const failures: SubagentError[] = []
|
||||
try {
|
||||
// Release remains child-first even though cancellation propagated
|
||||
// top-down: every owned child completes before this handle is removed.
|
||||
@@ -965,71 +965,73 @@ export class SubagentContinuationManager {
|
||||
}))
|
||||
const reasons = childFailures.filter(reason => reason !== undefined)
|
||||
if (reasons.length > 0) {
|
||||
failure = new SubagentError(
|
||||
failures.push(new SubagentError(
|
||||
`subagent "${childId}" child teardown failed: ${reasons.map(reason => errorChain(reason)).join('; ')}`,
|
||||
'ACTIVATION_TEARDOWN_FAILED',
|
||||
)
|
||||
))
|
||||
}
|
||||
// Quiesce before the checkpoint: a turn still running would keep
|
||||
// Quiesce before the flush: a turn still running would keep
|
||||
// appending events the flush cannot cover.
|
||||
await idle
|
||||
const durability = await this.checkpoint(activation)
|
||||
failure ??= durability
|
||||
await this.flushFinalState(activation)
|
||||
// Capture the child-dependent edge data while the child is still live:
|
||||
// handle disposal unregisters it, and consumers read its log and scope.
|
||||
activation.observer.capture(activation.handle.agent)
|
||||
} catch (error: unknown) {
|
||||
failure ??= new SubagentError(
|
||||
failures.push(new SubagentError(
|
||||
`subagent "${childId}" activation teardown failed: ${errorChain(error)}`,
|
||||
'ACTIVATION_TEARDOWN_FAILED',
|
||||
{ cause: error },
|
||||
)
|
||||
} finally {
|
||||
try {
|
||||
await activation.handle.dispose()
|
||||
} catch (error: unknown) {
|
||||
failure ??= new SubagentError(
|
||||
`subagent "${childId}" activation handle disposal failed: ${errorChain(error)}`,
|
||||
'ACTIVATION_TEARDOWN_FAILED',
|
||||
{ cause: error },
|
||||
)
|
||||
} finally {
|
||||
// Only now is the Activation gone: keeping the entry until disposal
|
||||
// settles makes a racing delivery wait for release rather than
|
||||
// cold-resume into the still-registered agent.
|
||||
this.activations.delete(childId)
|
||||
// Release ownership even on failure: a retained failed child would pin
|
||||
// its ancestors in `waiting` forever.
|
||||
this.releaseOwnership(childId)
|
||||
// Emit once the disposal outcome is known, so a rejecting scoped cleanup
|
||||
// cannot be reported as a successful epoch.
|
||||
activation.observer.settle(failure)
|
||||
}
|
||||
))
|
||||
}
|
||||
try {
|
||||
await activation.handle.dispose()
|
||||
} catch (error: unknown) {
|
||||
failures.push(new SubagentError(
|
||||
`subagent "${childId}" activation handle disposal failed: ${errorChain(error)}`,
|
||||
'ACTIVATION_TEARDOWN_FAILED',
|
||||
{ cause: error },
|
||||
))
|
||||
}
|
||||
|
||||
let failure: SubagentError | undefined
|
||||
if (failures.length === 1) {
|
||||
failure = failures[0]
|
||||
} else if (failures.length > 1) {
|
||||
failure = new SubagentError(
|
||||
`subagent "${childId}" activation teardown failed at ${failures.length} boundaries: `
|
||||
+ failures.map(item => errorChain(item)).join('; '),
|
||||
'ACTIVATION_TEARDOWN_FAILED',
|
||||
{ cause: new AggregateError(failures) },
|
||||
)
|
||||
}
|
||||
// Only now is the Activation gone: keeping the entry until disposal settles
|
||||
// makes a racing delivery wait for release rather than cold-resume into the
|
||||
// still-registered agent.
|
||||
this.activations.delete(childId)
|
||||
// Release ownership even on failure: a retained failed child would pin its
|
||||
// ancestors in `waiting` forever.
|
||||
this.releaseOwnership(childId)
|
||||
// Emit once the disposal outcome is known, so a rejecting scoped cleanup
|
||||
// cannot be reported as a successful epoch.
|
||||
activation.observer.settle(failure)
|
||||
if (failure !== undefined) throw failure
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the final durability checkpoint. Only `true` confirms durability;
|
||||
* `false` and rejection both report `DURABILITY_FAILED` so the persisted
|
||||
* child state is known to be possibly missing or stale on a later resume.
|
||||
* Request a best-effort final session flush after the child is quiescent.
|
||||
* Listener failure is logged because flush participation cannot identify a
|
||||
* particular persistence backend, and teardown must still release ownership.
|
||||
* @param activation - the Activation whose final events should be flushed.
|
||||
*/
|
||||
private async checkpoint(activation: Activation): Promise<SubagentError | undefined> {
|
||||
private async flushFinalState(activation: Activation): Promise<void> {
|
||||
const child = activation.handle.agent
|
||||
try {
|
||||
const participated = await child.ctx.sessions.flush(child.session)
|
||||
if (participated) return undefined
|
||||
return new SubagentError(
|
||||
`subagent "${activation.childId}" required durability checkpoint has no registered listener; `
|
||||
+ 'the latest child state was not confirmed persisted and may be unavailable or stale on resume',
|
||||
'DURABILITY_FAILED',
|
||||
)
|
||||
await child.ctx.sessions.flush(child.session)
|
||||
} catch (error: unknown) {
|
||||
return new SubagentError(
|
||||
`subagent "${activation.childId}" durability checkpoint failed; the latest child state was not `
|
||||
+ `confirmed persisted and may be unavailable or stale on resume: ${errorChain(error)}`,
|
||||
'DURABILITY_FAILED',
|
||||
{ cause: error },
|
||||
this.ctx.logger.warn(
|
||||
`subagent "${activation.childId}" best-effort final session flush failed; `
|
||||
+ `the persisted state may be unavailable or stale on resume: ${errorChain(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* @module @deepseek-ai/dsh-subagent/run-settlement
|
||||
*/
|
||||
|
||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import type { TaskOutcome } from '@deepseek-ai/dsh-tasks'
|
||||
import type { SubagentResult, SubagentRun } from './types.ts'
|
||||
@@ -41,13 +40,6 @@ function runOutcome(result: SubagentResult): TaskOutcome {
|
||||
}
|
||||
}
|
||||
|
||||
/** Render infrastructure failure detail without hiding a durability diagnosis. */
|
||||
function runFailureDetail(error: unknown): string {
|
||||
return error instanceof HarnessError && error.code === 'DURABILITY_FAILED'
|
||||
? error.message
|
||||
: String(error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Await the child result, dispose the run, then return its task outcome. Result
|
||||
* and disposal failures become `failed`; when both fail, both details survive.
|
||||
@@ -59,7 +51,7 @@ export async function settleRun(run: SubagentRun): Promise<TaskOutcome> {
|
||||
try {
|
||||
outcome = runOutcome(await run.result)
|
||||
} catch (error: unknown) {
|
||||
outcome = { status: 'failed', detail: runFailureDetail(error) }
|
||||
outcome = { status: 'failed', detail: String(error) }
|
||||
}
|
||||
try {
|
||||
await run.dispose()
|
||||
|
||||
@@ -615,7 +615,7 @@ describe('continuable child ownership', () => {
|
||||
})
|
||||
|
||||
describe('continuable durability and teardown', () => {
|
||||
it('reports DURABILITY_FAILED without leaking a waiting Activation', async () => {
|
||||
it('settles when the best-effort final flush has no listeners', async () => {
|
||||
const releaseResponse = Promise.withResolvers<undefined>()
|
||||
const adapter = new GatedAdapter([
|
||||
{ chunks: textResponse('unconfirmed answer'), gate: releaseResponse.promise },
|
||||
@@ -626,32 +626,60 @@ describe('continuable durability and teardown', () => {
|
||||
|
||||
const started = await ctx.subagents.startContinuable(startSpec(parent))
|
||||
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
|
||||
// Remove every durability listener, so the final checkpoint cannot confirm.
|
||||
// Remove every persistence listener; the final flush is advisory.
|
||||
await disposePersistence!()
|
||||
releaseResponse.resolve(undefined)
|
||||
|
||||
// The handle is still disposed and ownership released, so nothing is pinned.
|
||||
await waitNoActivation(ctx, started.childId)
|
||||
await vi.waitFor(() => {
|
||||
expect(warnings.some(warning => warning.includes('durability'))).toBe(true)
|
||||
})
|
||||
expect(warnings.some(warning => warning.includes('final session flush'))).toBe(false)
|
||||
})
|
||||
|
||||
it('reports DURABILITY_FAILED when the final checkpoint rejects', async () => {
|
||||
it('logs a failed final flush after every listener settles without failing the Activation', async () => {
|
||||
const { ctx, parent } = await setup([textResponse('answer')])
|
||||
const warnings: string[] = []
|
||||
const ends: SubagentRunEndInfo[] = []
|
||||
let peerFlushed = false
|
||||
ctx.logger.warn = (message: string) => { warnings.push(message) }
|
||||
// A listener that throws makes flush reject rather than return false.
|
||||
ctx.on('subagent/end', info => void ends.push(info))
|
||||
ctx.on('session/flush', (session) => {
|
||||
if (session.header.parentSession !== undefined) throw new Error('disk full')
|
||||
})
|
||||
ctx.on('session/flush', (session) => {
|
||||
if (session.header.parentSession !== undefined) peerFlushed = true
|
||||
})
|
||||
|
||||
const started = await ctx.subagents.startContinuable(startSpec(parent))
|
||||
// The handle is still disposed and ownership released, so nothing is pinned.
|
||||
await waitNoActivation(ctx, started.childId)
|
||||
expect(peerFlushed).toBe(true)
|
||||
expect(warnings.some(warning => warning.includes('best-effort final session flush failed'))).toBe(true)
|
||||
expect(ends.at(-1)?.stopReason).toBe('completed')
|
||||
})
|
||||
|
||||
it('logs a teardown failure reached through normal settlement', async () => {
|
||||
const hold = Promise.withResolvers<undefined>()
|
||||
const adapter = new GatedAdapter([{ chunks: textResponse('answer'), gate: hold.promise }])
|
||||
const { ctx, parent } = await setupWith(adapter)
|
||||
const warnings: string[] = []
|
||||
ctx.logger.warn = (message: string) => { warnings.push(message) }
|
||||
|
||||
const started = await ctx.subagents.startContinuable(startSpec(parent))
|
||||
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
|
||||
const manager = (ctx.subagents as unknown as {
|
||||
continuations: { activations: Map<SessionId, { handle: { dispose: () => Promise<void> } }> }
|
||||
}).continuations
|
||||
const activation = manager.activations.get(started.childId)!
|
||||
const realDispose = activation.handle.dispose.bind(activation.handle)
|
||||
activation.handle.dispose = async () => {
|
||||
await realDispose()
|
||||
throw new Error('normal settlement cleanup failed')
|
||||
}
|
||||
|
||||
hold.resolve(undefined)
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(warnings.some(warning => warning.includes('durability checkpoint failed'))).toBe(true)
|
||||
expect(warnings.some(warning => warning.includes('normal settlement cleanup failed'))).toBe(true)
|
||||
})
|
||||
expect(ctx.agents.get(started.childId)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('disposes every live Activation forest child-first on manager teardown', async () => {
|
||||
@@ -1184,7 +1212,38 @@ describe('continuable review regressions', () => {
|
||||
expect(ends[0]!.stopReason).toBe('error')
|
||||
})
|
||||
|
||||
it('cancels a running turn before the final durability checkpoint', async () => {
|
||||
it('preserves independent pre-disposal and handle-disposal failures', async () => {
|
||||
const hold = Promise.withResolvers<undefined>()
|
||||
const adapter = new GatedAdapter([{ chunks: textResponse('answer'), gate: hold.promise }])
|
||||
const { ctx, parent } = await setupWith(adapter)
|
||||
const started = await ctx.subagents.startContinuable(startSpec(parent))
|
||||
const manager = (ctx.subagents as unknown as {
|
||||
continuations: {
|
||||
activations: Map<SessionId, {
|
||||
handle: { dispose: () => Promise<void> }
|
||||
observer: { capture: (child: Agent) => void }
|
||||
}>
|
||||
}
|
||||
}).continuations
|
||||
const activation = manager.activations.get(started.childId)!
|
||||
const realDispose = activation.handle.dispose.bind(activation.handle)
|
||||
activation.observer.capture = () => { throw new Error('capture failed') }
|
||||
activation.handle.dispose = async () => {
|
||||
await realDispose()
|
||||
throw new Error('scoped cleanup failed')
|
||||
}
|
||||
|
||||
const drained = drainManager(ctx)
|
||||
hold.resolve(undefined)
|
||||
const failure = await drained.catch((error: unknown) => error)
|
||||
|
||||
expect(failure).toMatchObject({ code: 'ACTIVATION_TEARDOWN_FAILED' })
|
||||
expect(String(failure)).toContain('capture failed')
|
||||
expect(String(failure)).toContain('scoped cleanup failed')
|
||||
expect(ctx.agents.get(started.childId)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('cancels a running turn before the best-effort final flush', async () => {
|
||||
const hold = Promise.withResolvers<undefined>()
|
||||
const adapter = new GatedAdapter([{ chunks: textResponse('slow'), gate: hold.promise }])
|
||||
const { ctx, parent } = await setupWith(adapter)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { settleRun } from '../src/index.ts'
|
||||
|
||||
@@ -44,19 +43,6 @@ describe('outcome mapping helpers', () => {
|
||||
expect(failed).toEqual({ status: 'failed', detail: 'Error: transport gone' })
|
||||
expect(disposed).toBe(true)
|
||||
|
||||
const durabilityMessage = 'subagent "child-3" durability checkpoint failed; latest state unavailable: disk full'
|
||||
const durabilityFailed = await settleRun({
|
||||
id: SessionId('child-3'),
|
||||
localAgent: undefined,
|
||||
result: Promise.reject(new HarnessError(
|
||||
durabilityMessage,
|
||||
'DURABILITY_FAILED',
|
||||
{ cause: new Error('disk full') },
|
||||
)),
|
||||
dispose: () => Promise.resolve(),
|
||||
})
|
||||
expect(durabilityFailed).toEqual({ status: 'failed', detail: durabilityMessage })
|
||||
|
||||
const disposeFailed = await settleRun({
|
||||
id: SessionId('child-4'),
|
||||
localAgent: undefined,
|
||||
|
||||
Reference in New Issue
Block a user