@zhushanwen/subagent-core 0.2.0 → 0.4.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/README.md +45 -2
- package/agents/analyst.md +60 -0
- package/agents/coder.md +69 -0
- package/agents/debugger.md +66 -0
- package/agents/doc-reviewer.md +49 -0
- package/agents/explorer.md +63 -0
- package/agents/general-purpose.md +32 -0
- package/agents/orchestrator.md +61 -0
- package/agents/planner.md +53 -0
- package/agents/researcher.md +67 -0
- package/agents/reviewer.md +73 -0
- package/dist/chunk-4KN4TTG7.js +240 -0
- package/dist/chunk-APZY4IME.js +27 -0
- package/dist/chunk-X7SZ5HLQ.js +66 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
- package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
- package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
- package/dist/execution/engine/engines/zcode/constants.js +43 -1
- package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
- package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
- package/dist/execution/engine/engines/zcode/reader.js +4 -234
- package/dist/execution/engine/paths.js +7 -19
- package/dist/index.cjs +14709 -100
- package/dist/index.d.cts +4053 -200
- package/dist/index.d.ts +4053 -200
- package/dist/index.js +14358 -102
- package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
- package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
- package/package.json +6 -3
- package/src/__tests__/review-fix-loop-script.test.ts +90 -2
- package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
- package/src/__tests__/smoke.test.ts +9 -2
- package/src/execution/__tests__/agent-profile.test.ts +232 -0
- package/src/execution/__tests__/agents-assembly.test.ts +219 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
- package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
- package/src/execution/__tests__/delivery-methods.test.ts +2 -0
- package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
- package/src/execution/__tests__/dialog-queue.test.ts +199 -1
- package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
- package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
- package/src/execution/__tests__/finalize-record.test.ts +106 -0
- package/src/execution/__tests__/gc-timer.test.ts +2 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
- package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
- package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
- package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
- package/src/execution/__tests__/notify-ledger.test.ts +294 -0
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
- package/src/execution/__tests__/record-store.test.ts +83 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
- package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
- package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
- package/src/execution/__tests__/session-pending.test.ts +228 -0
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
- package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
- package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
- package/src/execution/__tests__/spawned-children.test.ts +153 -10
- package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
- package/src/execution/__tests__/timeout-integration.test.ts +7 -7
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
- package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
- package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
- package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
- package/src/execution/agent-registry.ts +225 -1
- package/src/execution/agents-assembly.ts +88 -0
- package/src/execution/concurrency-pool.ts +71 -9
- package/src/execution/dialog-queue.ts +101 -3
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
- package/src/execution/engine/__tests__/registry.test.ts +90 -1
- package/src/execution/engine/engine-discovery.ts +12 -16
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
- package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
- package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
- package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
- package/src/execution/engine/engines/zcode/connection.ts +585 -0
- package/src/execution/engine/engines/zcode/constants.ts +129 -0
- package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
- package/src/execution/engine/engines/zcode/preparer.ts +25 -22
- package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
- package/src/execution/engine/host-task-spec.ts +3 -3
- package/src/execution/engine/port.ts +41 -1
- package/src/execution/engine/registry.ts +53 -0
- package/src/execution/finalize-record.ts +63 -5
- package/src/execution/get-state-handshake.ts +85 -10
- package/src/execution/lifecycle-manager.ts +27 -3
- package/src/execution/manifest-store.ts +53 -62
- package/src/execution/notifier.ts +10 -10
- package/src/execution/notify-ledger.ts +117 -16
- package/src/execution/record-entry.ts +8 -2
- package/src/execution/record-store.ts +29 -15
- package/src/execution/session-pending.ts +213 -62
- package/src/execution/session-runner.ts +974 -128
- package/src/execution/sessions-index.ts +10 -55
- package/src/execution/settled-watchdog.ts +99 -0
- package/src/execution/subagent-actions-core.ts +686 -0
- package/src/execution/subagent-service.ts +396 -49
- package/src/execution/ui-request-handler-factory.ts +27 -4
- package/src/execution/worktree-git-ops.ts +397 -0
- package/src/execution/worktree-manager.ts +72 -10
- package/src/execution/worktree-registry.ts +6 -12
- package/src/index.ts +413 -6
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
- package/src/orchestration/__tests__/args-meta.test.ts +358 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
- package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
- package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
- package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
- package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
- package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
- package/src/orchestration/__tests__/script-generate.test.ts +314 -0
- package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
- package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
- package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
- package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
- package/src/orchestration/agent-opts-resolver.ts +7 -7
- package/src/orchestration/args-meta.ts +198 -0
- package/src/orchestration/error-recovery.ts +416 -146
- package/src/orchestration/execute-agent-call.ts +9 -9
- package/src/orchestration/file-run-store.ts +327 -0
- package/src/orchestration/launcher.ts +35 -26
- package/src/orchestration/lifecycle.ts +355 -79
- package/src/orchestration/models/agent-call.ts +7 -7
- package/src/orchestration/models/budget.ts +5 -5
- package/src/orchestration/models/run-runtime.ts +13 -13
- package/src/orchestration/models/trace.ts +8 -8
- package/src/orchestration/models/workflow-run.ts +27 -27
- package/src/orchestration/models/workflow-script.ts +4 -4
- package/src/orchestration/run-snapshot.ts +266 -0
- package/src/orchestration/script-generate.ts +154 -0
- package/src/orchestration/script-lint.ts +33 -33
- package/src/orchestration/worker-handle.ts +10 -10
- package/src/orchestration/worker-host.ts +10 -10
- package/src/orchestration/worker-script-builder.ts +401 -346
- package/src/orchestration/workflow-files.ts +37 -11
- package/src/orchestration/workflow-run-summary.ts +69 -0
- package/src/orchestration/workflow-script-registry-impl.ts +31 -9
- package/src/shared/__tests__/agent-ref.test.ts +209 -2
- package/src/shared/__tests__/atomic-write.test.ts +267 -0
- package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
- package/src/shared/__tests__/injection-render.test.ts +518 -0
- package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
- package/src/shared/__tests__/resource-discovery.test.ts +3 -2
- package/src/shared/agent-ref.ts +143 -2
- package/src/shared/atomic-write.ts +320 -0
- package/src/shared/bounded-serialize.ts +154 -0
- package/src/shared/injection-render.ts +279 -0
- package/src/shared/meta-parser.ts +41 -1
- package/src/shared/resource-discovery.ts +104 -37
- package/src/shared/resource-meta.ts +14 -0
- package/src/shared/xml-injection.ts +9 -9
- package/workflows/README.md +9 -9
- package/workflows/chain.js +4 -2
- package/workflows/map-reduce.js +5 -3
- package/workflows/parallel.js +5 -3
- package/workflows/review-fix-loop-utils.cjs +14 -3
- package/workflows/review-fix-loop.js +18 -7
- package/workflows/scatter-gather.js +4 -2
- package/dist/chunk-3VOERJPJ.js +0 -22
|
@@ -11,13 +11,21 @@ import type { ExtensionMode } from "./host-mode.ts";
|
|
|
11
11
|
|
|
12
12
|
import type { AgentResult as WorkflowAgentResult } from "../orchestration/models/types.ts";
|
|
13
13
|
import { displayAgentName } from "../shared/agent-ref.ts";
|
|
14
|
+
import { MAX_TIMER_DELAY_MS } from "../shared/timer-delay.ts";
|
|
14
15
|
// D-A10: workflow 侧 AgentResult 映射(executeAndAwait 出口)
|
|
15
16
|
import { mapToWorkflowAgentResult } from "./agent-result-mapper.ts";
|
|
16
17
|
import { removeAliveMarker, findForeignLiveInstance, writeAliveMarker } from "./alive-store.ts";
|
|
17
18
|
import { bestEffort } from "./best-effort.ts";
|
|
18
19
|
// [V2 决策 3] lifecycle-manager idle timer:chatMode 统一投递新 turn disarm(防误杀活进程)。
|
|
19
20
|
// [M3] hasIdleTimer:piAdapter.hasRunningBackground 排除等待续聊(timer armed)的 record。
|
|
20
|
-
|
|
21
|
+
// [T4②] DEFAULT_IDLE_TIMEOUT_MS:deliverMessage 非 EPIPE 失败 re-arm 的防御性兜底时长。
|
|
22
|
+
import {
|
|
23
|
+
acquireActivateLock,
|
|
24
|
+
armIdleTimer,
|
|
25
|
+
disarmIdleTimer,
|
|
26
|
+
hasIdleTimer,
|
|
27
|
+
DEFAULT_IDLE_TIMEOUT_MS,
|
|
28
|
+
} from "./lifecycle-manager.ts";
|
|
21
29
|
import { type ConcurrencyPool,DefaultConcurrencyPool } from "./concurrency-pool.ts";
|
|
22
30
|
import type { DialogGlobalQueue, UiRequestHandler } from "./dialog-queue.ts";
|
|
23
31
|
import {
|
|
@@ -45,11 +53,26 @@ import type { ModelConfigService } from "./model-config-service.ts";
|
|
|
45
53
|
import type { AgentConfig, ModelInfo, ResolvedModel } from "./model-resolver.ts";
|
|
46
54
|
import type { BgNotifyRecord, BgNotifier, NotifierHost } from "./notifier.ts";
|
|
47
55
|
import { createNotifier } from "./notifier.ts";
|
|
56
|
+
import { getBoundNotifyLedger, NOTIFY_LEDGER_CUSTOM_TYPE } from "./notify-ledger.ts";
|
|
48
57
|
import { getSubagentRecordsDir, getSubagentSessionDir } from "./path-encoding.ts";
|
|
49
58
|
import type { StatusFilter } from "./record-store.ts";
|
|
50
59
|
import { RecordStore } from "./record-store.ts";
|
|
51
60
|
import { MAX_FORK_DEPTH } from "./session-context-resolver.ts";
|
|
52
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
getChildByRecord,
|
|
63
|
+
killAllSpawnedChildren,
|
|
64
|
+
killRecordChildWithEscalation,
|
|
65
|
+
registerSpawnedChildForRecord,
|
|
66
|
+
runSpawn,
|
|
67
|
+
spawnedChildren,
|
|
68
|
+
type SessionRunnerContext,
|
|
69
|
+
type SpawnResumeOpts,
|
|
70
|
+
} from "./session-runner.ts";
|
|
71
|
+
import {
|
|
72
|
+
armSettledWatchdog,
|
|
73
|
+
disarmSettledWatchdog,
|
|
74
|
+
SETTLED_WATCHDOG_TIMEOUT_MS,
|
|
75
|
+
} from "./settled-watchdog.ts";
|
|
53
76
|
import { isIdle, isResumable, hasLiveProcessHandle } from "./lifecycle-predicates.ts";
|
|
54
77
|
import { startIdleGc } from "./idle-gc.ts";
|
|
55
78
|
import {
|
|
@@ -117,7 +140,7 @@ interface PiLike {
|
|
|
117
140
|
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" }, // g4-allow: 类型注解——PiLike 接口形状(pi.sendMessage 签面子集),非投递调用
|
|
118
141
|
): void;
|
|
119
142
|
/** 订阅 pi 事件(D8:notifier 的 settled 边沿订阅用 'agent_settled')。
|
|
120
|
-
* pi 0.84.
|
|
143
|
+
* pi 0.84.4 的 on 返回 void 且无 off——退订语义由调用侧 disposed 标志包装兑现。
|
|
121
144
|
* 可选:旧测试 mock pi 可能未实现 on,缺省时 notifier 退化为内核退避路径。 */
|
|
122
145
|
on?(event: "agent_settled", handler: () => void): void;
|
|
123
146
|
}
|
|
@@ -149,7 +172,12 @@ function emitPendingUnregister(
|
|
|
149
172
|
});
|
|
150
173
|
}
|
|
151
174
|
|
|
152
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* Service 构造参数(进程级)。
|
|
177
|
+
*
|
|
178
|
+
* @experimental execution 运行时面(设计 docs/design/subagent-core-sink-design.md §3.3 D6):
|
|
179
|
+
* 一个 minor 周期内允许签名微调,稳定后转常规 semver 承诺。
|
|
180
|
+
*/
|
|
153
181
|
export interface SubagentServiceInit {
|
|
154
182
|
cwd: string;
|
|
155
183
|
/** 配置/模型域 Service(execute 内部调其 resolveModel)。 */
|
|
@@ -190,6 +218,30 @@ export interface SubagentServiceSessionInit {
|
|
|
190
218
|
/** background 优先级(保留 priority 排序机制,单一值)。 */
|
|
191
219
|
const PRIORITY_BACKGROUND = 1000;
|
|
192
220
|
|
|
221
|
+
/** 时间换算常数(settled watchdog 分钟数展示用;与 session-runner 同名常量同语义)。 */
|
|
222
|
+
const MS_PER_SECOND = 1000;
|
|
223
|
+
const SECONDS_PER_MINUTE = 60;
|
|
224
|
+
|
|
225
|
+
/** [T4① / PS-2] notify 门拦截集:disposeAllRecords 因这两类原因关闭的 record,其迟到的
|
|
226
|
+
* kickOffBackground.then 完成回注不得注入新 session——/new、/fork 的决策([v4 A-6])
|
|
227
|
+
* 是「被关 record 的告知改由 list 的 closedReason 表达」,不主动通知;旧门只排除
|
|
228
|
+
* cancelled,parent-new/parent-fork 放行 → 新对话被「Subagent X failed: closed due to
|
|
229
|
+
* parent-new」的僵尸回执 triggerTurn 唤醒,已废弃会话的通知注入新上下文。 */
|
|
230
|
+
const NOTIFY_BLOCKED_CLOSED_REASONS: ReadonlySet<ClosedReason> = new Set(["parent-new", "parent-fork"]);
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* [T4① / PS-2] kickOffBackground.then 完成回注的 notify 门(按 closedReason 白名单放行)。
|
|
234
|
+
*
|
|
235
|
+
* cancelled(cancelBackground 自己 notify)与 parent-new/parent-fork(编排性关闭,
|
|
236
|
+
* 告知由 list 的 closedReason 表达)不放行;其余(undefined = 本路径抢到 CAS 尚未
|
|
237
|
+
* 终态化的迟到回调、user-close/gc 等真实终态)照旧回注。导出供测试与未来调用点复用。
|
|
238
|
+
*/
|
|
239
|
+
export function notifyGateAllowsDelivery(closedReason: ClosedReason | undefined): boolean {
|
|
240
|
+
if (closedReason === undefined) return true;
|
|
241
|
+
if (closedReason === "cancelled") return false;
|
|
242
|
+
return !NOTIFY_BLOCKED_CLOSED_REASONS.has(closedReason);
|
|
243
|
+
}
|
|
244
|
+
|
|
193
245
|
/** 跨进程身份贯穿的 env 名(父进程 spawn 子进程时注入,子进程 initSession 读取)。
|
|
194
246
|
* 仿照 PI_SUBAGENT_FORK_DEPTH 机制,让递归 subagent 的身份(rootSessionId / parentRecordId / depth)
|
|
195
247
|
* 跨进程传递,使主进程 /subagents 能看到完整递归树(设计见 docs/design/recursive-subagent-visibility.md)。
|
|
@@ -214,13 +266,18 @@ interface ResolvedIdentity {
|
|
|
214
266
|
* 执行编排 Service。进程级单例。
|
|
215
267
|
*
|
|
216
268
|
* session_start:
|
|
217
|
-
* 1. modelService = getModelConfigService() ?? new ModelConfigService({
|
|
269
|
+
* 1. modelService = getModelConfigService() ?? new ModelConfigService({cwd, agentDir})
|
|
218
270
|
* 2. service = getSubagentService() ?? new SubagentService({cwd, modelService})
|
|
219
271
|
* 3. modelService.initModel({modelRegistry, sessionId, entries})
|
|
220
272
|
* 4. service.initSession({pi, sessionId})
|
|
221
273
|
*
|
|
222
274
|
* session_shutdown:
|
|
223
275
|
* service.dispose()
|
|
276
|
+
*
|
|
277
|
+
* 第三宿主不经 session_start 流程时改用 createSubagentService(init) 参数注入构造。
|
|
278
|
+
*
|
|
279
|
+
* @experimental execution 运行时面(设计 docs/design/subagent-core-sink-design.md §3.3 D6):
|
|
280
|
+
* 一个 minor 周期内允许签名微调,稳定后转常规 semver 承诺。
|
|
224
281
|
*/
|
|
225
282
|
export class SubagentService {
|
|
226
283
|
private readonly pool: ConcurrencyPool;
|
|
@@ -297,6 +354,13 @@ export class SubagentService {
|
|
|
297
354
|
|
|
298
355
|
private readonly manifestStore: ManifestStore;
|
|
299
356
|
|
|
357
|
+
/**
|
|
358
|
+
* [T1/PS-9] subagent sessionDir(getSubagentSessionDir 推导,与 store 同源同一 rootCwd)。
|
|
359
|
+
* 传给 doFinalizeRecord 的 FinalizeDeps.sessionDir——record.sessionFile 缺失时 finalize
|
|
360
|
+
* 用它做磁盘 identity 反查(marker/alive 清理的依据)。
|
|
361
|
+
*/
|
|
362
|
+
private readonly sessionsDir: string;
|
|
363
|
+
|
|
300
364
|
constructor(init: SubagentServiceInit) {
|
|
301
365
|
this.cwd = init.cwd;
|
|
302
366
|
this.modelService = init.modelService;
|
|
@@ -313,6 +377,7 @@ export class SubagentService {
|
|
|
313
377
|
this.rootCwd = envRootCwd && envRootCwd !== "" ? envRootCwd : init.cwd;
|
|
314
378
|
const sessionsDir = getSubagentSessionDir(this.modelService.getAgentDir(), this.rootCwd);
|
|
315
379
|
const recordsDir = getSubagentRecordsDir(this.modelService.getAgentDir(), this.rootCwd);
|
|
380
|
+
this.sessionsDir = sessionsDir;
|
|
316
381
|
this.manifestStore = new ManifestStore(recordsDir);
|
|
317
382
|
this.store = new RecordStore(sessionsDir, this.manifestStore, this.pi ?? undefined);
|
|
318
383
|
this.notifier = createNotifier(this.piAdapter());
|
|
@@ -361,10 +426,30 @@ export class SubagentService {
|
|
|
361
426
|
if (init.dialogQueue !== undefined) {
|
|
362
427
|
this.dialogQueue = init.dialogQueue;
|
|
363
428
|
}
|
|
364
|
-
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
429
|
+
this.initForkDepthBaseline();
|
|
430
|
+
// [递归可见性] 跨进程身份贯穿(设计 recursive-subagent-visibility.md)。
|
|
431
|
+
// 父进程 spawn 时注入 env 描述「子进程自己的身份」(rootSessionId / selfRecordId /
|
|
432
|
+
// depth / rootCwd),语义与基线建立见 initExecContextBaseline。根进程无 env →
|
|
433
|
+
// sessionRootId = init.sessionId(自己是 root),execCtxAls 不 enterWith(顶层)。
|
|
434
|
+
const envRoot = process.env[ENV_ROOT_SESSION_ID];
|
|
435
|
+
this.sessionRootId = envRoot ?? init.sessionId;
|
|
436
|
+
this.initExecContextBaseline(envRoot, init.sessionId);
|
|
437
|
+
// revive(dispose 的逆操作:/resume /fork /new 后复活)
|
|
438
|
+
this._disposed = false;
|
|
439
|
+
this.store.revive();
|
|
440
|
+
this.notifier.revive();
|
|
441
|
+
// 孤儿终态恢复(放 initSession 末尾:setPi 已注入(appendEntry 可用)、
|
|
442
|
+
// sessionRootId 已建立(过滤当前根的 record);单扫描者判据见 recoverOrphansIfRootProcess)
|
|
443
|
+
this.recoverOrphansIfRootProcess();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* [SPAWN fork depth 跨进程传递] fork 链深度基线:子进程被父 spawn 时,父通过 env
|
|
448
|
+
* PI_SUBAGENT_FORK_DEPTH 传入当前 fork 链深度。子进程 session_start 时读取作为
|
|
449
|
+
* forkDepthAls 基线,使后续嵌套 spawn fork 能从正确深度递增。未设置(顶层主
|
|
450
|
+
* session)→ 基线 0。enterWith 贯穿整个 session 生命周期。
|
|
451
|
+
*/
|
|
452
|
+
private initForkDepthBaseline(): void {
|
|
368
453
|
const envDepth = process.env.PI_SUBAGENT_FORK_DEPTH;
|
|
369
454
|
if (envDepth !== undefined && envDepth !== "") {
|
|
370
455
|
const base = Number.parseInt(envDepth, 10);
|
|
@@ -373,17 +458,14 @@ export class SubagentService {
|
|
|
373
458
|
this.forkDepthBaseline = base;
|
|
374
459
|
}
|
|
375
460
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
// enterWith 贯穿整个 session 生命周期(与 forkDepthAls 同构,决策 4)。
|
|
385
|
-
const envRoot = process.env[ENV_ROOT_SESSION_ID];
|
|
386
|
-
this.sessionRootId = envRoot ?? init.sessionId;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* [递归可见性] exec 上下文基线:子进程读 env PI_SUBAGENT_SELF_RECORD_ID / DEPTH
|
|
465
|
+
* 建立身份基线后,createRecordForMode 读 execCtxAls 自动正确(孙挂到子名下)。
|
|
466
|
+
* enterWith 贯穿整个 session 生命周期(与 forkDepthAls 同构,决策 4)。
|
|
467
|
+
*/
|
|
468
|
+
private initExecContextBaseline(envRoot: string | undefined, sessionId: string): void {
|
|
387
469
|
const envSelfRecord = process.env[ENV_SELF_RECORD_ID];
|
|
388
470
|
if (envSelfRecord !== undefined && envSelfRecord !== "") {
|
|
389
471
|
const envNestingDepth = Number.parseInt(process.env[ENV_DEPTH] ?? "0", 10);
|
|
@@ -394,19 +476,32 @@ export class SubagentService {
|
|
|
394
476
|
this.execCtxAls.enterWith({ recordId: envSelfRecord, depth: nestingDepth });
|
|
395
477
|
if (process.env.XYZ_AGENT_DEBUG) {
|
|
396
478
|
logger.debug(
|
|
397
|
-
`[subagents] execCtxAls initialized: recordId=${envSelfRecord} depth=${nestingDepth} rootSessionId=${envRoot ??
|
|
479
|
+
`[subagents] execCtxAls initialized: recordId=${envSelfRecord} depth=${nestingDepth} rootSessionId=${envRoot ?? sessionId}`,
|
|
398
480
|
);
|
|
399
481
|
}
|
|
400
482
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* 孤儿终态恢复(residual-fixes):session_start 主动触发一次——父扩展死后再无人写
|
|
487
|
+
* 终态 entry 的 record 在此判定落盘(否则侧栏永久 running)。幂等不 throw,失败不
|
|
488
|
+
* 阻断 session_start。
|
|
489
|
+
*
|
|
490
|
+
* [T5① / PS-8] 只有根进程做扫描者:恢复机制假设「单扫描者」,但子进程 sessionRootId
|
|
491
|
+
* 经 env 与父同值(过滤域 = 整树共享的 sessions/records 目录),env 贯穿让每个子进程
|
|
492
|
+
* 都成了扫描者——递归编排中任一子进程启动时,恰有兄弟记录 marker 缺失或超软超时
|
|
493
|
+
*(hours-long wave 必然命中)→ 活记录被无关进程盖 .finalized sidecar,closed entry
|
|
494
|
+
* 写进别的进程的 session 文件(跨进程互写,无任何锁)。子进程身份判据 = env
|
|
495
|
+
* PI_SUBAGENT_SELF_RECORD_ID(父 spawn 时注入的「子进程自己的 record id」,仅子进程
|
|
496
|
+
* 非空)——与 execCtxBaseline 同源。根进程恢复语义不变。
|
|
497
|
+
*/
|
|
498
|
+
private recoverOrphansIfRootProcess(): void {
|
|
499
|
+
const isChildProcess = (process.env[ENV_SELF_RECORD_ID] ?? "") !== "";
|
|
500
|
+
if (!isChildProcess) {
|
|
501
|
+
this.recoverOrphanRecords();
|
|
502
|
+
} else if (process.env.XYZ_AGENT_DEBUG) {
|
|
503
|
+
logger.debug("[subagents] child process detected (PI_SUBAGENT_SELF_RECORD_ID set), skipping orphan recovery scan");
|
|
504
|
+
}
|
|
410
505
|
}
|
|
411
506
|
|
|
412
507
|
/** 孤儿终态恢复委托(RecordStore.recoverOrphanRecords 的唯一公开入口,维持 store
|
|
@@ -448,6 +543,15 @@ export class SubagentService {
|
|
|
448
543
|
* 遍历 store 中所有 running record,逐个 CAS 转终态 + completeRecord + archive。
|
|
449
544
|
* 对有 worktreeHandle 的 record 触发 worktreeManager.cleanup(T3: worktree 绑定清理)。
|
|
450
545
|
*
|
|
546
|
+
* [T2⑥ / PS-1] 补齐三回收面(对照 dispose() 的既有形态,消除同文件双标):
|
|
547
|
+
* controller.abort + kill(收敛到 killChildWithEscalation)+ disarmIdleTimer +
|
|
548
|
+
* disarmSettledWatchdog。旧实现只关 record 不中止执行——「record 已关」≠「执行已
|
|
549
|
+
* 处置」:在途子进程继续跑且无任何用户可及的取消通道(cancel 只查内存 running,
|
|
550
|
+
* archive 后恒 false),若挂死唯一上界是默认关闭的 spawn watchdog → 泄漏至宿主退出。
|
|
551
|
+
* abort/kill/timer 三面对已终态/已死 record 均幂等 no-op,dispose() 先行的
|
|
552
|
+
* abortRunningControllers + killAllSpawnedChildren 不受影响(parent-shutdown 路径
|
|
553
|
+
* 双保险)。
|
|
554
|
+
*
|
|
451
555
|
* [v4 A-6] 旧实现的 recentlyCascaded 收集(供已删除的 before_agent_start 注入告知)
|
|
452
556
|
* 与 drainCascaded 已一并移除——被关 record 的告知改由 list 的 closedReason 表达。
|
|
453
557
|
*
|
|
@@ -458,6 +562,14 @@ export class SubagentService {
|
|
|
458
562
|
const activeRecords = this.store.listAllActive();
|
|
459
563
|
let count = 0;
|
|
460
564
|
for (const record of activeRecords) {
|
|
565
|
+
// [T2⑥/PS-1] 回收面 i:abort 在途 controller(排队的 acquire / 在途 signal listener
|
|
566
|
+
// 立即感知取消)。幂等:已 aborted 的 controller.abort() 是 no-op。
|
|
567
|
+
record.controller?.abort();
|
|
568
|
+
// 回收面 ii:回收子进程(SIGTERM + 30s SIGKILL 升级,收敛 T2④同款)。
|
|
569
|
+
// 回收面 iii:disarm idle timer + settled watchdog(进程回收后 timer 只会误触发)。
|
|
570
|
+
killRecordChildWithEscalation(record.id, `disposeAllRecords (${reason})`);
|
|
571
|
+
disarmIdleTimer(record.id);
|
|
572
|
+
disarmSettledWatchdog(record.id);
|
|
461
573
|
// tryTransition 只对 running 生效;idle 需要直接 completeRecord(无 CAS 保护)。
|
|
462
574
|
// 与 closeChatIdle 对称:idle 无在途 AgentResult,构造合成 result。
|
|
463
575
|
if (record.status === "running") {
|
|
@@ -542,12 +654,51 @@ export class SubagentService {
|
|
|
542
654
|
// [review MF1] 在途 resume 守卫清空(正常由 runAndFinalize finally 清除;此处兜底
|
|
543
655
|
// abort/kill 后仍挂着的条目,防跨 session 复活时残留)
|
|
544
656
|
this.resumesInFlight.clear();
|
|
545
|
-
// flush
|
|
657
|
+
// flush 待发通知(session_shutdown 尽力投递一次)。
|
|
546
658
|
this.notifier.flushPendingNotifications();
|
|
659
|
+
// [T4④ / PS-5] flush 被 isIdle 门拦时的丢失面闭合:attemptDeliver 的 isIdle 二次
|
|
660
|
+
// 复查失败时 pending 挂在 ledger 内存态,随后 dispose 销毁——旧注释「flush 后 dispose
|
|
661
|
+
// (防丢失)」的承诺与行为不符。现把未投递 pending 经 ledger entry 通道复写落盘
|
|
662
|
+
// (notifyId 幂等,重复 entry 由恢复扫描后写覆盖吸收),重启 recoverFromSession 按
|
|
663
|
+
// 账面差集重放补投——「不丢」由落盘账本承接而非本次 flush。
|
|
664
|
+
this.persistUndeliveredNotificationsForReplay();
|
|
547
665
|
this.notifier.dispose();
|
|
548
666
|
this.store.dispose();
|
|
549
667
|
}
|
|
550
668
|
|
|
669
|
+
/**
|
|
670
|
+
* [T4④ / PS-5] shutdown flush 被门拦时把未投递 pending 复写落盘(供重启 replay)。
|
|
671
|
+
*
|
|
672
|
+
* 触发条件:flushPendingNotifications 后 ledger 仍有 pending(isIdle 门拦 / sendDelivery
|
|
673
|
+
* 受理失败的残留)且主 agent 非 idle——即本次 shutdown 注定投不出去。落盘动作 =
|
|
674
|
+
* pi.appendEntry 重写 NOTIFY_LEDGER_CUSTOM_TYPE entry(与 ledger.record 同通道同 schema,
|
|
675
|
+
* notifyId 幂等:恢复扫描按后写覆盖 + ack/abandoned 差集去重,重复账面不产生重复投递)。
|
|
676
|
+
* 主 agent idle 时 flush 已投出,无需复写(零开销)。
|
|
677
|
+
*/
|
|
678
|
+
private persistUndeliveredNotificationsForReplay(): void {
|
|
679
|
+
try {
|
|
680
|
+
const ledger = getBoundNotifyLedger();
|
|
681
|
+
const pending = ledger?.pendingEntries() ?? [];
|
|
682
|
+
if (pending.length === 0) return;
|
|
683
|
+
if (this.isIdleFn?.() !== false) return; // idle:flush 已投出(或无 gate 场景照旧)
|
|
684
|
+
for (const item of pending) {
|
|
685
|
+
this.pi?.appendEntry(NOTIFY_LEDGER_CUSTOM_TYPE, {
|
|
686
|
+
v: 1,
|
|
687
|
+
notifyId: item.notifyId,
|
|
688
|
+
content: item.content,
|
|
689
|
+
record: item.record,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
logger.warn(
|
|
693
|
+
`[subagents] shutdown flush blocked by busy main agent: ${pending.length} pending notification(s) persisted to ledger for replay on next session_start`,
|
|
694
|
+
{ count: pending.length },
|
|
695
|
+
);
|
|
696
|
+
} catch (err) {
|
|
697
|
+
// 落盘复写是防丢失增强,失败不阻断 dispose(账本原 entry 仍随 pi flush 落盘)。
|
|
698
|
+
bestEffort(err, "persistUndeliveredNotificationsForReplay", "error");
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
551
702
|
// ── 执行(subagent-tool 调)────────────────────────────
|
|
552
703
|
|
|
553
704
|
/** background 完成回注(record → BgNotifyRecord 映射 + notifier.notify)。
|
|
@@ -674,6 +825,8 @@ export class SubagentService {
|
|
|
674
825
|
*/
|
|
675
826
|
async execute(opts: ExecuteOptions): Promise<ExecutionHandle> {
|
|
676
827
|
this.assertReady();
|
|
828
|
+
// [T4② / PS-4] idleTimeoutMs 配置错误在首个副作用前同步 fail-fast(错误含合法范围)。
|
|
829
|
+
this.assertIdleTimeoutMsSafe(opts);
|
|
677
830
|
|
|
678
831
|
// 通用嵌套深度护栏(D-033):execCtxAls 记录所有 subagent 嵌套层级(fork + 非 fork),
|
|
679
832
|
// 每层 +1。MAX_FORK_DEPTH 同时限 fork 链与通用嵌套——非 fork 递归虽不累积 session 体积,
|
|
@@ -1014,6 +1167,14 @@ export class SubagentService {
|
|
|
1014
1167
|
record.result = undefined;
|
|
1015
1168
|
record.resumable = undefined;
|
|
1016
1169
|
this.store.reportRecordTransition(record);
|
|
1170
|
+
// [T2③ / LC-1] 热路径轮 settled 等待固定硬上限(双挂载原语之热路径调用点,
|
|
1171
|
+
// 首轮调用点在 session-runner runSpawn)。prompt 发出即起算(整轮含 turn 执行与
|
|
1172
|
+
// 收尾都在窗口内,impl-plan §5 窗口口径裁决),settled 到达(session-runner
|
|
1173
|
+
// handleSdkEvent 的 disarm,幂等生效于热路径轮)/ 进程 close(waitForChildExit
|
|
1174
|
+
// 的 disarm)/ 本方终态化处置后即清——settled 永不到达(事件行丢失 / 子进程
|
|
1175
|
+
// wedged)时本 timer 是唯一独立回收通道:现状「settled 不 arm 则 idle timer 不挂、
|
|
1176
|
+
// runSpawn 已在首轮返回、spawn watchdog 默认关」的三无窗口由此收敛。
|
|
1177
|
+
armSettledWatchdog(record.id, () => this.onHotPathSettledWatchdogTimeout(record));
|
|
1017
1178
|
} catch (err) {
|
|
1018
1179
|
// EPIPE 兜底:stdin 管道已断,进程实际已死但 close 事件尚未到达。
|
|
1019
1180
|
// 检测 EPIPE 关键词 → 进程按 dead 处理 → 自动转冷路径 resume + 消息重放。
|
|
@@ -1045,6 +1206,13 @@ export class SubagentService {
|
|
|
1045
1206
|
this.resumeRound(record, text);
|
|
1046
1207
|
return;
|
|
1047
1208
|
}
|
|
1209
|
+
// [T2⑧ / PS-3] 非 EPIPE 写失败(如 ERR_STREAM_DESTROYED——错误分类只认 "EPIPE"
|
|
1210
|
+
// 子串,过窄):deliverMessage 入口已无条件 disarmIdleTimer,旧实现直接 rethrow
|
|
1211
|
+
// 不 re-arm——「timer armed」这个防泄漏前提在失败路径永久丢失:进程无人回收
|
|
1212
|
+
//(无 timer、无轮、record=running)泄漏至宿主退出,hasRunningBackground 恒真
|
|
1213
|
+
// 还会顺延其他完成通知。修复:rethrow 前再武装 idle timer(含防御性降级),恢复
|
|
1214
|
+
// 进程回收通道 + 通知放行门;调用方错误语义不变(仍 rethrow)。
|
|
1215
|
+
this.rearmIdleTimerAfterHotPathFailure(record, err);
|
|
1048
1216
|
// 非 EPIPE 错误——不应发生,重新抛出让调用方处理
|
|
1049
1217
|
throw err;
|
|
1050
1218
|
}
|
|
@@ -1063,6 +1231,76 @@ export class SubagentService {
|
|
|
1063
1231
|
}
|
|
1064
1232
|
}
|
|
1065
1233
|
|
|
1234
|
+
/**
|
|
1235
|
+
* [T2③] 热路径轮 settled watchdog 到期处置(对齐 u-t2a 首轮形态:kill + 该轮失败
|
|
1236
|
+
* 终态化 + 失败通知,error 含 'settled watchdog' 标记与恢复指引)。
|
|
1237
|
+
*
|
|
1238
|
+
* 与首轮的差异:runSpawn 已返回(无收尾链路承接 settledWatchdogFired 标记),失败
|
|
1239
|
+
* 终态化在本回调内完成。chatMode 按 MF-6 语义回退 running-resumable(与首轮 watchdog
|
|
1240
|
+
* 经 runAndFinalize 失败分支的最终形态一致——对话可冷路径复活);非 chatMode 终态
|
|
1241
|
+
* 销毁。CAS(tryTransition closed+gc)防与 cancel/dispose 双收尾,抢锁失败即跳过。
|
|
1242
|
+
*
|
|
1243
|
+
* 回调在 timer 触发的同步上下文执行:同步段只做 kill + CAS(不抛),异步收尾
|
|
1244
|
+
* fire-and-forget 且 catch 归 bestEffort——错误逃出回调 = uncaughtException 崩宿主。
|
|
1245
|
+
*/
|
|
1246
|
+
private onHotPathSettledWatchdogTimeout(record: ExecutionRecord): void {
|
|
1247
|
+
logger.warn(
|
|
1248
|
+
`[subagents] settled watchdog fired for ${record.id}: no agent_settled within ` +
|
|
1249
|
+
`${SETTLED_WATCHDOG_TIMEOUT_MS / MS_PER_SECOND / SECONDS_PER_MINUTE} min of hot-path prompt, terminating (LC-1 wedge recovery)`,
|
|
1250
|
+
);
|
|
1251
|
+
killRecordChildWithEscalation(record.id, "settled watchdog (hot path)");
|
|
1252
|
+
const failedResult: AgentResult = {
|
|
1253
|
+
text: "",
|
|
1254
|
+
turns: record.turnCount,
|
|
1255
|
+
durationMs: Date.now() - record.startedAt,
|
|
1256
|
+
success: false,
|
|
1257
|
+
error:
|
|
1258
|
+
`subagent did not reach agent_settled within ${SETTLED_WATCHDOG_TIMEOUT_MS / MS_PER_SECOND / SECONDS_PER_MINUTE} min (settled watchdog); ` +
|
|
1259
|
+
`the process was terminated to bound the wait. ` +
|
|
1260
|
+
`Recovery: check state with subagents action:'list', then re-send your message to continue.`,
|
|
1261
|
+
sessionId: record.id,
|
|
1262
|
+
toolCalls: [],
|
|
1263
|
+
};
|
|
1264
|
+
if (!tryTransition(record, "closed", "gc")) {
|
|
1265
|
+
return; // 已被 cancel/dispose 抢先终态化——不重复收尾(watchdog disarm 由对方承接)
|
|
1266
|
+
}
|
|
1267
|
+
const finalize = record.chatMode
|
|
1268
|
+
? this.finalizeRoundToIdle(record, failedResult)
|
|
1269
|
+
: this.finalizeRecord(record, failedResult, "closed", "gc");
|
|
1270
|
+
void finalize
|
|
1271
|
+
.then(() => this.notifyComplete(record))
|
|
1272
|
+
.catch((err: unknown) => bestEffort(err, "settled watchdog hot-path finalize", "error"));
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* [T2⑧ / PS-3] 非 EPIPE 热路径失败后的 idle timer 再武装(防泄漏底线)。
|
|
1277
|
+
*
|
|
1278
|
+
* record.idleTimeoutMs 已在 spawn 入口经 assertIdleTimeoutMsSafe 校验(T4②),此处
|
|
1279
|
+
* armIdleTimer 理论不 throw;降级链仍保底:非法 → 挂 DEFAULT_IDLE_TIMEOUT_MS + warn
|
|
1280
|
+
* (兜底可见,对齐 session-runner agent_settled 侧的 T4② 降级形态),双重失败退回
|
|
1281
|
+
* 「不挂」但留 error 痕。
|
|
1282
|
+
*/
|
|
1283
|
+
private rearmIdleTimerAfterHotPathFailure(record: ExecutionRecord, cause: unknown): void {
|
|
1284
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
1285
|
+
const onTimeout = (): void => {
|
|
1286
|
+
killRecordChildWithEscalation(record.id, "idle timer (hot-path failure fallback)");
|
|
1287
|
+
};
|
|
1288
|
+
try {
|
|
1289
|
+
armIdleTimer(record.id, onTimeout, record.idleTimeoutMs);
|
|
1290
|
+
} catch {
|
|
1291
|
+
try {
|
|
1292
|
+
armIdleTimer(record.id, onTimeout, DEFAULT_IDLE_TIMEOUT_MS);
|
|
1293
|
+
} catch (fallbackErr) {
|
|
1294
|
+
bestEffort(fallbackErr, "rearmIdleTimer fallback (deliverMessage non-EPIPE failure)", "error");
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
logger.warn(
|
|
1299
|
+
`[subagents] deliverMessage hot path failed for ${record.id}; idle timer re-armed to keep process recovery bounded`,
|
|
1300
|
+
{ detail },
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1066
1304
|
// ── 对话模式 message/close action 支持(M2-B3)──────────────
|
|
1067
1305
|
|
|
1068
1306
|
/**
|
|
@@ -1126,15 +1364,35 @@ export class SubagentService {
|
|
|
1126
1364
|
* @returns 重建的 record;磁盘也无则 undefined
|
|
1127
1365
|
* @throws ResurrectDeniedError 可重连候选被 worktree/异进程活实例守卫拦截 */
|
|
1128
1366
|
/** 冷查候选定位(coldLookupForAction 步骤 1):idToFile 索引直查 running 命中,
|
|
1129
|
-
* 未命中再全目录 collectRecords 兜底(running,或 allowReconnect 且可重连 closed)。
|
|
1367
|
+
* 未命中再全目录 collectRecords 兜底(running,或 allowReconnect 且可重连 closed)。
|
|
1368
|
+
*
|
|
1369
|
+
* [T5③ / PS-7b] running 候选异进程活实例守卫:冷查 running 候选(跨重启 / 内存重建)
|
|
1370
|
+
* 此前不经任何探针直接 resurrect + resume spawn——若其 .alive marker 仍指向活着的
|
|
1371
|
+
* 异进程实例(父进程重启后旧子进程尚存的窗口),resume 会 spawn 第二个 pi 子进程
|
|
1372
|
+
* 写同一 session JSONL(本代码最忌惮的双写者形态,v4 A-5/P7 事故模式)。closed 候选
|
|
1373
|
+
* 的同款守卫已在 assertReconnectAllowed(v8.5 D);本守卫闭合 running 候选的防御
|
|
1374
|
+
* 不对称。marker 的 pid 是子进程 pi 的 pid(非父进程),本进程持有的 running record
|
|
1375
|
+
* 恒在内存(archive 才移出),可达本冷查分支的 running 候选必然来自磁盘重建——
|
|
1376
|
+
* 探针命中即拒绝(ResurrectDeniedError,与 closed 候选守卫同异常类型,错误含 pid
|
|
1377
|
+
* 与恢复指引)。 */
|
|
1130
1378
|
private findColdLookupCandidate(id: string, allowReconnect: boolean): SubagentRecord | undefined {
|
|
1131
1379
|
const direct = this.store.findLightById(id);
|
|
1132
|
-
|
|
1380
|
+
const found =
|
|
1133
1381
|
(direct?.status === "running" ? direct : undefined) ??
|
|
1134
1382
|
this.store
|
|
1135
1383
|
.collectRecords(COLD_LOOKUP_SCAN_LIMIT, "all", undefined)
|
|
1136
|
-
.find((r) => r.id === id && (r.status === "running" || (allowReconnect && this.isReconnectableClosed(r))))
|
|
1137
|
-
)
|
|
1384
|
+
.find((r) => r.id === id && (r.status === "running" || (allowReconnect && this.isReconnectableClosed(r))));
|
|
1385
|
+
if (found?.status === "running" && found.sessionFile) {
|
|
1386
|
+
const foreign = findForeignLiveInstance(found.sessionFile);
|
|
1387
|
+
if (foreign) {
|
|
1388
|
+
throw new ResurrectDeniedError(
|
|
1389
|
+
`subagent ${id} is currently running in another process instance (pid ${foreign.pid}, ` +
|
|
1390
|
+
`startedAt=${new Date(foreign.startedAt).toISOString()}); resuming here would double-write ${found.sessionFile}. ` +
|
|
1391
|
+
`Recovery: retry once that process exits; if it never exits, action:'close' this subagent, then action:'start' a fresh one.`,
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return found;
|
|
1138
1396
|
}
|
|
1139
1397
|
|
|
1140
1398
|
/** 可重连守卫(coldLookupForAction 步骤 2,[v8.5 D]):先于任何状态突变与注册。
|
|
@@ -1304,10 +1562,14 @@ export class SubagentService {
|
|
|
1304
1562
|
* completeRecord 直接覆盖 status,与 cancelBackground 对 record 的处理同构)。
|
|
1305
1563
|
*/
|
|
1306
1564
|
private async closeChatIdle(record: ExecutionRecord): Promise<void> {
|
|
1307
|
-
// [M5] Path A:回收保活进程 + disarm idle timer(终态化后无其他 kill
|
|
1565
|
+
// [M5] Path A:回收保活进程 + disarm idle timer(终态化后无其他 kill 路径)。
|
|
1566
|
+
// [T2④ / LC-2] 裸 SIGTERM 收敛到 killRecordChildWithEscalation:SIGTERM 被无视时
|
|
1567
|
+
// 30s 升级 SIGKILL——终态化后 record 已 archive,此前裸 SIGTERM 挂住 = 幽灵进程
|
|
1568
|
+
// 无任何后续回收通道。同时 disarm settled watchdog(本路径终态化 = settled 等待
|
|
1569
|
+
// 窗口终结,防 watchdog 误杀后续同 id 资源)。
|
|
1308
1570
|
disarmIdleTimer(record.id);
|
|
1309
|
-
|
|
1310
|
-
|
|
1571
|
+
disarmSettledWatchdog(record.id);
|
|
1572
|
+
killRecordChildWithEscalation(record.id, "closeChatIdle");
|
|
1311
1573
|
// 合成 closed result(无在途 AgentResult,对齐 closeAfterRoundSettled 的
|
|
1312
1574
|
// `record.result ?? ""` 模式)。[W16 P-1 修复] text 必须沿用轮终真实 result:
|
|
1313
1575
|
// completeRecord 会执行 record.result = result.text,合成空串会把轮终真实值抹空,
|
|
@@ -1329,6 +1591,7 @@ export class SubagentService {
|
|
|
1329
1591
|
modelService: this.modelService,
|
|
1330
1592
|
pi: this.pi,
|
|
1331
1593
|
emitUnregister: (id, st) => emitPendingUnregister(this.pi, id, st),
|
|
1594
|
+
sessionDir: this.sessionsDir,
|
|
1332
1595
|
},
|
|
1333
1596
|
record,
|
|
1334
1597
|
doneResult,
|
|
@@ -1358,10 +1621,12 @@ export class SubagentService {
|
|
|
1358
1621
|
* 60s dedup 吞(不与下方终态通知叠加成第三条——后者 key 是裸 id)。
|
|
1359
1622
|
*/
|
|
1360
1623
|
private async closeAfterRoundSettled(record: ExecutionRecord): Promise<void> {
|
|
1361
|
-
// 回收保活进程(Path A:轮次完成后进程仍活)+ disarm idle timer(终态化后无其他 kill
|
|
1624
|
+
// 回收保活进程(Path A:轮次完成后进程仍活)+ disarm idle timer(终态化后无其他 kill 路径)。
|
|
1625
|
+
// [T2④ / LC-2] 裸 SIGTERM 收敛到 killRecordChildWithEscalation(30s 升级 SIGKILL,
|
|
1626
|
+
// 终态化后无后续回收通道);settled 等待窗口同步终结(disarm 幂等)。
|
|
1362
1627
|
disarmIdleTimer(record.id);
|
|
1363
|
-
|
|
1364
|
-
|
|
1628
|
+
disarmSettledWatchdog(record.id);
|
|
1629
|
+
killRecordChildWithEscalation(record.id, "closeAfterRoundSettled");
|
|
1365
1630
|
if (!tryTransition(record, "closed", "user-close")) {
|
|
1366
1631
|
return; // 已被 cancel/finalize 抢先(CAS 失败),标志已消费即可——不发终态通知(幂等)
|
|
1367
1632
|
}
|
|
@@ -1398,6 +1663,8 @@ export class SubagentService {
|
|
|
1398
1663
|
stream?: SubagentStream,
|
|
1399
1664
|
): Promise<WorkflowAgentResult> {
|
|
1400
1665
|
this.assertReady();
|
|
1666
|
+
// [T4② / PS-4] 与 execute() 同款入口校验(两入口共享 runAndFinalize → armIdleTimer 链)。
|
|
1667
|
+
this.assertIdleTimeoutMsSafe(opts);
|
|
1401
1668
|
|
|
1402
1669
|
// ── BC-12 嵌套护栏:复用 execute() 的 execCtxAls 深度检查 ──
|
|
1403
1670
|
// [ALS 断裂修复] getStore() 可能读空,基线兜底(与 execute 同)。
|
|
@@ -1723,6 +1990,23 @@ export class SubagentService {
|
|
|
1723
1990
|
const retargetJournal = (poolKey: string): void => {
|
|
1724
1991
|
journal.retarget(resolveJournalPath(getEngineDataDir(), engine.id, poolKey, record.id));
|
|
1725
1992
|
};
|
|
1993
|
+
// [R4 §3.4 不变量 3] 运行中句柄回填:create 应答后(远早于 run resolve)回填
|
|
1994
|
+
// record.engineHandle 并经 reportRecordTransition 落 entry——运行中的 GUI 经 entry
|
|
1995
|
+
// 重建 record 即拿到 ①②级读取钥匙(sessionRef/dbPath/poolKey/journalPath),
|
|
1996
|
+
// 不再等终态回填(详情页中途打开可见当时进度快照)。journalPath 此时已是
|
|
1997
|
+
// retarget 后的最终路径(onPoolResolved 在 prepare 期先行触发)。仅 chat 域接线
|
|
1998
|
+
// ——workflow 域 SAR 无运行中 record 读取方,刻意不做同类回填(防误扩展)。
|
|
1999
|
+
const backfillEngineHandle = (partial: { sessionRef: Record<string, string>; poolKey: string }): void => {
|
|
2000
|
+
if (record.engineHandle !== undefined && record.engineHandle.sessionRef["sessionId"] !== undefined) {
|
|
2001
|
+
return; // 幂等守卫:终态回填已落(迟到重复回调不覆盖)
|
|
2002
|
+
}
|
|
2003
|
+
record.engineHandle = {
|
|
2004
|
+
sessionRef: partial.sessionRef,
|
|
2005
|
+
poolKey: partial.poolKey,
|
|
2006
|
+
journalPath: journal.path,
|
|
2007
|
+
};
|
|
2008
|
+
this.store.reportRecordTransition(record);
|
|
2009
|
+
};
|
|
1726
2010
|
// 对齐点③:journal 路径权威 = 引擎声明的池 key(writer 初始用占位,retarget 后
|
|
1727
2011
|
// 与 handle.poolKey 同源)。模式对齐 SAR 的 journalingOnEvent:先落盘再转发。
|
|
1728
2012
|
const runCtx: RunContext = {
|
|
@@ -1732,6 +2016,7 @@ export class SubagentService {
|
|
|
1732
2016
|
ctxModel: opts.ctxModel,
|
|
1733
2017
|
onEvent: (event) => journal.append(event),
|
|
1734
2018
|
onPoolResolved: retargetJournal,
|
|
2019
|
+
onHandleReady: backfillEngineHandle,
|
|
1735
2020
|
// D9①:路由层 fallback 留痕投影进 outcome(zcode 无独立 record 通路)
|
|
1736
2021
|
...(record.engineFallback !== undefined ? { engineFallback: record.engineFallback } : {}),
|
|
1737
2022
|
// D10 终止链:engine spawn 的子进程注册进 spawnedChildren 记账
|
|
@@ -1740,7 +2025,8 @@ export class SubagentService {
|
|
|
1740
2025
|
};
|
|
1741
2026
|
try {
|
|
1742
2027
|
const { handle, outcome } = await engine.run(executeOptionsToEngineTaskSpec(opts), runCtx);
|
|
1743
|
-
// engineHandle
|
|
2028
|
+
// engineHandle 终态回填(U2:终态迁移落 entry 前;R4 起为兜底面——运行中回填
|
|
2029
|
+
// 已由 onHandleReady 提前落 entry,此处覆写终态权威值)。sessionRef 整体透传——
|
|
1744
2030
|
// 失败终态 sessionId 缺失时也回填已有部分(dbPath/poolKey),读侧①级降②级
|
|
1745
2031
|
// 的防御形态;journalPath 取 retarget 后的实际落盘路径(writer 是路径权威)。
|
|
1746
2032
|
record.engineHandle = {
|
|
@@ -1972,9 +2258,11 @@ export class SubagentService {
|
|
|
1972
2258
|
undefined, stream, resume,
|
|
1973
2259
|
)
|
|
1974
2260
|
.then(() => {
|
|
1975
|
-
// background 回注:仅当本路径抢到 CAS
|
|
1976
|
-
//
|
|
1977
|
-
|
|
2261
|
+
// background 回注:仅当本路径抢到 CAS 才 notify。cancel 抢先时 closedReason=
|
|
2262
|
+
// 'cancelled'(cancelBackground 自己 notify);[T4①/PS-2] parent-new/parent-fork
|
|
2263
|
+
// 是 disposeAllRecords 的编排性关闭(record 已关、告知由 list 的 closedReason
|
|
2264
|
+
// 表达)——迟到的完成回注不注入(可能已切换的)新 session。
|
|
2265
|
+
if (notifyGateAllowsDelivery(record.closedReason)) {
|
|
1978
2266
|
this.notifyComplete(record);
|
|
1979
2267
|
}
|
|
1980
2268
|
})
|
|
@@ -1990,7 +2278,15 @@ export class SubagentService {
|
|
|
1990
2278
|
});
|
|
1991
2279
|
}
|
|
1992
2280
|
|
|
1993
|
-
/**
|
|
2281
|
+
/**
|
|
2282
|
+
* 取消 background record。CAS 抢锁(tryTransition)——抢到则 notify + 写 tombstone;
|
|
2283
|
+
* 没抢到(detached 已 finalize,record 已终态)返回 false,不触碰任何收尾副作用。
|
|
2284
|
+
*
|
|
2285
|
+
* stop 手段(abort/kill/disarm)无条件先执行:对已终态 record 幂等无害,且保证
|
|
2286
|
+
* cancel 语义 = 进程必死;收尾副作用(completeRecord/tombstone/archive/notify)只归
|
|
2287
|
+
* CAS 赢家。[A2-1] 此前 CAS 被误删,cancel 可在 doFinalizeRecord Step 0 await 窗口
|
|
2288
|
+
* 命中已终态 record——覆写终态 + tombstone/finalized 双标 + notify 双发 + 谎报 true。
|
|
2289
|
+
*/
|
|
1994
2290
|
private cancelBackground(record: ExecutionRecord): boolean {
|
|
1995
2291
|
record.controller?.abort();
|
|
1996
2292
|
// [M6] 显式 kill + disarm:chatMode 首轮 agent_settled 后 runSpawn 提前 resolveRun(0)
|
|
@@ -1999,11 +2295,18 @@ export class SubagentService {
|
|
|
1999
2295
|
// 进 runSpawn。此后 cancel 只有 controller.abort() 无人响应——record 已终态化 cancelled
|
|
2000
2296
|
// 但子进程继续跑完当前 turn(工具副作用继续发生),之后 agent_settled 还对已 archived
|
|
2001
2297
|
// record 触发脏通知(round+1 → 新 dedup key → "finished a round"),最终靠 5min idle
|
|
2002
|
-
// timer 兜底 kill。故 cancel 必须显式
|
|
2298
|
+
// timer 兜底 kill。故 cancel 必须显式 kill + disarm idle timer。非 chatMode 路径
|
|
2003
2299
|
// listener 仍在(abort 已 kill 一次),此处对已 killed child 是 no-op,无副作用。
|
|
2004
|
-
|
|
2005
|
-
|
|
2300
|
+
// [T2④ / LC-2] 裸 SIGTERM 收敛到 killRecordChildWithEscalation:SIGTERM 被无视时
|
|
2301
|
+
// 30s 升级 SIGKILL——cancel 语义 = 进程必死,不能赌 SIGTERM 生效(record 已终态化,
|
|
2302
|
+
// 此后无其他回收通道)。settled watchdog 同步撤下(等待窗口随取消终结,幂等)。
|
|
2303
|
+
killRecordChildWithEscalation(record.id, "cancelBackground");
|
|
2006
2304
|
disarmIdleTimer(record.id);
|
|
2305
|
+
disarmSettledWatchdog(record.id);
|
|
2306
|
+
// [A2-1] CAS 抢锁防重复收尾:running 才放行。doFinalizeRecord Step 0 await 窗口内
|
|
2307
|
+
// record 可能已被终态化(closed)但尚未 archive——没抢到说明 detached 已 finalize,
|
|
2308
|
+
// cancel 来晚了,闭嘴返回 false(completeRecord 会覆写终态 + tombstone/finalized
|
|
2309
|
+
// 双标 + notify 双发,绝不可执行)。
|
|
2007
2310
|
if (!tryTransition(record, "closed", "cancelled")) {
|
|
2008
2311
|
return false; // detached 已 finalize,cancel 来晚了
|
|
2009
2312
|
}
|
|
@@ -2061,6 +2364,7 @@ export class SubagentService {
|
|
|
2061
2364
|
modelService: this.modelService,
|
|
2062
2365
|
pi: this.pi,
|
|
2063
2366
|
emitUnregister: (id, st) => emitPendingUnregister(this.pi, id, st),
|
|
2367
|
+
sessionDir: this.sessionsDir,
|
|
2064
2368
|
},
|
|
2065
2369
|
record,
|
|
2066
2370
|
result,
|
|
@@ -2141,6 +2445,34 @@ export class SubagentService {
|
|
|
2141
2445
|
}
|
|
2142
2446
|
}
|
|
2143
2447
|
|
|
2448
|
+
/**
|
|
2449
|
+
* [T4② / PS-4] idleTimeoutMs 合法域入口校验(>2^31-1 / 非有限值 fail-fast)。
|
|
2450
|
+
*
|
|
2451
|
+
* 旧链路:非法值穿透到 agent_settled 回调里的 armIdleTimer → assertSafeTimerDelay
|
|
2452
|
+
* throw 被异步 catch 降级——配置错误被吞成静默语义变更(每轮完成通知被 isIdle 放行门
|
|
2453
|
+
* 吞 + 进程无回收 timer)。对齐 shared/timer-delay「不静默 clamp」既有裁决:配置错误
|
|
2454
|
+
* 显式暴露,错误消息含合法范围(0/负数 = 显式禁用是合法语义,不在本校验域;env 非法值
|
|
2455
|
+
* 由 lifecycle-manager 的 warn 回落承接)。execute/executeAndAwait 两入口共用。
|
|
2456
|
+
*/
|
|
2457
|
+
private assertIdleTimeoutMsSafe(opts: ExecuteOptions): void {
|
|
2458
|
+
const v = opts.idleTimeoutMs;
|
|
2459
|
+
if (v === undefined) return;
|
|
2460
|
+
if (!Number.isFinite(v)) {
|
|
2461
|
+
throw new Error(
|
|
2462
|
+
`subagent idleTimeoutMs = ${v} is not a finite number (NaN/±Infinity). ` +
|
|
2463
|
+
`Valid range: a positive millisecond number up to ${MAX_TIMER_DELAY_MS} (2^31-1, Node setTimeout limit), or 0/negative to disable idle GC. ` +
|
|
2464
|
+
"Recovery: fix the idleTimeoutMs value and retry.",
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
if (v > MAX_TIMER_DELAY_MS) {
|
|
2468
|
+
throw new Error(
|
|
2469
|
+
`subagent idleTimeoutMs = ${v} exceeds the Node setTimeout limit ` +
|
|
2470
|
+
`(${MAX_TIMER_DELAY_MS} ms = 2^31-1); larger delays silently collapse to 1ms and would kill the subagent immediately. ` +
|
|
2471
|
+
`Recovery: pass idleTimeoutMs <= ${MAX_TIMER_DELAY_MS}, or omit it for the default (${DEFAULT_IDLE_TIMEOUT_MS}ms), or use 0/negative to disable idle GC.`,
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2144
2476
|
/** 构造 SessionRunnerContext(spawn 模式:无需 SDK 实例)。 */
|
|
2145
2477
|
private buildSessionRunnerContext(overrideCwd?: string): SessionRunnerContext {
|
|
2146
2478
|
return {
|
|
@@ -2266,3 +2598,18 @@ export function getSubagentService(): SubagentService | null {
|
|
|
2266
2598
|
export function setSubagentService(service: SubagentService): void {
|
|
2267
2599
|
getServiceSlot().current = service;
|
|
2268
2600
|
}
|
|
2601
|
+
|
|
2602
|
+
/**
|
|
2603
|
+
* [U10① D6] 第三宿主最小构造入口:仅凭参数注入构造 SubagentService(无全局查找)。
|
|
2604
|
+
*
|
|
2605
|
+
* 构造依赖(modelService / getMainSessionFile / uiRequestHandler)全部经 init
|
|
2606
|
+
* 参数注入;本工厂是 `new SubagentService(init)` 的薄包装,不读也不写
|
|
2607
|
+
* getSubagentService/setSubagentService 的全局槽位——session_start 单例流程
|
|
2608
|
+
* 行为零改动,宿主自持实例时用本工厂。构造内部行为与直接 new 逐字等价。
|
|
2609
|
+
*
|
|
2610
|
+
* @experimental execution 运行时面(设计 docs/design/subagent-core-sink-design.md §3.3 D6):
|
|
2611
|
+
* 一个 minor 周期内允许签名微调,稳定后转常规 semver 承诺。
|
|
2612
|
+
*/
|
|
2613
|
+
export function createSubagentService(init: SubagentServiceInit): SubagentService {
|
|
2614
|
+
return new SubagentService(init);
|
|
2615
|
+
}
|