@zhushanwen/pi-subagent-workflow 7.4.0 → 8.1.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 +15 -0
- package/package.json +7 -4
- package/scripts/rfl.mjs +308 -0
- package/skills/workflow-script-format/SKILL.md +1 -1
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
- package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
- package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
- package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
- package/src/execution/__tests__/crash-recovery.test.ts +8 -2
- package/src/execution/__tests__/delivery-methods.test.ts +385 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -72
- package/src/execution/__tests__/execution-record.test.ts +199 -0
- package/src/execution/__tests__/finalize-record.test.ts +197 -15
- package/src/execution/__tests__/format.test.ts +131 -7
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +37 -10
- package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
- package/src/execution/__tests__/index-session-start.test.ts +257 -5
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/list-component.test.ts +59 -5
- package/src/execution/__tests__/list-fields.test.ts +109 -0
- package/src/execution/__tests__/model-resolver.test.ts +38 -1
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/notifier-flush.test.ts +209 -7
- package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
- package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
- package/src/execution/__tests__/record-store.test.ts +442 -54
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
- package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
- package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
- package/src/execution/__tests__/session-pending.test.ts +61 -2
- package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
- package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
- package/src/execution/__tests__/spawn-args.test.ts +127 -49
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/status-refactor.test.ts +345 -0
- package/src/execution/__tests__/stdin-writer.test.ts +97 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +49 -11
- package/src/execution/__tests__/timeout-integration.test.ts +27 -13
- package/src/execution/__tests__/tool-action.test.ts +12 -10
- package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/worktree-manager.test.ts +300 -90
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +17 -13
- package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
- package/src/execution/__tests__/worktree-registry.test.ts +72 -34
- package/src/execution/agent-result-mapper.ts +4 -1
- package/src/execution/argv-mirror.ts +21 -2
- package/src/execution/channel-registry-access.ts +3 -1
- package/src/execution/execution-record.ts +126 -9
- package/src/execution/finalize-record.ts +99 -13
- package/src/execution/idle-gc.ts +47 -0
- package/src/execution/lifecycle-manager.ts +491 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +61 -16
- package/src/execution/model-resolver.ts +26 -5
- package/src/execution/notifier.ts +69 -12
- package/src/execution/pi-invocation.ts +21 -1
- package/src/execution/record-entry.ts +118 -0
- package/src/execution/record-store.ts +844 -108
- package/src/execution/session-pending.ts +121 -49
- package/src/execution/session-reconstructor.ts +224 -7
- package/src/execution/session-runner.ts +713 -316
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/stdin-writer.ts +93 -7
- package/src/execution/stream-sink.ts +20 -3
- package/src/execution/subagent-service.ts +917 -138
- package/src/execution/temp-prompt.ts +8 -3
- package/src/execution/turn-limiter.ts +14 -0
- package/src/execution/types.ts +218 -19
- package/src/execution/worktree-manager.ts +449 -59
- package/src/execution/worktree-registry.ts +97 -29
- package/src/index.ts +318 -20
- package/src/injectors/subagent-list-injector.ts +26 -8
- package/src/injectors/workflow-list-injector.ts +25 -8
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +17 -4
- package/src/interface/__tests__/tool-render.test.ts +10 -8
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +103 -26
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +179 -0
- package/src/interface/bg-notify-render.ts +32 -8
- package/src/interface/command-actions.ts +26 -7
- package/src/interface/commands.ts +21 -22
- package/src/interface/format.ts +53 -20
- package/src/interface/gui-mappers.ts +6 -8
- package/src/interface/helpers.ts +170 -10
- package/src/interface/list-component.ts +53 -14
- package/src/interface/subagent-actions.ts +235 -17
- package/src/interface/subagent-tool.ts +81 -16
- package/src/interface/subagents.ts +2 -1
- package/src/interface/tool-render.ts +21 -27
- package/src/interface/tool-workflow-script.ts +29 -33
- package/src/interface/tool-workflow.ts +67 -100
- package/src/interface/views/WorkflowsView.ts +89 -32
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
- package/src/interface/views/detail-content.ts +1 -1
- package/src/interface/views/format.ts +3 -3
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +333 -0
- package/src/orchestration/__tests__/args-validator.test.ts +1 -1
- package/src/orchestration/__tests__/config-loader.test.ts +38 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
- package/src/orchestration/__tests__/execute-agent-call.test.ts +144 -1
- package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +171 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +820 -19
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
- package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +1108 -19
- package/src/orchestration/__tests__/skill-discovery.test.ts +130 -61
- package/src/orchestration/__tests__/test-mocks.ts +197 -0
- package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +110 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/__tests__/workflows-e2e.test.ts +38 -40
- package/src/orchestration/agent-opts-resolver.ts +4 -1
- package/src/orchestration/args-validator.ts +2 -2
- package/src/orchestration/config-loader.ts +30 -1
- package/src/orchestration/error-recovery.ts +137 -30
- package/src/orchestration/execute-agent-call.ts +44 -10
- package/src/orchestration/jsonl-run-store.ts +397 -71
- package/src/orchestration/launcher.ts +7 -1
- package/src/orchestration/lifecycle.ts +145 -133
- package/src/orchestration/models/__tests__/trace.test.ts +408 -0
- package/src/orchestration/models/budget.ts +1 -1
- package/src/orchestration/models/run-runtime.ts +15 -17
- package/src/orchestration/models/run-spec.ts +2 -2
- package/src/orchestration/models/run-state.ts +3 -3
- package/src/orchestration/models/trace.ts +95 -15
- package/src/orchestration/models/types.ts +8 -9
- package/src/orchestration/models/workflow-run.ts +50 -71
- package/src/orchestration/models/workflow-script.ts +32 -1
- package/src/orchestration/skill-discovery.ts +30 -0
- package/src/orchestration/worker-handle.ts +1 -1
- package/src/orchestration/worker-host.ts +1 -1
- package/src/orchestration/worker-script-builder.ts +38 -11
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
- package/src/shared/__tests__/resource-discovery.test.ts +79 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/agent-ref.ts +22 -1
- package/src/shared/resource-discovery.ts +162 -59
- package/src/shared/schema-jsonify.ts +56 -0
- package/workflows/review-fix-loop-utils.cjs +542 -32
- package/workflows/review-fix-loop.js +462 -109
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
AgentResult,
|
|
21
21
|
AgentUsage,
|
|
22
22
|
AgentUsageTotal,
|
|
23
|
+
ClosedReason,
|
|
23
24
|
DisplayItem,
|
|
24
25
|
ExecutionMode,
|
|
25
26
|
ExecutionRecord,
|
|
@@ -156,6 +157,10 @@ export function createRecord(
|
|
|
156
157
|
parentRecordId?: string;
|
|
157
158
|
/** subagent 递归深度。顶层=0。 */
|
|
158
159
|
depth?: number;
|
|
160
|
+
/** 对话模式标志(true = 可持续对话,轮次完成进 idle)。默认 undefined/false = 一次性。 */
|
|
161
|
+
chatMode?: boolean;
|
|
162
|
+
/** 空闲超时毫秒数(仅 chatMode 有意义)。覆盖默认 5min。 */
|
|
163
|
+
idleTimeoutMs?: number;
|
|
159
164
|
controller?: AbortController;
|
|
160
165
|
},
|
|
161
166
|
): ExecutionRecord {
|
|
@@ -171,6 +176,8 @@ export function createRecord(
|
|
|
171
176
|
rootSessionId: identity.rootSessionId,
|
|
172
177
|
parentRecordId: identity.parentRecordId,
|
|
173
178
|
depth: identity.depth ?? 0,
|
|
179
|
+
chatMode: identity.chatMode,
|
|
180
|
+
idleTimeoutMs: identity.idleTimeoutMs,
|
|
174
181
|
|
|
175
182
|
// 状态(实时更新)
|
|
176
183
|
status: "running",
|
|
@@ -180,6 +187,8 @@ export function createRecord(
|
|
|
180
187
|
turnCount: 0,
|
|
181
188
|
totalTokens: 0,
|
|
182
189
|
lastError: undefined,
|
|
190
|
+
// 对话轮次计数(首轮 = 0,每完成一轮 finalizeRoundToIdle +1)。非 chatMode 不自增。
|
|
191
|
+
round: 0,
|
|
183
192
|
|
|
184
193
|
// 完成(completeRecord 唯一写点)
|
|
185
194
|
endedAt: undefined,
|
|
@@ -233,6 +242,30 @@ function findRunningToolCall(
|
|
|
233
242
|
return undefined;
|
|
234
243
|
}
|
|
235
244
|
|
|
245
|
+
/**
|
|
246
|
+
* [perf] running toolCall 倒序索引:tool_start push 位置入索引,tool_end 弹尾定位
|
|
247
|
+
*(尾部 = 最后 push 的同名项,与 findRunningToolCall 倒序全扫的语义等价),把每次
|
|
248
|
+
* tool_end 的 O(所有 turns × toolCalls) 扫描降为 O(1)。WeakMap 按 record 实例隔离
|
|
249
|
+
*(createRecord 新实例从空索引开始,不影响旧实例)。
|
|
250
|
+
* 索引 miss(重建 record 的历史 running toolCall / 外部注入工具无 tool_start)
|
|
251
|
+
* 回退 findRunningToolCall 全扫兜底——正确性不依赖索引完整性。
|
|
252
|
+
*/
|
|
253
|
+
const runningToolIndex = new WeakMap<ExecutionRecord, Map<string, Array<{ turn: Turn; idx: number }>>>();
|
|
254
|
+
|
|
255
|
+
function indexToolStart(record: ExecutionRecord, turn: Turn, toolName: string): void {
|
|
256
|
+
let byName = runningToolIndex.get(record);
|
|
257
|
+
if (byName === undefined) {
|
|
258
|
+
byName = new Map();
|
|
259
|
+
runningToolIndex.set(record, byName);
|
|
260
|
+
}
|
|
261
|
+
const arr = byName.get(toolName);
|
|
262
|
+
if (arr === undefined) {
|
|
263
|
+
byName.set(toolName, [{ turn, idx: turn.toolCalls.length - 1 }]);
|
|
264
|
+
} else {
|
|
265
|
+
arr.push({ turn, idx: turn.toolCalls.length - 1 });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
236
269
|
/**
|
|
237
270
|
* 从 AgentEvent 更新 record。所有数据收口进 record.turns[]。
|
|
238
271
|
* - text/thinking:流式累积进 currentTurn()(完整内容,非切片)
|
|
@@ -271,13 +304,30 @@ export function updateFromEvent(record: ExecutionRecord, event: AgentEvent): voi
|
|
|
271
304
|
_status: "running",
|
|
272
305
|
startedTs: Date.now(),
|
|
273
306
|
};
|
|
274
|
-
currentTurn(record)
|
|
307
|
+
const turn = currentTurn(record);
|
|
308
|
+
turn.toolCalls.push(tc);
|
|
309
|
+
indexToolStart(record, turn, event.toolName);
|
|
275
310
|
return;
|
|
276
311
|
}
|
|
277
312
|
|
|
278
|
-
// ── tool_end
|
|
313
|
+
// ── tool_end:索引弹尾 O(1) 定位 running 同名 toolCall,miss 回退全扫兜底 ──
|
|
279
314
|
case "tool_end": {
|
|
280
|
-
|
|
315
|
+
let matched: readonly [Turn, number] | undefined;
|
|
316
|
+
const byName = runningToolIndex.get(record);
|
|
317
|
+
const arr = byName?.get(event.toolName);
|
|
318
|
+
if (arr !== undefined && arr.length > 0) {
|
|
319
|
+
const item = arr[arr.length - 1];
|
|
320
|
+
arr.pop();
|
|
321
|
+
const tc = item.turn.toolCalls[item.idx];
|
|
322
|
+
if (tc !== undefined && tc._status === "running") {
|
|
323
|
+
matched = [item.turn, item.idx] as const;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (matched === undefined) {
|
|
327
|
+
// 兜底:重建 record 的历史 running toolCall(索引未覆盖)、索引项被外部路径
|
|
328
|
+
// 置非 running 等场景——保持与旧实现一致的跨 turn 倒序全扫。
|
|
329
|
+
matched = findRunningToolCall(record, event.toolName);
|
|
330
|
+
}
|
|
281
331
|
if (matched !== undefined) {
|
|
282
332
|
const [turn, i] = matched;
|
|
283
333
|
const tc = turn.toolCalls[i]!;
|
|
@@ -488,6 +538,51 @@ export function getFullText(record: ExecutionRecord): string {
|
|
|
488
538
|
.join("\n\n");
|
|
489
539
|
}
|
|
490
540
|
|
|
541
|
+
/**
|
|
542
|
+
* [增量通知] 从 fromTurnIndex 起聚合 turn 文本(getFullText 的增量视图)。
|
|
543
|
+
*
|
|
544
|
+
* 语义:record.turns.slice(Math.max(0, fromTurnIndex)) 的 text 数组
|
|
545
|
+
* filter(text => text.length > 0)(判据与 getFullText 逐字对齐——空白串 " " 按非空
|
|
546
|
+
* 处理,不发散)后 join("\n\n")。
|
|
547
|
+
*
|
|
548
|
+
* 不变式(单测锁死):
|
|
549
|
+
* - fromTurnIndex = 0 与 getFullText(record) 逐字节等价(one-shot/首轮回退零成本)
|
|
550
|
+
* - fromTurnIndex >= turns.length 返回 ""(不抛错、不回绕)
|
|
551
|
+
* - fromTurnIndex < 0 规范化为 0(防御,Math.max 不产生 slice 负偏移语义)
|
|
552
|
+
*
|
|
553
|
+
* 纯函数只读;getFullText 本体与既有调用方(collectResult 等)零改动。
|
|
554
|
+
* 唯一调用点 onRoundSettled 的增量派生(record.roundBaseTurnIndex ?? 0)。
|
|
555
|
+
*/
|
|
556
|
+
export function getFullTextFrom(record: ExecutionRecord, fromTurnIndex: number): string {
|
|
557
|
+
return record.turns
|
|
558
|
+
.slice(Math.max(0, fromTurnIndex))
|
|
559
|
+
.map((t) => t.text)
|
|
560
|
+
.filter((text) => text.length > 0)
|
|
561
|
+
.join("\n\n");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* [增量通知] 轮次边界推进公式(D1):返回下一轮增量的 turns[] 起始下标。
|
|
566
|
+
*
|
|
567
|
+
* record.turns.length - (末 turn 存在且 !closed 且 text.length === 0 ? 1 : 0)
|
|
568
|
+
*
|
|
569
|
+
* - 末 turn 已闭合(pi 现序常态:带 usage 的 message_end 恒先于 turn_end,settle 时
|
|
570
|
+
* 全闭合)→ 返回 length,下一轮从新 turn 起。
|
|
571
|
+
* - 末 turn 是滞后 message_end 开出的空 turn(防御分支,pi 现序不可达)→ 不计入边界
|
|
572
|
+
* (-1),留在下一轮增量内:新轮首个 text_delta 经 currentTurn 复用该空 turn,复用
|
|
573
|
+
* 累积被 slice(from) 覆盖;若直用 turns.length 会把这段文本挤出 slice 范围静默丢失(D1)。
|
|
574
|
+
* - 末 turn 未闭合且 text 非空(防御形态,pi 现序不可达)→ 计入本轮返回 length(该形态
|
|
575
|
+
* 即 onRoundSettled 推进前观测哨 logger.warn 的触发条件)。
|
|
576
|
+
*
|
|
577
|
+
* 纯函数只读不写 record;唯一调用点 onRoundSettled 第 5 步(notify 之后推进)。
|
|
578
|
+
*/
|
|
579
|
+
export function nextRoundBaseTurnIndex(record: ExecutionRecord): number {
|
|
580
|
+
const last = record.turns[record.turns.length - 1];
|
|
581
|
+
const trailingOpenEmpty =
|
|
582
|
+
last !== undefined && !last.closed && last.text.length === 0 ? 1 : 0;
|
|
583
|
+
return record.turns.length - trailingOpenEmpty;
|
|
584
|
+
}
|
|
585
|
+
|
|
491
586
|
/**
|
|
492
587
|
* 聚合所有 turn 的 toolCalls(扁平化),并 strip InternalToolCall 的内部字段。
|
|
493
588
|
* 供 collectResult / schema enforcement 读,替代旧闭包 toolCalls 旁路。
|
|
@@ -499,6 +594,17 @@ export function getAllToolCalls(record: ExecutionRecord): ToolCall[] {
|
|
|
499
594
|
return record.turns.flatMap((t) => t.toolCalls.map(stripInternal));
|
|
500
595
|
}
|
|
501
596
|
|
|
597
|
+
/**
|
|
598
|
+
* 聚合所有 turn 的 toolCalls 总数(免克隆计数)。
|
|
599
|
+
*
|
|
600
|
+
* getAllToolCalls 会 flatMap + strip 克隆出完整数组——只需计数的调用方(渲染签名
|
|
601
|
+
* 每 200ms tick 调用一次)用它是纯浪费;本函数 reduce 累加各 turn 的 length,零分配。
|
|
602
|
+
* 与 getAllToolCalls(...).length 恒等(同一 turns 源)。
|
|
603
|
+
*/
|
|
604
|
+
export function countAllToolCalls(record: ExecutionRecord): number {
|
|
605
|
+
return record.turns.reduce((sum, t) => sum + t.toolCalls.length, 0);
|
|
606
|
+
}
|
|
607
|
+
|
|
502
608
|
/** 把 InternalToolCall 映射回纯净的 ToolCall(丢弃 _status / startedTs)。 */
|
|
503
609
|
function stripInternal(tc: InternalToolCall): ToolCall {
|
|
504
610
|
return {
|
|
@@ -540,21 +646,26 @@ export function getTotalUsage(record: ExecutionRecord): AgentUsageTotal | undefi
|
|
|
540
646
|
/**
|
|
541
647
|
* status 状态机的 CAS 互斥锁。仅当 `record.status === "running"` 时改为 target
|
|
542
648
|
* 并返回 true,否则返回 false。**status 状态机本身就是互斥锁**——终态
|
|
543
|
-
* (
|
|
649
|
+
* (closed/cancelled)不可逆,check-then-set 在 JS 单线程事件循环里天然原子。
|
|
544
650
|
*
|
|
545
651
|
* 用途:executor 的收尾竞争。cancelBackground 与 background detached 完成回调
|
|
546
652
|
* 都调 tryTransition 抢锁:抢到负责完整收尾,没抢到闭嘴不做事。
|
|
547
653
|
*
|
|
548
|
-
* target
|
|
549
|
-
*
|
|
550
|
-
*
|
|
654
|
+
* target 仅限正常执行流的两终态(closed + cancelled)。
|
|
655
|
+
* closed 携带 closedReason(L2 原因子枚举),由重建路径或正常执行流写入。
|
|
656
|
+
* 重建路径也可用 markReconstructedStatus 直接赋值(跳过 CAS)。
|
|
657
|
+
*
|
|
658
|
+
* @param closedReason closed 终态的 L2 关闭原因。仅 target="closed" 时有意义;
|
|
659
|
+
* target="cancelled" 时忽略。缺省 "gc"(通用完成/失败)。
|
|
551
660
|
*/
|
|
552
661
|
export function tryTransition(
|
|
553
662
|
record: ExecutionRecord,
|
|
554
|
-
target: "
|
|
663
|
+
target: "closed",
|
|
664
|
+
closedReason?: ClosedReason,
|
|
555
665
|
): boolean {
|
|
556
666
|
if (record.status !== "running") return false;
|
|
557
667
|
record.status = target;
|
|
668
|
+
record.closedReason = closedReason ?? "gc";
|
|
558
669
|
return true;
|
|
559
670
|
}
|
|
560
671
|
|
|
@@ -577,13 +688,18 @@ export function markReconstructedStatus(
|
|
|
577
688
|
* 不修改 turns/totalTokens——已由 updateFromEvent 累积,completeRecord 只读不重置。
|
|
578
689
|
*
|
|
579
690
|
* ⚠ 前置条件:调用方必须先通过 tryTransition 抢到锁(status 已被 CAS 设为 target)。
|
|
691
|
+
*
|
|
692
|
+
* @param closedReason closed 终态的 L2 关闭原因。仅 status="closed" 时写入;
|
|
693
|
+
* "cancelled" 时忽略。
|
|
580
694
|
*/
|
|
581
695
|
export function completeRecord(
|
|
582
696
|
record: ExecutionRecord,
|
|
583
697
|
result: AgentResult,
|
|
584
|
-
status: "
|
|
698
|
+
status: "closed",
|
|
699
|
+
closedReason?: ClosedReason,
|
|
585
700
|
): void {
|
|
586
701
|
record.status = status;
|
|
702
|
+
record.closedReason = closedReason ?? "gc";
|
|
587
703
|
record.endedAt = Date.now();
|
|
588
704
|
record.agentResult = result;
|
|
589
705
|
record.result = result.text;
|
|
@@ -663,6 +779,7 @@ export function snapshot(record: ExecutionRecord): RecordSnapshot {
|
|
|
663
779
|
task: record.task,
|
|
664
780
|
slug: record.slug,
|
|
665
781
|
status: record.status,
|
|
782
|
+
chatMode: record.chatMode,
|
|
666
783
|
turns: record.turnCount,
|
|
667
784
|
totalTokens: record.totalTokens,
|
|
668
785
|
startedAt: record.startedAt,
|
|
@@ -27,7 +27,7 @@ import type { ModelConfigService } from "./model-config-service.ts";
|
|
|
27
27
|
import { getSubagentSessionDir } from "./path-encoding.ts";
|
|
28
28
|
import type { RecordStore } from "./record-store.ts";
|
|
29
29
|
import { writeCancelledTombstone } from "./tombstone-store.ts";
|
|
30
|
-
import type { AgentResult, ExecutionRecord } from "./types.ts";
|
|
30
|
+
import type { AgentResult, ClosedReason, ExecutionRecord } from "./types.ts";
|
|
31
31
|
import type { WorktreeManager } from "./worktree-manager.ts";
|
|
32
32
|
|
|
33
33
|
const logger = getLogger("subagents");
|
|
@@ -40,10 +40,10 @@ export interface FinalizeDeps {
|
|
|
40
40
|
modelService: ModelConfigService;
|
|
41
41
|
/** Pi ExtensionAPI(仅用 appendEntry 记录 manifest 写失败事件)。null 在 dispose 后。 */
|
|
42
42
|
pi: { appendEntry?: (type: string, data: unknown) => void } | null;
|
|
43
|
-
/** 清节流状态(防 trailing timer 在 record 归档后误发陈旧 onUpdate)。 */
|
|
44
|
-
clearThrottle(id: string): void;
|
|
45
43
|
/** pending-notifications 终态注销(绑定 pi.events.emit,由调用方闭包提供)。 */
|
|
46
44
|
emitUnregister(id: string, status: string): void;
|
|
45
|
+
// [review 修复] 已删除 redeliverPending 回调(MF-1 消费确认制补投):pendingMessages
|
|
46
|
+
// 三段消费链随 deliverToRunning 一并移除(无生产调用方,死机制)。
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
@@ -57,11 +57,9 @@ export async function doFinalizeRecord(
|
|
|
57
57
|
deps: FinalizeDeps,
|
|
58
58
|
record: ExecutionRecord,
|
|
59
59
|
result: AgentResult,
|
|
60
|
-
status: "
|
|
60
|
+
status: "closed",
|
|
61
|
+
closedReason?: ClosedReason,
|
|
61
62
|
): Promise<void> {
|
|
62
|
-
// 终态清节流状态:防 trailing timer 在 record 归档后误发陈旧 onUpdate
|
|
63
|
-
deps.clearThrottle(record.id);
|
|
64
|
-
|
|
65
63
|
// ── Step 0: collectPatch(best-effort)──
|
|
66
64
|
// [MF#3] patchFile 写到 worktree 之外(sessionsDir/<branch>.patch),避免被 cleanup 删除;
|
|
67
65
|
// 路径回填 record.patchFile,供调用方(tool result / /subagents list)应用。
|
|
@@ -73,7 +71,7 @@ export async function doFinalizeRecord(
|
|
|
73
71
|
);
|
|
74
72
|
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
75
73
|
const patchFile = path.join(sessionsDir, `${record.worktreeHandle.branch}.patch`);
|
|
76
|
-
const patch = deps.worktreeManager.collectPatch(record.worktreeHandle, patchFile);
|
|
74
|
+
const patch = await deps.worktreeManager.collectPatch(record.worktreeHandle, patchFile);
|
|
77
75
|
if (patch.written) record.patchFile = patchFile;
|
|
78
76
|
} catch (pe: unknown) {
|
|
79
77
|
bestEffort(pe, "collectPatch (finalizeRecord Step0)");
|
|
@@ -82,7 +80,7 @@ export async function doFinalizeRecord(
|
|
|
82
80
|
|
|
83
81
|
// ── Step 1: completeRecord(B9: 抛错→后续仍执行)──
|
|
84
82
|
try {
|
|
85
|
-
completeRecord(record, result, status);
|
|
83
|
+
completeRecord(record, result, status, closedReason);
|
|
86
84
|
} catch (err) {
|
|
87
85
|
bestEffort(err, "completeRecord (finalizeRecord B9)", "error");
|
|
88
86
|
}
|
|
@@ -100,8 +98,8 @@ export async function doFinalizeRecord(
|
|
|
100
98
|
// (否则 worktree 泄漏)。各件独立 try/catch,互不阻断。
|
|
101
99
|
if (record.sessionFile) {
|
|
102
100
|
try {
|
|
103
|
-
// MF-1 fix: cancelled
|
|
104
|
-
if (
|
|
101
|
+
// MF-1 fix / v4 B-1: cancelled(closedReason='cancelled')写 tombstone 而非 finalized,防重建丢失 cancelled
|
|
102
|
+
if (closedReason === "cancelled") {
|
|
105
103
|
writeCancelledTombstone(record.sessionFile, {
|
|
106
104
|
id: record.id,
|
|
107
105
|
status: "cancelled",
|
|
@@ -118,7 +116,7 @@ export async function doFinalizeRecord(
|
|
|
118
116
|
}
|
|
119
117
|
if (record.worktreeHandle) {
|
|
120
118
|
try {
|
|
121
|
-
deps.worktreeManager.cleanup(record.worktreeHandle);
|
|
119
|
+
await deps.worktreeManager.cleanup(record.worktreeHandle);
|
|
122
120
|
} catch (err) {
|
|
123
121
|
bestEffort(err, "worktree cleanup (finalizeRecord Step3)");
|
|
124
122
|
}
|
|
@@ -146,7 +144,8 @@ export async function doFinalizeRecord(
|
|
|
146
144
|
rootSessionId: record.rootSessionId ?? "",
|
|
147
145
|
parentRecordId: record.parentRecordId,
|
|
148
146
|
agentName: record.agent,
|
|
149
|
-
|
|
147
|
+
// v4 B-1: manifest status 统一为 closed(cancelled 折入 closed,区分靠 tombstone sidecar)
|
|
148
|
+
status: "closed",
|
|
150
149
|
createdAt: record.startedAt,
|
|
151
150
|
completedAt: record.endedAt ?? Date.now(),
|
|
152
151
|
sessionFile: record.sessionFile,
|
|
@@ -163,3 +162,90 @@ export async function doFinalizeRecord(
|
|
|
163
162
|
});
|
|
164
163
|
}
|
|
165
164
|
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 对话模式轮次完成收尾:record 进 idle 态(非终态化,等待续聊)。
|
|
168
|
+
*
|
|
169
|
+
* 与 doFinalizeRecord 的关键区别(M2-A idle 语义):
|
|
170
|
+
* - 不调 completeRecord(record 不冻结,保留 turns[] 等运行时状态供续聊累积)
|
|
171
|
+
* - 不调 store.archive(record 留内存,getMutable 可查、list 可见)
|
|
172
|
+
* - 不 cleanup worktree(保留对话模式工作目录)
|
|
173
|
+
* - 不写 manifest(idle 非终态,manifest 是终态诊断辅助)
|
|
174
|
+
* - 删 .alive marker(进程已 SIGTERM 回收,不再是活进程)
|
|
175
|
+
* - emitUnregister(进程已死,从 pending 活跃后代差集移除;record 留内存不 archive)
|
|
176
|
+
*
|
|
177
|
+
* MF-2:设 record.result = result.text(否则 notifier idle 回复正文恒为 "(empty)",
|
|
178
|
+
* G1/G2 多轮回复送达不成立)。失败轮次(result.success=false,MF-6 回退 idle 路径)
|
|
179
|
+
* 的 result.text 可能为空,用 result.error 兜底让 notify 可读。
|
|
180
|
+
*
|
|
181
|
+
* 状态:record.status = "idle"(覆盖 tryTransition 设的 done/failed),record.round += 1。
|
|
182
|
+
* 各步骤 best-effort 互不阻断(参照 doFinalizeRecord 的 bestEffort 用法)。
|
|
183
|
+
*
|
|
184
|
+
* @param deps 与 doFinalizeRecord 同源(从 SubagentService 注入)
|
|
185
|
+
* @param result 本轮 AgentResult(MF-2:result.text 写入 record.result 供 notifier idle 回复)
|
|
186
|
+
*/
|
|
187
|
+
export async function doFinalizeRoundToIdle(
|
|
188
|
+
deps: FinalizeDeps,
|
|
189
|
+
record: ExecutionRecord,
|
|
190
|
+
result: AgentResult,
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
// MF-2:设 record.result 供 notifier idle 回复正文(否则恒 "(empty)",G1/G2 不成立)。
|
|
193
|
+
// MF-6 兜底:失败轮次(success=false)的 result.text 可能为空,用 error 让 notify 可读。
|
|
194
|
+
// [R2-1] 轮终写点恒写非空:one-shot 空文本成功完成(collectResult getFullText 返回 ""、
|
|
195
|
+
// success=true、真实可达,本写点被 subagent-service runAndFinalize 的成功分支共用)首轮
|
|
196
|
+
// result 前值 undefined,兜底补 "(empty)" 占位——措辞与 notifier buildLlmContent 的
|
|
197
|
+
// `record.result ?? "(empty)"` 兜底同款,通知文案逐字节不变([增量 G2] G4 取舍保持)。
|
|
198
|
+
// 轮终信号优先:record.result 非 undefined 是 renderer hasRunning 排除「轮终
|
|
199
|
+
// running-resumable」的判据(shared SubagentRecord.result 契约),保持 undefined 会让
|
|
200
|
+
// 完成注入后末位 turn 永久「工作中」。续轮(前值存在)沿用前值:one-shot 无增量语义,
|
|
201
|
+
// record.result = 该 subagent 最终输出。chatMode 空增量轮 → 固定占位
|
|
202
|
+
// "(no output this round)"(D5:增量语义下沿用旧 record.result = 上一轮增量,本轮通知
|
|
203
|
+
// 正文 = 上一轮内容,父 agent 误读为原样重复回复)。
|
|
204
|
+
let nextResult: string | undefined;
|
|
205
|
+
if (result.text) {
|
|
206
|
+
nextResult = result.text;
|
|
207
|
+
} else if (result.error) {
|
|
208
|
+
nextResult = `round did not complete: ${result.error}`;
|
|
209
|
+
} else if (record.chatMode) {
|
|
210
|
+
nextResult = "(no output this round)";
|
|
211
|
+
} else {
|
|
212
|
+
nextResult = record.result ?? "(empty)";
|
|
213
|
+
}
|
|
214
|
+
record.result = nextResult;
|
|
215
|
+
|
|
216
|
+
// 删 .alive marker(进程已 SIGTERM 回收)。
|
|
217
|
+
// sessionFile 窗口期可能 undefined(极少——对话模式轮次完成意味着 session 已跑过),
|
|
218
|
+
// 缺失时跳过但仍设内存 idle(重启后磁盘重建会落到 crashed,边界可接受)。
|
|
219
|
+
if (record.sessionFile) {
|
|
220
|
+
try {
|
|
221
|
+
removeAliveMarker(record.sessionFile);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
bestEffort(err, "removeAliveMarker (doFinalizeRoundToIdle)");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// pending-notifications:进程已死,从活跃后代差集移除(record 留内存不 archive,
|
|
228
|
+
// v4 B-1:record 现为 running-resumable,但 pending 注册的是进程活跃性,进程死了需注销)。
|
|
229
|
+
deps.emitUnregister(record.id, "running");
|
|
230
|
+
|
|
231
|
+
// 状态机(v4 B-1):record 保持 running(旧 idle 折入 running,覆盖 tryTransition 设的 closed,
|
|
232
|
+
// 可冷路径 resume),轮次计数 +1。idleSince 时间戳独立保留供 GC 判据。
|
|
233
|
+
// [S10] closedReason 同步清除:调用方(runAndFinalize catch / MF-6 分支)先 tryTransition
|
|
234
|
+
// 设了 closed+closedReason 再回退 running——不清则 "gc"/"cancelled" 残留在 running record 上,
|
|
235
|
+
// 泄漏进 list 投影与后续 notify 载荷(toNotifyRecord 透传 record.closedReason),让一个
|
|
236
|
+
// 活跃 record 看起来像已被某原因关闭过。
|
|
237
|
+
record.status = "running";
|
|
238
|
+
record.closedReason = undefined;
|
|
239
|
+
record.round = (record.round ?? 0) + 1;
|
|
240
|
+
record.idleSince = Date.now();
|
|
241
|
+
// 执行态信号(residual-fixes):轮终回 running-resumable = 无活进程驱动(idle timer
|
|
242
|
+
// 回收/保活等待续聊),GUI 侧据此判 waiting(非 streaming)。冷路径续轮(进程启动)清除。
|
|
243
|
+
record.resumable = true;
|
|
244
|
+
|
|
245
|
+
// W16 [D4]:轮终回 running-resumable 是类外状态写点(record 留内存不走 archive),
|
|
246
|
+
// 显式上报迁移——entry 携带新 round 与本轮 result,pi 文件的重建源不滞后。
|
|
247
|
+
deps.store.reportRecordTransition(record);
|
|
248
|
+
|
|
249
|
+
// [review 修复] 已删除残留 pendingMessages 的 redeliverPending 补投段(MF-1 消费
|
|
250
|
+
// 确认安全网):三段消费链随 deliverToRunning 一并移除,本段不可达。
|
|
251
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SP-4 idle record GC:30 天 TTL 定时归档,防 idle record 永久驻留内存。
|
|
3
|
+
* 从 subagent-service.ts 抽出(文件超 max-lines;逻辑自包含:interval + TTL 扫描)。
|
|
4
|
+
* 启动幂等由调用方(service.startGcTimer)守卫;返回 stop 函数供 dispose 清理。
|
|
5
|
+
*/
|
|
6
|
+
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
7
|
+
import { bestEffort } from "./best-effort.ts";
|
|
8
|
+
import { isResumable } from "./lifecycle-predicates.ts";
|
|
9
|
+
import type { RecordStore } from "./record-store.ts";
|
|
10
|
+
|
|
11
|
+
const logger = getLogger("subagents");
|
|
12
|
+
|
|
13
|
+
/** GC 扫描间隔:1 小时。 */
|
|
14
|
+
// eslint-disable-next-line no-magic-numbers -- 60*60*1000 = 1h 的毫秒换算常数
|
|
15
|
+
const GC_INTERVAL_MS = 60 * 60 * 1000;
|
|
16
|
+
/** idle record TTL:30 天(超龄归档)。 */
|
|
17
|
+
// eslint-disable-next-line no-magic-numbers -- 30*24*60*60*1000 = 30d 的毫秒换算常数
|
|
18
|
+
const IDLE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
19
|
+
/** 毫秒/天(GC 日志的 d 换算)。 */
|
|
20
|
+
// eslint-disable-next-line no-magic-numbers -- 24*60*60*1000 = 1d 的毫秒换算常数
|
|
21
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 启动 idle record GC 定时器,返回 stop 函数(清理 interval;幂等)。
|
|
25
|
+
* 每个扫描周期:对 store 内全部 active record 中 resumable 且带 idleSince 的,
|
|
26
|
+
* 超过 IDLE_TTL_MS 的归档(archive 单条失败不阻断其余,bestEffort 留痕)。
|
|
27
|
+
*/
|
|
28
|
+
export function startIdleGc(store: RecordStore): () => void {
|
|
29
|
+
const timer = setInterval(() => {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
for (const record of store.listAllActive()) {
|
|
32
|
+
if (isResumable(record) && record.idleSince) {
|
|
33
|
+
const age = now - record.idleSince;
|
|
34
|
+
if (age > IDLE_TTL_MS) {
|
|
35
|
+
logger.warn(`[subagents] GC: archiving idle record ${record.id} (idle for ${Math.round(age / MS_PER_DAY)}d)`);
|
|
36
|
+
try {
|
|
37
|
+
store.archive(record);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
bestEffort(err, `GC archive record ${record.id}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}, GC_INTERVAL_MS);
|
|
45
|
+
timer.unref?.();
|
|
46
|
+
return () => clearInterval(timer);
|
|
47
|
+
}
|