@deepseek-ai/dsh-subagent-acp 0.1.2-alpha.5 → 0.1.3-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/subagent/subagent-acp/README.md
5
- README.md: 2d929738de2f761cbc597fae6cb52f7d44d83536
6
- README.zh.md: d67e174a44497148d08ef54d13f17df934db2018
5
+ README.md: 62b20bd1b35a80682dbacbe1e9ff4c0b1b7c3d63
6
+ README.zh.md: fa1b1c8897cee115de25240fa543ac0d496d8f22
package/README.md CHANGED
@@ -67,7 +67,7 @@ A successful run returns the child's final streamed assistant text as the result
67
67
 
68
68
  ### Failure and recovery
69
69
 
70
- A spawn, initialization, or new-session failure rejects before publication, ordinarily after the child process is reaped. If cleanup also fails, the rejection preserves ordered safe startup and teardown facts without claiming whole-tree quiescence. Non-cancellation errors expose only fixed provider, stage, and category facts; the original failure stays on the internal cause chain and in Host diagnostics. After publication, a prompt, transport, or early-process failure resolves as `error` with a safe diagnostic, while local cancellation resolves as `aborted` without failure detail.
70
+ A spawn, initialization, or new-session failure rejects before publication, ordinarily after the managed range is proven quiescent. If cleanup also fails, the rejection preserves ordered safe startup and teardown facts without claiming whole-range quiescence. Non-cancellation errors expose only fixed provider, stage, and category facts; the original failure stays on the internal cause chain and in Host diagnostics. After publication, a prompt, transport, or early-process failure resolves as `error` with a safe diagnostic, while local cancellation resolves as `aborted` without failure detail.
71
71
 
72
72
  ### Safe diagnostics
73
73
 
@@ -91,7 +91,7 @@ This section explains how the backend drives a child over ACP and where the obse
91
91
 
92
92
  ### Start and ownership flow
93
93
 
94
- A start resolves the child's working directory (the configured `cwd` override, else the parent session's cwd), spawns the command through the subprocess seam, performs the ACP `initialize` and `newSession` handshake, and only then publishes the run. Fulfillment means a remote session is ready and ownership has transferred to the caller. Disposal is idempotent: it closes stdin and waits a configured grace for cooperative quiescence, then escalates through SIGTERM to SIGKILL and awaits whole-tree exit. Cleanup failures remain observable as ordered safe facts and never claim quiescence.
94
+ A start resolves the child's working directory (the configured `cwd` override, else the parent session's cwd), spawns the command through the subprocess seam, performs the ACP `initialize` and `newSession` handshake, and only then publishes the run. Fulfillment means a remote session is ready and ownership has transferred to the caller. Disposal is idempotent: it closes stdin and waits a configured grace for cooperative quiescence, then escalates through SIGTERM to SIGKILL and awaits whole-range exit. Cleanup failures remain observable as ordered safe facts and never claim quiescence.
95
95
 
96
96
  ### Stop-reason mapping
97
97
 
