@zhushanwen/pi-subagent-workflow 8.13.0 → 8.14.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-subagent-workflow",
3
- "version": "8.13.0",
3
+ "version": "8.14.0",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
@@ -54,21 +54,21 @@
54
54
  ]
55
55
  },
56
56
  "dependencies": {
57
- "@xyz-agent/extension-protocol": "0.10.0",
58
- "@xyz-agent/session-delivery": "0.3.1",
59
- "@zhushanwen/pi-ext-guards": "0.3.0",
60
- "@zhushanwen/pi-extension-logger": "0.6.0",
61
- "@zhushanwen/subagent-core": "0.10.0",
62
- "@zhushanwen/pi-subagent-cli": "0.3.0",
63
- "@zhushanwen/zcode-subagent-cli": "0.3.0"
57
+ "@xyz-agent/extension-protocol": "0.11.0",
58
+ "@xyz-agent/session-delivery": "0.4.1",
59
+ "@zhushanwen/pi-ext-guards": "0.4.0",
60
+ "@zhushanwen/subagent-core": "0.10.1",
61
+ "@zhushanwen/pi-subagent-cli": "0.4.0",
62
+ "@zhushanwen/zcode-subagent-cli": "0.3.1",
63
+ "@zhushanwen/pi-extension-logger": "0.6.0"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "@earendil-works/pi-ai": "^0.84.4",
67
67
  "@earendil-works/pi-coding-agent": "^0.84.4",
68
68
  "@earendil-works/pi-tui": "^0.84.4",
69
69
  "typebox": "*",
70
- "@zhushanwen/pi-pending-notifications": "0.7.1",
71
- "@zhushanwen/pi-structured-output": "5.1.5"
70
+ "@zhushanwen/pi-pending-notifications": "0.7.2",
71
+ "@zhushanwen/pi-structured-output": "5.1.7"
72
72
  },