package/README.zh.md CHANGED
@@ -67,7 +67,7 @@ DeepSeek Harness 子进程使用产品启动器和一个显式的绝对路径 `D
67
67
 
68
68
  ### 失败与恢复
69
69
 
70
- spawn、初始化或新建会话失败会在发布前拒绝,通常先等待子进程被回收。如果清理也失败,拒绝会保留有序、安全的启动与拆卸事实,但不会声称整棵进程树已经停稳。非取消错误只暴露固定的提供方、阶段与类别事实;原始失败保留在内部 cause 链与 Host 诊断中。发布后,提示词、传输或进程提前退出会以携带安全诊断的 `error` 结算;本地取消则以不带失败详情的 `aborted` 结算。
70
+ spawn、初始化或新建会话失败会在发布前拒绝,通常先证明 managed range 已经完全停稳。如果清理也失败,拒绝会保留有序、安全的启动与拆卸事实,但不会声称整个 range 已经停稳。非取消错误只暴露固定的提供方、阶段与类别事实;原始失败保留在内部 cause 链与 Host 诊断中。发布后,提示词、传输或进程提前退出会以携带安全诊断的 `error` 结算;本地取消则以不带失败详情的 `aborted` 结算。
71
71
 
72
72
  ### 安全诊断
73
73
 
@@ -91,7 +91,7 @@ spawn、初始化或新建会话失败会在发布前拒绝,通常先等待子
91
91
 
92
92
  ### 启动与所有权流程
93
93
 
94
- 一次启动先解析子 agent 的工作目录(配置的 `cwd` 覆盖值,否则取父会话 cwd),经子进程 seam spawn 命令,完成 ACP `initialize` 与 `newSession` 握手,然后才发布运行。兑现意味着远程会话已就绪、所有权已转移给调用方。dispose(资源释放)是幂等的:先关闭 stdin 并按配置的宽限等待协作式完全停稳,再经 SIGTERM 升级到 SIGKILL,并等待整棵进程树退出。清理失败会作为有序的安全事实保持可观察,且绝不声称已经完全停稳。
94
+ 一次启动先解析子 agent 的工作目录(配置的 `cwd` 覆盖值,否则取父会话 cwd),经子进程 seam spawn 命令,完成 ACP `initialize` 与 `newSession` 握手,然后才发布运行。兑现意味着远程会话已就绪、所有权已转移给调用方。dispose(资源释放)是幂等的:先关闭 stdin 并按配置的宽限等待协作式完全停稳,再经 SIGTERM 升级到 SIGKILL,并等待整个 managed range 退出。清理失败会作为有序的安全事实保持可观察,且绝不声称已经完全停稳。
95
95
 
96
96
  ### 停止原因映射
97
97
 
package/lib/index.js CHANGED
@@ -74,8 +74,8 @@ function permissionRequestKind(kind) {
74
74
  const candidate = kind ?? "unknown";
75
75
  return ACP_TOOL_KINDS.has(candidate) ? candidate : "unknown";
76
76
  }
77
- /** Bounded whole-tree exit wait: polls the handle's tree liveness until it exits or `ms` elapses. */
78
- async function treeExitsWithin(child, ms) {
77
+ /** Bounded managed-range exit wait: observes the handle's range until it is empty or `ms` elapses. */
78
+ async function rangeExitsWithin(child, ms) {
79
79
  const controller = new AbortController();
80
80
  const timer = setTimeout(() => {
81
81
  controller.abort();
@@ -88,22 +88,31 @@ async function treeExitsWithin(child, ms) {
88
88
  }
89
89
  /**
90
90
  * Cooperative teardown ladder for an out-of-process agent, over the seam's
91
- * public verbs; resolves only at whole-tree quiescence: stdin EOF (the child's
91
+ * public verbs; resolves only at whole-range quiescence: stdin EOF (the child's
92
92
  * window to flush persistence and reap its own descendants), then the
93
93
  * terminate() escalation (SIGTERM → spec grace → SIGKILL) and its
94
- * whole-tree exit proof.
94
+ * whole-range exit proof.
95
95
  * @param child - the spawned ACP child's handle.
96
96
  * @param eofGraceMs - tier-1 window after stdin EOF.
97
97
  */
98
98
  async function disposeAcpChild(child, eofGraceMs) {
99
- if (child.pid <= 0) {
100
- await child.done.catch(() => {});
101
- return;
102
- }
99
+ const failures = [];
103
100
  child.stdin?.end();
104
- if (await treeExitsWithin(child, eofGraceMs)) return;
101
+ let exited = false;
102
+ try {
103
+ exited = await rangeExitsWithin(child, eofGraceMs);
104
+ } catch (error) {
105
+ failures.push(toError(error));
106
+ }
107
+ if (exited) return;
105
108
  child.terminate();
106
- await child.waitForExit();
109
+ try {
110
+ await child.waitForExit();
111
+ } catch (error) {
112
+ failures.push(toError(error));
113
+ }
114
+ if (failures.length === 1) throw failures[0];
115
+ if (failures.length > 1) throw new AggregateError(failures, "ACP subprocess teardown failed");
107
116
  }
108
117
  /**
109
118
  * Map an ACP {@link StopReason} to a harness {@link SubagentStopReason}.
@@ -154,11 +163,7 @@ function reportFailure(spec, error) {
154
163
  } catch {}
155
164
  }
156
165
  /** Classify an unpublished failure from the active protocol operation and observed process facts. */
157
- function startupFailure(error, stage, child, outcome) {
158
- if (child.pid <= 0) return new AcpRunFailure({
159
- stage: "process",
160
- category: "process-start"
161
- }, error);
166
+ function startupFailure(error, stage, outcome) {
162
167
  return new AcpRunFailure(
163
168
  /* v8 ignore next -- Windows anonymous pipes cannot expose a live-child protocol close during startup. */
164
169
  outcome === void 0 ? {
@@ -194,10 +199,10 @@ function terminalFailure(reason, permission) {
194
199
  /**
195
200
  * Start and publish one ACP child after initialization and session creation.
196
201
  * Child failures resolve through the run result. Startup rejects with fixed
197
- * safe facts after provider-owned cleanup; successful cleanup proves process
198
- * reap. Cleanup failure preserves startup plus teardown facts for an ordinary
202
+ * safe facts after provider-owned cleanup; successful cleanup proves managed
203
+ * range quiescence. Cleanup failure preserves startup plus teardown facts for an ordinary
199
204
  * failure, or teardown alone after cancellation, without claiming quiescence.
200
- * Disposal cancels, kills, and reaps the child.
205
+ * Disposal cancels, terminates, and settles the child's managed range.
201
206
  * @param request - the start request; its signal is the cancellation channel.
202
207
  * @param spec - the resolved spawn spec: command/args/cwd, env, permission
203
208
  * policy, dispose graces, and the optional error sink.
@@ -230,18 +235,22 @@ async function startAcpRun(request, spec) {
230
235
  if (child.stdin === void 0 || child.stdout === void 0) throw new Error("subagent-acp: subprocess implementation dropped a piped protocol stream");
231
236
  /* v8 ignore stop */
232
237
  let processOutcome;
238
+ let processFailure;
233
239
  const processDone = child.done.then((outcome) => {
234
240
  processOutcome = outcome;
235
241
  return outcome;
242
+ }, (error) => {
243
+ processFailure = toError(error);
244
+ throw processFailure;
236
245
  });
237
- const spawnFailed = processDone.then(
246
+ const processRejected = processDone.then(
238
247
  /* v8 ignore next -- the success arm's never-settling executor is intentionally empty. */
239
248
  () => new Promise(() => {}),
240
249
  (err) => Promise.reject(toError(err))
241
250
  );
242
- spawnFailed.catch(() => {});
251
+ processRejected.catch(() => {});
243
252
  const observeProcessOutcome = async (signal) => {
244
- if (processOutcome !== void 0 || child.pid <= 0) return processOutcome;
253
+ if (processOutcome !== void 0) return processOutcome;
245
254
  const timeout = AbortSignal.timeout(Math.ceil(spec.disposeGraceMs));
246
255
  const bound = signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
247
256
  const aborted = Promise.withResolvers();
@@ -255,7 +264,6 @@ async function startAcpRun(request, spec) {
255
264
  try {
256
265
  return await Promise.race([processDone, aborted.promise]);
257
266
  } catch {
258
- /* v8 ignore next -- a published child.done cannot reject; spawn rejection is consumed before publication. */
259
267
  return processOutcome;
260
268
  } finally {
261
269
  bound.removeEventListener("abort", onObservationAbort);
@@ -331,18 +339,20 @@ async function startAcpRun(request, spec) {
331
339
  /* v8 ignore next -- cancelSettled wins the startup race before this post-response guard can settle it. */
332
340
  if (flags.cancelled) throw new Error("subagent cancelled before the ACP session started");
333
341
  })(),
334
- spawnFailed,
342
+ processRejected,
335
343
  cancelSettled.then(() => {
336
344
  throw new Error("subagent cancelled before the ACP session started");
337
345
  })
338
346
  ]);
339
347
  } catch (error) {
340
348
  request.signal.removeEventListener("abort", onAbort);
341
- const startup = flags.cancelled ? { kind: "cancelled" } : {
349
+ const cancelledBeforeCleanup = flags.cancelled;
350
+ const observedOutcome = !cancelledBeforeCleanup && !(error instanceof AcpRunFailure) ? await observeProcessOutcome() : void 0;
351
+ const startup = cancelledBeforeCleanup ? { kind: "cancelled" } : {
342
352
  kind: "failed",
343
- failure: error instanceof AcpRunFailure ? error : startupFailure(error, startupStage, child, await observeProcessOutcome())
353
+ failure: error instanceof AcpRunFailure ? error : startupFailure(error, startupStage, observedOutcome)
344
354
  };
345
- if (startup.kind === "cancelled") {} else reportFailure(spec, error instanceof AcpRunFailure ? error.cause : error);
355
+ if (startup.kind === "cancelled") {} else reportFailure(spec, error instanceof AcpRunFailure ? error.cause : processFailure ?? error);
346
356
  try {
347
357
  await disposeProcess();
348
358
  } catch (cleanupError) {
@@ -392,7 +402,7 @@ async function startAcpRun(request, spec) {
392
402
  outcome
393
403
  }, latestPermission);
394
404
  }
395
- throw error;
405
+ throw processFailure ?? error;
396
406
  }
397
407
  },
398
408
  collectOutput,
@@ -51,7 +51,7 @@ export interface AcpRunSpec {
51
51
  disposeGraceMs: number;
52
52
  /**
53
53
  * Spawn function from the subprocess seam (`ctx.subprocess.spawn`), so the
54
- * child rides the shared scrub, tree-scoped teardown, and service-owned
54
+ * child rides the shared scrub, managed-range teardown, and service-owned
55
55
  * lifetime instead of a package-local child_process path.
56
56
  */
57
57
  spawn: (spec: SubprocessSpawnSpec) => SubprocessHandle;
@@ -74,10 +74,10 @@ export declare const DEFAULT_DISPOSE_GRACE_MS = 3000;
74
74
  export declare function acpConfigurationFailure(cause: unknown): Error;
75
75
  /**
76
76
  * Cooperative teardown ladder for an out-of-process agent, over the seam's
77
- * public verbs; resolves only at whole-tree quiescence: stdin EOF (the child's
77
+ * public verbs; resolves only at whole-range quiescence: stdin EOF (the child's
78
78
  * window to flush persistence and reap its own descendants), then the
79
79
  * terminate() escalation (SIGTERM → spec grace → SIGKILL) and its
80
- * whole-tree exit proof.
80
+ * whole-range exit proof.
81
81
  * @param child - the spawned ACP child's handle.
82
82
  * @param eofGraceMs - tier-1 window after stdin EOF.
83
83
  */
@@ -104,10 +104,10 @@ export declare function toAcpPrompt(prompt: ContentBlock[]): AcpContentBlock[];
104
104
  /**
105
105
  * Start and publish one ACP child after initialization and session creation.
106
106
  * Child failures resolve through the run result. Startup rejects with fixed
107
- * safe facts after provider-owned cleanup; successful cleanup proves process
108
- * reap. Cleanup failure preserves startup plus teardown facts for an ordinary
107
+ * safe facts after provider-owned cleanup; successful cleanup proves managed
108
+ * range quiescence. Cleanup failure preserves startup plus teardown facts for an ordinary
109
109
  * failure, or teardown alone after cancellation, without claiming quiescence.
110
- * Disposal cancels, kills, and reaps the child.
110
+ * Disposal cancels, terminates, and settles the child's managed range.
111
111
  * @param request - the start request; its signal is the cancellation channel.
112
112
  * @param spec - the resolved spawn spec: command/args/cwd, env, permission
113
113
  * policy, dispose graces, and the optional error sink.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-subagent-acp",
3
3
  "description": "Out-of-process ACP subagent backend: drives a child agent in a spawned subprocess over the Agent Client Protocol",
4
- "version": "0.1.2-alpha.5",
4
+ "version": "0.1.3-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,34 +27,34 @@
27
27
  ],
28
28
  "license": "MIT",
29
29
  "peerDependencies": {
30
- "@deepseek-ai/dsh-agent": "^0.1.2-alpha.5",
31
- "@deepseek-ai/dsh-llm": "^0.1.2-alpha.5",
30
+ "@deepseek-ai/dsh-agent": "^0.1.3-alpha.2",
31
+ "@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
32
32
  "@deepseek-ai/cordis": "^4.0.2",
33
- "@deepseek-ai/dsh-session": "^0.1.2-alpha.5",
34
- "@deepseek-ai/dsh-subagent": "^0.1.2-alpha.5",
35
- "@deepseek-ai/dsh-timeout": "^0.1.2-alpha.5",
36
- "@deepseek-ai/dsh-subprocess": "^0.1.2-alpha.5"
33
+ "@deepseek-ai/dsh-subprocess": "^0.1.3-alpha.2",
34
+ "@deepseek-ai/dsh-timeout": "^0.1.3-alpha.2",
35
+ "@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
36
+ "@deepseek-ai/dsh-subagent": "^0.1.3-alpha.2"
37
37
  },
38
38
  "dependencies": {
39
39
  "@agentclientprotocol/sdk": "1.4.0",
40
- "@deepseek-ai/dsh-brand": "^0.1.2-alpha.5",
41
- "@deepseek-ai/schemastery": "^3.18.2"
40
+ "@deepseek-ai/schemastery": "^3.18.2",
41
+ "@deepseek-ai/dsh-brand": "^0.1.3-alpha.2"
42
42
  },
43
43
  "devDependencies": {
44
- "@deepseek-ai/dsh-agent": "^0.1.2-alpha.5",
45
- "@deepseek-ai/dsh-app-boot": "^0.1.2-alpha.5",
46
44
  "@deepseek-ai/cordis": "^4.0.2",
47
45
  "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
48
- "@deepseek-ai/dsh-loader-smoke": "^0.1.2-alpha.5",
49
- "@deepseek-ai/dsh-llm": "^0.1.2-alpha.5",
50
- "@deepseek-ai/dsh-session": "^0.1.2-alpha.5",
51
- "@deepseek-ai/dsh-session-checkpoint-policy": "^0.1.2-alpha.5",
52
- "@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.2-alpha.5",
53
- "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.5",
54
- "@deepseek-ai/dsh-subagent": "^0.1.2-alpha.5",
55
- "@deepseek-ai/dsh-subprocess": "^0.1.2-alpha.5",
56
- "@deepseek-ai/dsh-subprocess-local": "^0.1.2-alpha.5",
57
- "@deepseek-ai/dsh-timeout": "^0.1.2-alpha.5",
58
- "@deepseek-ai/dsh-tool-subagent": "^0.1.2-alpha.5"
46
+ "@deepseek-ai/dsh-agent": "^0.1.3-alpha.2",
47
+ "@deepseek-ai/dsh-app-boot": "^0.1.3-alpha.2",
48
+ "@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
49
+ "@deepseek-ai/dsh-loader-smoke": "^0.1.3-alpha.2",
50
+ "@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
51
+ "@deepseek-ai/dsh-session-checkpoint-policy": "^0.1.3-alpha.2",
52
+ "@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.3-alpha.2",
53
+ "@deepseek-ai/dsh-session-projection": "^0.1.3-alpha.2",
54
+ "@deepseek-ai/dsh-subagent": "^0.1.3-alpha.2",
55
+ "@deepseek-ai/dsh-subprocess": "^0.1.3-alpha.2",
56
+ "@deepseek-ai/dsh-subprocess-local": "^0.1.3-alpha.2",
57
+ "@deepseek-ai/dsh-timeout": "^0.1.3-alpha.2",
58
+ "@deepseek-ai/dsh-tool-subagent": "^0.1.3-alpha.2"
59
59
  }
60
60
  }