73
73
  "peerDependenciesMeta": {
74
74
  "@earendil-works/pi-coding-agent": {
@@ -37,9 +37,10 @@
37
37
  // (30min 有界),不丢 errs-safe 兜底。
38
38
 
39
39
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
40
- import { SUBAGENT_INFLIGHT_MARKER, isInFlightReportAck } from "@xyz-agent/extension-protocol";
40
+ import { SUBAGENT_INFLIGHT_MARKER, callMarkerRpc, isInFlightReportAck } from "@xyz-agent/extension-protocol";
41
41
  import { getInFlightSnapshot } from "@zhushanwen/subagent-core";
42
42
  import { getLogger } from "@zhushanwen/pi-extension-logger";
43
+ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
43
44
 
44
45
  /** select 通道级超时(控制面单请求,秒级校准——超时默认原则规则 19)。取值对齐
45
46
  * plugin-bridge 启动 sync 的 2s 自愈闸:session_start 首帧可能早于 runtime adapter
@@ -142,25 +143,36 @@ export function createInFlightReporter(opts: InFlightReporterOpts = {}): InFligh
142
143
  sessionId: getSessionId(active),
143
144
  emittedAt: Date.now(),
144
145
  });
145
- let value: unknown;
146
- try {
147
- value = await active.ui.select(SUBAGENT_INFLIGHT_MARKER, [payload], { timeout: selectTimeoutMs });
148
- } catch (err) {
149
- // 通道异常折叠(plugin-bridge callBridge 同款:不静默吞,但只首败 warn)。
150
- value = undefined;
151
- logFailure("select channel threw", err);
152
- }
146
+ // 发送+折叠半边走 protocol 的 callMarkerRpc 原语(D8,fire-and-forget:void 发起
147
+ // 不变):ok:false 四态(cancelled/timeout/channel-error/non-json)统一折叠进下方
148
+ // 延迟重试路径;送达判据 = ack 全等匹配(不是 JSON 消费),留在本侧。原语的失败
149
+ // 留痕经注入的 log 承载本侧「首败 warn / 后续 debug」防刷屏策略。
150
+ // guiCtx = ExtensionContext 的 GuiContext 最小子集(ask-user runRpcInteraction 同款
151
+ // 先例:ui.custom 泛型签名静态不兼容,callMarkerRpc 只读 ui.select)。
152
+ const guiCtx = {
153
+ mode: active.mode,
154
+ hasUI: active.hasUI,
155
+ ui: { select: active.ui.select.bind(active.ui) },
156
+ };
157
+ const result = await callMarkerRpc(guiCtx, SUBAGENT_INFLIGHT_MARKER, payload, {
158
+ timeout: selectTimeoutMs,
159
+ log: primitiveLog,
160
+ });
153
161
  attemptInFlight = false;
154
- if (typeof value === "string" && isInFlightReportAck(value)) {
162
+ if (result.ok && isInFlightReportAck(result.value)) {
155
163
  // 送达确认:清重试与失败计数,补推积压脏帧。
156
164
  failureCount = 0;
157
165
  clearRetryTimer();
158
166
  if (dirty && ctx !== null) kick();
159
167
  return;
160
168
  }
161
- // 失败折叠(resolve undefined = 超时/取消/无路由)→ 延迟重试,累计到顶放弃
162
- //(放弃后镜像按 absent-report 走 errs 推迟,30min 有界,errs-safe 兜底不丢)。
163
- logFailure("no ack (timeout, cancelled, or runtime without marker routing)", value);
169
+ // 失败折叠(resolve undefined = 超时/取消/无路由 / 回包非 ack / 通道异常)→ 延迟
170
+ // 重试,累计到顶放弃(放弃后镜像按 absent-report 走 errs 推迟,30min 有界,
171
+ // errs-safe 兜底不丢)。
172
+ logFailure(
173
+ result.ok ? "no ack (non-ack response)" : `no ack (${result.reason})`,
174
+ result.ok ? result.value : undefined,
175
+ );
164
176
  failureCount += 1;
165
177
  if (failureCount >= maxAttempts) {
166
178
  givenUp = true;
@@ -181,11 +193,17 @@ export function createInFlightReporter(opts: InFlightReporterOpts = {}): InFligh
181
193
  }
182
194
  }
183
195
 
196
+ /** 原语留痕注入(D8):msg/detail 由 callMarkerRpc 产出;防刷屏策略(首败 warn /
197
+ * 后续 debug)留本侧 logFailure。detail 是原语侧小对象,序列化保信息。 */
198
+ function primitiveLog(msg: string, detail?: object): void {
199
+ logFailure(msg, detail === undefined ? undefined : JSON.stringify(detail));
200
+ }
201
+
184
202
  function logFailure(reason: string, detail: unknown): void {
185
203
  if (!firstFailureLogged) {
186
204
  firstFailureLogged = true;
187
205
  logger.warn(`[subagent-inflight] in-flight report failed (${reason}); retrying every ${retryDelayMs}ms (bounded at ${maxAttempts} attempts)`, {
188
- detail: detail instanceof Error ? detail.message : String(detail),
206
+ detail: toErrorMessage(detail),
189
207
  });
190
208
  return;
191
209
  }
@@ -42,6 +42,7 @@ import { getLogger } from "@zhushanwen/pi-extension-logger";
42
42
 
43
43
  // C5①/C5⑦:渲染统一走 core barrel(formatModelList + ModelEntry 类型为 barrel 导出面)
44
44
  import { formatModelList, type ModelEntry } from "@zhushanwen/subagent-core";
45
+ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
45
46
 
46
47
  const logger = getLogger("injector");
47
48
 
@@ -87,7 +88,7 @@ export function setupModelListInjector(pi: ExtensionAPI): void {
87
88
  return { systemPrompt: event.systemPrompt + injection };
88
89
  } catch (err) {
89
90
  logger.error("[model-list-injector] before_agent_start failed", {
90
- reason: err instanceof Error ? err.message : String(err),
91
+ reason: toErrorMessage(err),
91
92
  });
92
93
  }
93
94
  },
@@ -13,6 +13,8 @@ import os from "node:os";
13
13
 
14
14
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
15
15
 
16
+ import { firstContentText } from "@xyz-agent/extension-protocol";
17
+
16
18
  import type { AgentEventLogEntry, DisplayItem, ExecutionStatus } from "@zhushanwen/subagent-core";
17
19
  import { DEFAULT_AGENT_NAME } from "@zhushanwen/subagent-core";
18
20
  import type {
@@ -562,12 +564,13 @@ export function formatTokenStat(
562
564
  /**
563
565
  * renderResult 的文本兜底:从 result.content[0] 提取纯文本。
564
566
  * 多处 tool 的 renderResult 曾各自内联此逻辑,提取后统一调用。
567
+ * 内核收敛至 extension-protocol firstContentText(ext-simplify-17 D9);
568
+ * 本包装保留 content 可选的宽入参形态(tool renderResult 回调契约 content 可缺省)。
565
569
  */
566
570
  export function renderTextFallback(
567
571
  result: { content?: Array<{ type: string; text?: string }> },
568
572
  ): string {
569
- const first = result.content?.[0];
570
- return first?.type === "text" ? (first.text ?? "") : "";
573
+ return firstContentText({ content: result.content ?? [] });
571
574
  }
572
575
 
573
576
  /** Format a single activity line: ToolName(argsPreview). */
@@ -28,7 +28,7 @@ export { SLUG_MAX_LENGTH };
28
28
 
29
29
  // Params schema(跨包契约测试的真实 typebox 校验入口)。
30
30
  //
31
- // action:"start" 的 17 字段(task/slug/agent/model/...)拍平在顶层,不再用 startParam
31
+ // action:"start" 的 16 字段(task/slug/agent/model/...)拍平在顶层,不再用 startParam
32
32
  // 嵌套容器包。原因:弱模型(GLM/DeepSeek)信任 schema 结构信号 > 文本信号,经常省略
33
33
  // startParam 嵌套层把 task/slug 直接平铺到顶层导致调用失败。拍平后 schema 结构与模型
34
34
  // 的自然倾向一致,消除这层误用。task/slug 必填性由 startHandler runtime 校验(flat
@@ -40,7 +40,7 @@ export { SLUG_MAX_LENGTH };
40
40
  // 反映必填性。勿在此基础上继续堆 action 条件逻辑——要加就拆 tool。
41
41
  export const SubagentParams = Type.Object({
42
42
  action: StringEnum(["start", "list", "cancel", "message", "close", "fork-from"], {
43
- description: "Operation: 'start' runs a subagent, 'list' shows subagents, 'cancel' stops a background subagent, 'message' sends a follow-up to any of your subagents (running or idle — an idle one transparently revives and continues on its original session file; one-shot subagents are auto-upgraded to conversation mode on first message), 'close' archives a subagent (immediately when idle; after the current round, or immediately with force:true, when running), 'fork-from' spawns a NEW subagent inheriting an older one's history (recovery for restart-disconnected subagents; the old record is untouched).",
43
+ description: "Operation: 'start' runs a subagent, 'list' shows subagents, 'cancel' stops a background subagent, 'message' sends a follow-up to any of your subagents (running or idle — an idle one transparently revives and continues on its original session file), 'close' archives a subagent (immediately when idle; after the current round, or immediately with force:true, when running), 'fork-from' spawns a NEW subagent inheriting an older one's history (recovery for restart-disconnected subagents; the old record is untouched).",
44
44
  }),
45
45
  // ── action:"start" fields (flattened to top level). task/slug REQUIRED for start. ──
46
46
  // Missing/empty task or slug throws at runtime (startHandler).
@@ -88,40 +88,34 @@ export const SubagentParams = Type.Object({
88
88
  description: 'Override the working directory for the subagent execution. Must be an absolute path (no "~" shorthand, no relative paths); ".." segments are rejected. Defaults to the parent session\'s cwd.',
89
89
  pattern: "^/",
90
90
  })),
91
- conversation: Type.Optional(Type.Boolean({
92
- description:
93
- "Enable continuous chat with this subagent. When true, the subagent stays available after each reply — you can send follow-up messages (action:'message') and it keeps the full conversation context across rounds, with no need to re-spawn or re-explain. " +
94
- "\nUse conversation:true for: multi-round collaboration (iterative review-fix loops, back-and-forth refinement), any task where you expect to send follow-up messages after the initial result. " +
95
- "\nOmit (or false) for: one-shot tasks — single exploration, lookup, file read, code generation that needs no follow-up. The subagent runs once, notifies on completion, and is cleaned up automatically (default). " +
96
- "\nFor long-interval collaboration (each round spaced >5min apart), set conversation:true AND increase idleTimeoutMs to avoid premature timeout. " +
97
- "Cost: a conversation-mode subagent holds resources (memory, and a worktree if enabled) until you explicitly end it with action:'close'. Always close when done.",
98
- })),
91
+ // [modeless 波5] conversation 参数已删除:chatMode 字段消亡后「模式」不存在——
92
+ // 一切 record 永续可续聊(idle 后 message 即续、fork-from 可继承),无模式开关可表达。
99
93
  idleTimeoutMs: Type.Optional(Type.Number({
100
94
  description:
101
- "Idle timeout in milliseconds for conversation-mode subagents. Controls how long an idle subagent (between rounds) stays alive before automatic cleanup. " +
102
- "Default: 300000 (5min). Override for long-interval collaboration where each round is spaced >5min apart. " +
103
- "Pass 0 or a negative value to DISABLE idle cleanup entirely (subagent stays alive until explicitly closed). " +
104
- "Only meaningful with conversation:true; ignored for one-shot subagents.",
95
+ "Idle-recycle cadence for ALL subagents (modeless: every subagent stays continuable — this is NOT a mode switch). Controls how long an idle subagent (between rounds, no activity) stays before being automatically archived. " +
96
+ "Default: 300000 (5min). Raise it for long-interval collaboration where your next message may arrive more than 5min after a round ends. " +
97
+ "Pass 0 or a negative value to DISABLE idle recycling entirely (subagent stays available until you close it). " +
98
+ "Priority: this param > env XYZ_SUBAGENT_IDLE_TIMEOUT_MS > default.",
105
99
  })),
106
100
  engine: Type.Optional(StringEnum(["pi", "zcode"], {
107
101
  description:
108
102
  "Execution engine for this subagent. Omit to inherit the global config. " +
109
103
  "Three-layer priority: this parameter > agent .md frontmatter engine > config.json defaultEngine. " +
110
- "Non-pi engines do not support conversation/fork/worktree (rejected before the subagent is created).",
104
+ "Non-pi engines do not support fork/worktree (rejected before the subagent is created).",
111
105
  })),
112
106
  collect: Type.Optional(StringEnum(["async", "sync"], {
113
107
  description:
114
- "Completion-notification collection mode for one-shot subagents (subagent-sync-collect). " +
108
+ "Completion-notification routing (NOT a record mode batch membership is routing bookkeeping only). " +
115
109
  "Omit to use the config default (currently async). " +
116
- "Use 'sync' when you dispatch >=2 independent one-shot subagents whose results you will combine: " +
117
- "their completions are held until ALL pending sync subagents finish, then delivered as ONE batch " +
118
- "notification (single wake-up, results inline). You may keep dispatching more sync subagents in " +
119
- "later turns they join the same pending batch. " +
120
- "Independent means no member's prompt or work depends on another member's output — dependent " +
121
- "tasks must be chained across messages (one start after the prior completes), never batched. " +
122
- "Use 'async' (or omit) for immediate per-subagent completion notifications. " +
123
- "Incompatible with conversation:truethat combination is rejected immediately before start; " +
124
- "remove either conversation or collect.",
110
+ "'async' = each subagent's completion notifies immediately. " +
111
+ "'sync' = batch wake-up: when you dispatch >=2 independent subagents whose results you will " +
112
+ "combine, their completions are held until ALL pending sync members finish, then delivered as " +
113
+ "ONE batch notification (single wake-up, results inline); when the batch closes, its members " +
114
+ "are automatically archived. Batch members cannot be messaged use action:'fork-from' to " +
115
+ "continue from one instead. You may keep dispatching more sync subagents in later turns " +
116
+ "they join the same pending batch. Independent means no member's prompt or work depends on " +
117
+ "another member's outputdependent tasks must be chained across messages (one start after " +
118
+ "the prior completes), never batched.",
125
119
  })),
126
120
  // action:"list" → listParam OPTIONAL (all fields optional, defaults apply). Ignored by other actions.
127
121
  listParam: Type.Optional(Type.Object({
@@ -147,11 +141,12 @@ export const SubagentParams = Type.Object({
147
141
  })),
148
142
  // action:"message" → messageParam.subagentId + text REQUIRED. Any reachable subagent works —
149
143
  // running joins the in-flight round (D2 打断入队);idle transparently revives on the same
150
- // session file([U4 §3.2.3] 万物可续——形态枚举 gate 消亡);one-shot auto-upgrades to
151
- // conversation mode on first message (SP-5)。description 与实现锚点见 messageHandler。
144
+ // session file([U4 §3.2.3] 万物可续——形态枚举 gate SP-5 升级路径均消亡,message 直接
145
+ // 续聊任何 idle record)。引擎续聊能力轴 gate 保留(core messageHandler 入口)。
146
+ // description 与实现锚点见 messageHandler。
152
147
  messageParam: Type.Optional(Type.Object({
153
148
  subagentId: Type.String({
154
- description: "REQUIRED for action:'message'. The subagentId to message. Any subagent reachable in this session tree works, running or idle: an idle subagent transparently revives on the same id and continues writing its original session file (a one-shot is auto-upgraded to conversation mode on first message); a running subagent has your message interrupt-and-join its in-flight round. Rejections: unknown id, session file held by another live process, a record from a different session tree, workflow-origin records (their results belong to the workflow run), and one-shot records on engines that do not support conversation upgrade.",
149
+ description: "REQUIRED for action:'message'. The subagentId to message. Any subagent reachable in this session tree works, running or idle: an idle subagent transparently revives on the same id and continues writing its original session file; a running subagent has your message interrupt-and-join its in-flight round. Rejections: unknown id, session file held by another live process, a record from a different session tree, workflow-origin records (their results belong to the workflow run), and records on engines that do not support continuation (fork-from or re-dispatch instead).",
155
150
  }),
156
151
  text: Type.String({
157
152
  description: "REQUIRED for action:'message'. The message to send. Whitespace-only throws.",
@@ -162,11 +162,11 @@ action:"list" before action:"start" — a reusable subagent may exist; compactio
162
162
 
163
163
  ## Actions
164
164
 
165
- - action:"start" — run a subagent. Pass task and slug as top-level fields (REQUIRED). Optional: agent, model, thinkingLevel, engine, collect, skillPath, appendSystemPrompt, schema, maxTurns, graceTurns, fork, worktree, cwd, conversation, idleTimeoutMs. Background only: returns a subagentId immediately, notifies on completion.
166
- - action:"message" — send a follow-up to any of your subagents — running or idle (idle revives in place; one-shots become conversation-mode); full context retained. REQUIRED messageParam: { subagentId, text }. The reply auto-notifies.
165
+ - action:"start" — run a subagent. Pass task and slug as top-level fields (REQUIRED). Optional: agent, model, thinkingLevel, engine, collect, skillPath, appendSystemPrompt, schema, maxTurns, graceTurns, fork, worktree, cwd, idleTimeoutMs. Background only: returns a subagentId immediately, notifies on completion.
166
+ - action:"message" — send a follow-up to any of your subagents — running or idle (idle revives in place; full context retained). REQUIRED messageParam: { subagentId, text }. The reply auto-notifies.
167
167
  - action:"close" — archive a subagent (hidden from list, recoverable): idle closes immediately; running finishes the current round first unless force:true (then terminates mid-round). REQUIRED closeParam: { subagentId }.
168
168
  - action:"list" — list subagents. listParam: { includeFinished?, includeWorkflow?, limit? } (all optional; includeWorkflow defaults false — workflow-dispatched subagents are hidden unless true). Read an item's sessionFile for full detail.
169
- - action:"cancel" — stop a background subagent (for conversation-mode use close). REQUIRED cancelParam: { subagentId }.
169
+ - action:"cancel" — stop a background subagent (to archive it instead, use close). REQUIRED cancelParam: { subagentId }.
170
170
  - action:"fork-from" — restart-disconnect recovery: spawn a NEW subagent inheriting the old one's history via --fork. REQUIRED forkFromParam: { sourceSubagentId }. Optional: prompt (continuation; default handover frame). Returns { newSubagentId, sourceSessionFile }. Rejects still-running / foreign-live / worktree-bound sources; unparseable history anchors are guided to action:"message" (same-id reopen).
171
171
 
172
172
  ## Examples
@@ -191,8 +191,8 @@ Completion auto-notifies you (steer wakes the next turn):
191
191
 
192
192
  ## Batch collection (collect)
193
193
 
194
- - collect:"sync" — >=2 independent one-shot subagents whose results you will combine: completions are held until every pending sync member finishes, then ONE batch notification delivers all results inline (one wake-up). Later sync starts join the same batch; each sync start response reports {"collect":{"mode":"sync","pendingSyncCount":N}}.
195
- - collect:"async" (default, omit) — immediate per-subagent completion; for conversational work or when each result is needed early.
194
+ - collect:"sync" — >=2 independent subagents whose results you will combine: completions are held until every pending sync member finishes, then ONE batch notification delivers all results inline (one wake-up) and batch members auto-archive. Later sync starts join the same batch; each sync start response reports {"collect":{"mode":"sync","pendingSyncCount":N}}. Batch members cannot be messaged — fork-from continues from one.
195
+ - collect:"async" (default, omit) — immediate per-subagent completion; use when each result is needed early.
196
196
  - Subagents in one sync batch must not depend on each other's output — dependent tasks must be chained across messages (see Calling patterns), never batched.
197
197
  Items over budget are truncated with a pointer: session_read {"action":"result","session":"<id>"} fetches the full text.
198
198
 
@@ -204,16 +204,15 @@ Items over budget are truncated with a pointer: session_read {"action":"result",
204
204
  - Treating subagent results as authoritative without verification.
205
205
  - Canceling by guessing a subagentId instead of using action:"list" first.
206
206
 
207
- ## Continuous chat (conversation mode)
207
+ ## Continuing a subagent (modeless)
208
208
 
209
- conversation:true keeps a subagent available across replies action:"message" continues with full context, action:"close" releases it (always close when done). For review/fix loops and long-interval rounds (>5min apart, raise idleTimeoutMs); omit for one-shot tasks.
210
- idleTimeoutMs: idle timeout before auto-cleanup (default 300000 / 5min; env XYZ_SUBAGENT_IDLE_TIMEOUT_MS overrides globally, per-call wins).
209
+ Every subagent stays continuable no mode switch: action:"message" revives an idle record in place (or joins a running one's round), action:"fork-from" branches a new subagent from old history, action:"close" archives it.
210
+ idleTimeoutMs: idle-recycle cadence for ALL subagents — idle records auto-archive on expiry (default 300000 / 5min; 0/negative disables; env XYZ_SUBAGENT_IDLE_TIMEOUT_MS: global default, per-call wins).
211
211
 
212
212
  ## You cannot
213
213
 
214
214
  - Get a synchronous/inline result — start always returns a subagentId immediately (background).
215
215
  - Read mid-flight streaming output — wait for the completion notification.
216
- - Combine collect:"sync" with conversation:true — rejected before start; sync is one-shot only (remove one).
217
216
  - See intermediate signals while a sync batch waits — nothing arrives until the whole batch closes. Hung member: action:"list" shows what is still running; action:"cancel" it — cancelled members count as terminal and the batch closes.
218
217
 
219
218
  ## Calling patterns
@@ -90,7 +90,7 @@ import {
90
90
  pruneStateFilesBeyondCap,
91
91
  type RunSnapshot,
92
92
  } from "@zhushanwen/subagent-core";
93
- import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
93
+ import { isEnoentError, toErrorMessage } from "@zhushanwen/pi-ext-guards";
94
94
 
95
95
  // ── Workflow-record self-describing entry (W17, D4) ─────────
96
96
 
@@ -226,16 +226,6 @@ async function loadRunFromStateFile(filePath: string): Promise<WorkflowRun | nul
226
226
 
227
227
  // ── JsonlRunStore ────────────────────────────────────────────
228
228
 
229
- /** Node fs 错误 code 判定(ENOENT = 路径不存在,并发删除场景)。 */
230
- function isEnoentError(err: unknown): boolean {
231
- return (
232
- typeof err === "object" &&
233
- err !== null &&
234
- "code" in err &&
235
- (err as { code: unknown }).code === "ENOENT"
236
- );
237
- }
238
-
239
229
  const logger = getLogger("subagents");
240
230
 
241
231
  /**
@@ -300,7 +300,6 @@ function appendSubagentIdentityEntry(pi: ExtensionAPI): void {
300
300
  process.env.PI_SUBAGENT_FORK_DEPTH !== undefined
301
301
  ? Number(process.env.PI_SUBAGENT_FORK_DEPTH)
302
302
  : undefined,
303
- chatMode: process.env.PI_SUBAGENT_CHAT_MODE === "true",
304
303
  // [review round2] worktree 隔离标志(session-runner 注入):跨重启重建路径据此
305
304
  // 拒绝续聊(handle 不可序列化,reattach 不可行)。
306
305
  worktree: process.env.PI_SUBAGENT_WORKTREE === "true",
@@ -373,7 +372,8 @@ export function bindLedgerHostAndRecover(pi: ExtensionAPI, ctx: ExtensionContext
373
372
  /**
374
373
  * 随迁块 4 的进程级维护三连(各 try-catch「失败记日志不阻断」,设计 §3.4):
375
374
  * 过期 session 文件清理 / ADR-035 manifest tmp 恢复 / ADR-035 worktree reaper 扫描。
376
- * 另含 [E1] sync 批崩溃恢复(per-session 域,不用 oncePerProcess——见块内注释)。
375
+ * ([modeless 波5] 原 [E1] sync 批崩溃恢复接线已摘除——collectMode 记录态消亡后
376
+ * core 侧 recoverSyncCollectBatch 已是 accepted-no-op,调用点随之退役。)
377
377
  */
378
378
  async function runProcessLevelMaintenance(
379
379
  agentDir: string,
@@ -381,22 +381,6 @@ async function runProcessLevelMaintenance(
381
381
  service: SubagentService,
382
382
  deps: SessionLifecycleDeps,
383
383
  ): Promise<void> {
384
- // [E1] sync 批崩溃恢复(subagent-sync-collect 设计 §3.1.5 E1,U5 接线):扫描主
385
- // session 末条 entry 重建批缓冲;全员终态未投递 → notifyBatch 补发 + 统一补
386
- // batchFinalized 标记(账本同 hash 幂等拒绝也算已投递)。须晚于 initSession(孤儿
387
- // 终态恢复先行收敛 running 成员,createOrReuseServices 内部同步完成)与 ledger
388
- // bind(补发走 notifyBatch 写账链,见上方 bindNotifyLedgerHost)。per-session 域
389
- // (主 session 文件),不用 oncePerProcess;best-effort 不阻断 session_start。
390
- // 时序约束在调用点 setupSessionLifecycle 已满足(bindLedgerHostAndRecover 与
391
- // createOrReuseServices 均先于本 helper 调用)。
392
- try {
393
- service.recoverSyncCollectBatch();
394
- } catch (err) {
395
- logger.warn("[subagents] sync collect batch recovery failed", {
396
- reason: toErrorMessage(err),
397
- });
398
- }
399
-
400
384
  try {
401
385
  // 递归扫描 <agentDir>/subagents + unlink 超 TTL 跨 session 文件属进程级维护
402
386
  // ——oncePerProcess 守卫防双跑(u-audit-fix)。