pi-firecode 0.8.1 → 1.0.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 +35 -15
- package/activity.ts +92 -0
- package/busy.ts +188 -0
- package/config.example.jsonc +38 -47
- package/config.ts +198 -172
- package/deliver.ts +100 -13
- package/flame.ts +149 -0
- package/format.ts +84 -31
- package/header.ts +208 -82
- package/index.ts +17 -11
- package/master/actions.ts +293 -0
- package/master/activity-list.ts +281 -0
- package/master/event-card.ts +18 -37
- package/master/event-format.ts +71 -40
- package/master/guard.ts +46 -0
- package/master/index.ts +88 -1036
- package/master/list-view.ts +129 -0
- package/master/outbox.ts +188 -0
- package/master/prompts/master.zh.md +17 -12
- package/master/prompts/worker.zh.md +1 -1
- package/master/role.ts +6 -1
- package/master/run.ts +315 -0
- package/master/runtime.ts +259 -0
- package/master/spawn.ts +79 -38
- package/master/state.ts +30 -6
- package/master/worker-view.ts +517 -0
- package/package.json +1 -1
- package/provider/claude-sub.ts +35 -13
- package/provider/openai-native/src/compact-client.ts +2 -14
- package/provider/openai-native/src/options.ts +4 -2
- package/review/advisor.ts +0 -2
- package/review/card.ts +36 -23
- package/review/checkpoint.ts +2 -8
- package/review/evidence.ts +42 -44
- package/review/index.ts +271 -525
- package/review/occupancy.ts +25 -0
- package/review/outcome.ts +31 -7
- package/review/prompt.ts +7 -8
- package/review/prompts/review.en.md +7 -6
- package/review/prompts/review.zh.md +7 -6
- package/review/reviewer.ts +2 -5
- package/review/session.ts +11 -30
- package/review/state.ts +32 -60
- package/review/ui.ts +30 -469
- package/round-recorder.ts +18 -0
- package/session/herdr-display.ts +1 -1
- package/session/presets.ts +130 -87
- package/session/quota.ts +123 -0
- package/session/stats.ts +4 -1
- package/statusbar/index.ts +242 -104
- package/statusbar/render.ts +95 -163
- package/theme.ts +2 -41
- package/tools/actions.ts +53 -0
- package/tools/assistant-view.ts +64 -0
- package/tools/click-anchor.ts +56 -0
- package/tools/group-view.ts +420 -0
- package/tools/grouping.ts +146 -221
- package/tools/host.ts +369 -0
- package/tools/index.ts +108 -105
- package/tools/line.ts +95 -58
- package/tools/machine.ts +78 -0
- package/tools/parts.ts +2 -2
- package/tools/round.ts +95 -0
- package/tools/turn-clock.ts +41 -0
- package/tools/turn-summary.ts +109 -0
- package/truncated-write.ts +18 -0
- package/watcher/card.ts +28 -22
- package/watcher/index.ts +16 -44
- package/watcher/observer.ts +3 -3
- package/watcher/prompts/watch.zh.md +1 -1
- package/watcher/transcript.ts +4 -12
- package/flame-frames.ts +0 -460
- package/review/progress.ts +0 -324
- package/session/bark.ts +0 -157
- package/session/working-flame.ts +0 -116
- package/statusbar/quota-cache.ts +0 -60
- package/statusbar/quota-parse.ts +0 -85
- package/statusbar/quota.ts +0 -203
- package/statusbar/tps.ts +0 -87
package/review/index.ts
CHANGED
|
@@ -3,26 +3,25 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 职责分界:
|
|
5
5
|
* - 领域状态只活在纯 reducer(state.ts)里,所有迁移经 reduce() 计算;
|
|
6
|
-
*
|
|
7
|
-
* 会话结果一律回灌成事件交给 reducer
|
|
6
|
+
* 本文件是唯一执行器,只做副作用(起审查会话、投递反馈、发卡、持久化、审查活动行),
|
|
7
|
+
* 会话结果一律回灌成事件交给 reducer。
|
|
8
|
+
* - 运行时状态按会话隔离:pi 在同一进程内对同一 cwd 复用扩展模块实例,主会话与每个
|
|
9
|
+
* Worker 子会话共用本文件;`registerReview(pi)` 各自持有一份 ReviewRuntime,模块级不留
|
|
10
|
+
* 任何会话状态,一个会话被 dispose 不会拖累另一个。
|
|
8
11
|
* - 渲染器在此顶层无条件注册(不懒加载),live 与 reload 外观一致。
|
|
9
12
|
*/
|
|
10
13
|
import { randomUUID } from "node:crypto";
|
|
11
|
-
import
|
|
14
|
+
import { wrapEnvelope } from "../deliver.js";
|
|
12
15
|
import {
|
|
13
|
-
getAgentDir,
|
|
14
|
-
ModelRuntime,
|
|
15
16
|
type ExtensionAPI,
|
|
16
17
|
type ExtensionContext,
|
|
17
18
|
} from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import { loadConfig, type Language, type ReviewConfig } from "../config.js";
|
|
19
|
-
import { formatDuration } from "../format.js";
|
|
19
|
+
import { loadConfig, type Language, type ReviewConfig, type Section } from "../config.js";
|
|
20
20
|
import { herdrPaneEnv, herdrRequest } from "../herdr-client.js";
|
|
21
21
|
import { InProcessSessionPool } from "../master/spawn.js";
|
|
22
|
-
import { buildCard, CARD_TYPE,
|
|
22
|
+
import { buildCard, CARD_TYPE, registerCardRenderer } from "./card.js";
|
|
23
23
|
import {
|
|
24
24
|
beginCheckpoint,
|
|
25
|
-
CHECKPOINT_TYPE,
|
|
26
25
|
CheckpointConflictError,
|
|
27
26
|
type CheckpointStamp,
|
|
28
27
|
readCheckpoint,
|
|
@@ -30,27 +29,16 @@ import {
|
|
|
30
29
|
writeCheckpoint,
|
|
31
30
|
} from "./checkpoint.js";
|
|
32
31
|
import { buildEvidence } from "./evidence.js";
|
|
33
|
-
import {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
type ReviewerProgress,
|
|
37
|
-
settleProgress,
|
|
38
|
-
} from "./progress.js";
|
|
39
|
-
import {
|
|
40
|
-
type ActivityView,
|
|
41
|
-
hideActivity,
|
|
42
|
-
lockEditor,
|
|
43
|
-
showActivity,
|
|
44
|
-
unlockEditor,
|
|
45
|
-
} from "./ui.js";
|
|
46
|
-
import { REVIEW_OCCUPANCY_LABEL as OCCUPANCY_LABEL } from "./outcome.js";
|
|
47
|
-
import { buildAdvisorPrompt, buildFixFeedback, buildReviewPrompt, buildSummaryPrompt, readPrompt, reviewEnvelope } from "./prompt.js";
|
|
32
|
+
import { hideReviewTitle, lockEditor, showReviewTitle } from "./ui.js";
|
|
33
|
+
import { OCCUPANCY_CHANNEL, OCCUPANCY_LABEL, type OccupancyPayload, type ReviewProgress, type ReviewStage } from "./occupancy.js";
|
|
34
|
+
import { buildAdvisorPrompt, buildFixFeedback, buildReviewPrompt, buildSummaryPrompt, readPrompt } from "./prompt.js";
|
|
48
35
|
import { runAdvisor } from "./advisor.js";
|
|
49
36
|
import { runReviewer, type ReviewModelConfig } from "./reviewer.js";
|
|
50
37
|
import { createReviewSessionRunner, type ReviewSessionRunner } from "./session.js";
|
|
51
38
|
import {
|
|
52
39
|
type AdvisorResult,
|
|
53
40
|
type CardData,
|
|
41
|
+
type Phase,
|
|
54
42
|
type ReviewEffect,
|
|
55
43
|
type ReviewEvent,
|
|
56
44
|
type ReviewLimits,
|
|
@@ -62,8 +50,6 @@ import {
|
|
|
62
50
|
export const FEEDBACK_TYPE = "firecode-review-feedback";
|
|
63
51
|
/** 总结回合提示:与修复反馈同通道(进上下文不渲染),不参与证据自指。 */
|
|
64
52
|
export const SUMMARY_REQUEST_TYPE = "firecode-review-summary";
|
|
65
|
-
const STATUS_KEY = "fire-review";
|
|
66
|
-
const OCCUPANCY_CHANNEL = "herdr:blocked";
|
|
67
53
|
const OCCUPANCY_SOURCE = "firecode-review";
|
|
68
54
|
/** herdr 按 seq 丢弃过期上报;同一 source 单调递增。 */
|
|
69
55
|
let occupancySeq = Date.now() * 1000;
|
|
@@ -93,13 +79,11 @@ function isActive(state: ReviewState) {
|
|
|
93
79
|
}
|
|
94
80
|
|
|
95
81
|
interface Controller {
|
|
96
|
-
pi: ExtensionAPI;
|
|
97
82
|
ctx: ExtensionContext;
|
|
98
83
|
config: ReviewConfig;
|
|
99
84
|
state: ReviewState;
|
|
100
85
|
signal: AbortController;
|
|
101
86
|
watchdog: ReturnType<typeof setTimeout> | undefined;
|
|
102
|
-
statusTimer?: ReturnType<typeof setInterval>;
|
|
103
87
|
/** 本 controller 上一次写入的凭证;null=本审查还没写过,undefined=冲突/失败后停写。 */
|
|
104
88
|
persistedStamp: CheckpointStamp | null | undefined;
|
|
105
89
|
/** streaming 时 sendMessage 会变成 steer;展示卡必须等 settled 后再发。 */
|
|
@@ -108,63 +92,77 @@ interface Controller {
|
|
|
108
92
|
runningAction?: string;
|
|
109
93
|
/** 当前审查者/顾问任务;执行模型 agent_start 必须 await 它退出后才能继续。 */
|
|
110
94
|
actionPromise?: Promise<void>;
|
|
111
|
-
runSession: ReviewSessionRunner;
|
|
112
95
|
actionController?: AbortController;
|
|
113
96
|
/** 反馈 sendMessage 已调用,等待 agent_start 回执。 */
|
|
114
97
|
feedbackStartTimer?: ReturnType<typeof setTimeout>;
|
|
115
|
-
/**
|
|
116
|
-
|
|
117
|
-
/** 当前 progress 属于审查者还是顾问:修复相据此决定是否展示裁决摘要。 */
|
|
118
|
-
progressKind?: "reviewers" | "advisor";
|
|
119
|
-
progressStartedAt?: number;
|
|
120
|
-
/** 编辑器是否已被审查接管(禁输入 + esc 取消)。 */
|
|
121
|
-
editorLocked?: boolean;
|
|
98
|
+
/** 审查接管了编辑器(禁输入 + esc 取消)时的解锁函数,还原成接管前的编辑器。 */
|
|
99
|
+
unlockEditor?: () => void;
|
|
122
100
|
/** Herdr blocked 频道采用计数语义,每个 true 必须由同一 controller 配对 false。 */
|
|
123
101
|
occupancyHeld?: boolean;
|
|
124
102
|
/** 占用标签租约续期计时器;释放与 shutdown 时清除。 */
|
|
125
103
|
occupancyTimer?: ReturnType<typeof setInterval>;
|
|
126
104
|
}
|
|
127
105
|
|
|
128
|
-
|
|
129
|
-
|
|
106
|
+
/** 一个会话的审查运行时:controller 是当前这场审查,queue 串行化它的状态迁移。 */
|
|
107
|
+
interface ReviewRuntime {
|
|
108
|
+
readonly pi: ExtensionAPI;
|
|
109
|
+
readonly runSession: ReviewSessionRunner;
|
|
110
|
+
controller: Controller | undefined;
|
|
111
|
+
queue: Promise<void>;
|
|
112
|
+
}
|
|
130
113
|
|
|
131
114
|
interface ReviewDependencies {
|
|
132
115
|
pool?: InProcessSessionPool;
|
|
133
|
-
resolveModel?: (id: string) => Promise<Model<any>>;
|
|
134
116
|
runSession?: ReviewSessionRunner;
|
|
135
117
|
}
|
|
136
118
|
|
|
119
|
+
export interface ReviewHandle {
|
|
120
|
+
/** 等待 event-loop barrier 与其产生的状态迁移排空(测试用)。 */
|
|
121
|
+
settled(): Promise<void>;
|
|
122
|
+
}
|
|
123
|
+
|
|
137
124
|
export function registerReview(
|
|
138
125
|
pi: ExtensionAPI,
|
|
139
126
|
enabled = true,
|
|
140
127
|
configBroken = false,
|
|
141
128
|
dependencies: ReviewDependencies = {},
|
|
142
|
-
):
|
|
129
|
+
): ReviewHandle {
|
|
143
130
|
// 渲染器与开关解耦:关闭 review 后历史卡 reload 仍使用原生结果卡样式。
|
|
144
131
|
registerCardRenderer(pi);
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
dependencies.
|
|
148
|
-
|
|
132
|
+
const rt: ReviewRuntime = {
|
|
133
|
+
pi,
|
|
134
|
+
runSession: dependencies.runSession ?? createReviewSessionRunner(dependencies.pool ?? new InProcessSessionPool()),
|
|
135
|
+
controller: undefined,
|
|
136
|
+
queue: Promise.resolve(),
|
|
137
|
+
};
|
|
138
|
+
const handle: ReviewHandle = {
|
|
139
|
+
settled: async () => {
|
|
140
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
141
|
+
await rt.queue;
|
|
142
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
143
|
+
await rt.queue;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
149
146
|
if (!enabled) {
|
|
150
147
|
// 只有用户明确关闭才封存活动 checkpoint(防重新启用后恢复幽灵审查);
|
|
151
148
|
// features 配置坏掉不是关闭:保留 checkpoint,修好配置重启后继续恢复。
|
|
152
149
|
if (!configBroken) pi.on("session_start", (_event, ctx) => settleDisabledCheckpoint(pi, ctx));
|
|
153
|
-
return;
|
|
150
|
+
return handle;
|
|
154
151
|
}
|
|
155
152
|
pi.registerCommand("fire-review", {
|
|
156
153
|
description: "对抗性审查:审这个会话到目前为止做完的事",
|
|
157
|
-
handler: (args, ctx) => handleCommand(
|
|
154
|
+
handler: (args, ctx) => handleCommand(rt, args, ctx),
|
|
158
155
|
});
|
|
159
|
-
pi.on("session_start", (_event, ctx) => handleSessionStart(
|
|
156
|
+
pi.on("session_start", (_event, ctx) => handleSessionStart(rt, ctx));
|
|
160
157
|
// 宿主保证 resources_discover 在整次 session_start(含所有异步 handler)完成后发出。
|
|
161
|
-
pi.on("resources_discover", (_event, ctx) => requestAdvance(
|
|
162
|
-
pi.on("agent_start", () => handleAgentStart(
|
|
158
|
+
pi.on("resources_discover", (_event, ctx) => requestAdvance(rt, ctx));
|
|
159
|
+
pi.on("agent_start", () => handleAgentStart(rt));
|
|
163
160
|
// agent_end 只记录修复/总结回合的结局,不在此推进审查。
|
|
164
|
-
pi.on("agent_end", (event) => handleAgentEnd(
|
|
161
|
+
pi.on("agent_end", (event) => handleAgentEnd(rt, event));
|
|
165
162
|
// settled 后尝试恢复;后续异步 handler 若再触发模型,agent_start 互锁会先停审查。
|
|
166
|
-
pi.on("agent_settled", (_event, ctx) =>
|
|
167
|
-
pi.on("session_shutdown", (event, ctx) => handleShutdown(
|
|
163
|
+
pi.on("agent_settled", (_event, ctx) => requestAdvance(rt, ctx));
|
|
164
|
+
pi.on("session_shutdown", (event, ctx) => handleShutdown(rt, event.reason, ctx));
|
|
165
|
+
return handle;
|
|
168
166
|
}
|
|
169
167
|
|
|
170
168
|
function settleDisabledCheckpoint(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
@@ -196,36 +194,32 @@ function settleUnavailableCheckpoint(
|
|
|
196
194
|
|
|
197
195
|
/** 串行化状态迁移:reducer 同步执行,副作用排队;同一时刻只有一个迁移在跑。
|
|
198
196
|
* 返回队尾 Promise,让 pi 的事件处理器(session_shutdown 等)可 await 持久化落盘。 */
|
|
199
|
-
function dispatch(
|
|
200
|
-
const run =
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
Date.now(),
|
|
207
|
-
);
|
|
208
|
-
if (state !== controller.state) {
|
|
209
|
-
controller.state = state;
|
|
197
|
+
function dispatch(rt: ReviewRuntime, event: ReviewEvent): Promise<void> {
|
|
198
|
+
const run = rt.queue.then(async () => {
|
|
199
|
+
const active = rt.controller;
|
|
200
|
+
if (!active) return;
|
|
201
|
+
const { state, effects } = reduce(active.state, event, limitsOf(active.config), Date.now());
|
|
202
|
+
if (state !== active.state) {
|
|
203
|
+
active.state = state;
|
|
210
204
|
// 持久化失败不能当成功继续:否则会拿不一致的状态去起会话、投反馈,
|
|
211
205
|
// 重启后又从旧 checkpoint 恢复,重现幽灵审查与重复反馈。
|
|
212
|
-
|
|
213
|
-
if (!
|
|
214
|
-
syncOccupancy(
|
|
215
|
-
syncUi();
|
|
206
|
+
if (!persist(rt, state)) return;
|
|
207
|
+
if (!isActive(state)) clearWatchdog(rt);
|
|
208
|
+
syncOccupancy(rt, active);
|
|
209
|
+
syncUi(rt);
|
|
216
210
|
}
|
|
217
|
-
await runEffects(effects);
|
|
211
|
+
await runEffects(rt, effects);
|
|
218
212
|
});
|
|
219
213
|
// 队列一旦 rejected 就再也不会执行后续迁移(连 esc 取消也会失效):
|
|
220
214
|
// 副作用异常只能到此为止,不得杀死状态机。
|
|
221
|
-
|
|
222
|
-
notifyEffectFailure(error);
|
|
215
|
+
rt.queue = run.catch((error) => {
|
|
216
|
+
notifyEffectFailure(rt, error);
|
|
223
217
|
});
|
|
224
|
-
return
|
|
218
|
+
return rt.queue;
|
|
225
219
|
}
|
|
226
220
|
|
|
227
|
-
function notifyEffectFailure(error: unknown) {
|
|
228
|
-
const active = controller;
|
|
221
|
+
function notifyEffectFailure(rt: ReviewRuntime, error: unknown) {
|
|
222
|
+
const active = rt.controller;
|
|
229
223
|
if (!active?.ctx.hasUI) return;
|
|
230
224
|
const message = error instanceof Error ? error.message : String(error);
|
|
231
225
|
active.ctx.ui.notify(
|
|
@@ -236,33 +230,9 @@ function notifyEffectFailure(error: unknown) {
|
|
|
236
230
|
);
|
|
237
231
|
}
|
|
238
232
|
|
|
239
|
-
/**
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
*/
|
|
243
|
-
function loadReviewConfig(): { config: ReviewConfig } | { error: string } {
|
|
244
|
-
let loaded: ReturnType<typeof loadConfig>;
|
|
245
|
-
try {
|
|
246
|
-
loaded = loadConfig();
|
|
247
|
-
} catch (error) {
|
|
248
|
-
return {
|
|
249
|
-
error: `fire-review 配置读取失败:${error instanceof Error ? error.message : String(error)}`,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
// 三类都必须阻断:文件整体解析不了、review 节自身有错、
|
|
253
|
-
// 以及 features.review 开关类型错(字符串 "false" 会因 `!== false` 静默启用付费审查)。
|
|
254
|
-
const problems = loaded.problems.filter(
|
|
255
|
-
(problem) =>
|
|
256
|
-
problem.startsWith("review") ||
|
|
257
|
-
problem.startsWith("未知字段 review.") ||
|
|
258
|
-
problem.startsWith("config.jsonc") ||
|
|
259
|
-
problem.startsWith("features"),
|
|
260
|
-
);
|
|
261
|
-
if (problems.length > 0)
|
|
262
|
-
return { error: `fire-review 配置有问题,已停止:${problems.join(";")}` };
|
|
263
|
-
if (!loaded.config.review.advisor.model || loaded.config.review.reviewers.length === 0)
|
|
264
|
-
return { error: "fire-review 配置有问题,已停止:请显式完整配置 review" };
|
|
265
|
-
return { config: loaded.config.review };
|
|
233
|
+
/** 命令与恢复两个入口共用同一判定:任何一个静默回退默认模型都会花真钱跑错模型。 */
|
|
234
|
+
function loadReviewConfig(): Section<ReviewConfig> {
|
|
235
|
+
return loadConfig().review;
|
|
266
236
|
}
|
|
267
237
|
|
|
268
238
|
function limitsOf(config: ReviewConfig): ReviewLimits {
|
|
@@ -278,12 +248,7 @@ function limitsOf(config: ReviewConfig): ReviewLimits {
|
|
|
278
248
|
};
|
|
279
249
|
}
|
|
280
250
|
|
|
281
|
-
async function handleCommand(
|
|
282
|
-
pi: ExtensionAPI,
|
|
283
|
-
args: string,
|
|
284
|
-
ctx: ExtensionContext,
|
|
285
|
-
runSession: ReviewSessionRunner,
|
|
286
|
-
) {
|
|
251
|
+
async function handleCommand(rt: ReviewRuntime, args: string, ctx: ExtensionContext) {
|
|
287
252
|
// 配置解析失败不能让命令无声失败:pi 会捕获 handler 异常,用户只会看到什么都没发生。
|
|
288
253
|
const loaded = loadReviewConfig();
|
|
289
254
|
if ("error" in loaded) {
|
|
@@ -291,7 +256,7 @@ async function handleCommand(
|
|
|
291
256
|
return;
|
|
292
257
|
}
|
|
293
258
|
const config = loaded.config;
|
|
294
|
-
if (controller && isActive(controller.state)) {
|
|
259
|
+
if (rt.controller && isActive(rt.controller.state)) {
|
|
295
260
|
if (ctx.hasUI) ctx.ui.notify(
|
|
296
261
|
config.language === "en"
|
|
297
262
|
? "A review is already running."
|
|
@@ -300,16 +265,12 @@ async function handleCommand(
|
|
|
300
265
|
);
|
|
301
266
|
return;
|
|
302
267
|
}
|
|
303
|
-
// 旧审查的看门狗必须在覆盖 controller 前停掉:它的回调读的是全局 controller,
|
|
304
|
-
// 否则旧超时到点时会把新一场审查中止。
|
|
305
|
-
clearWatchdog();
|
|
306
268
|
const command = parseCommand(args, config.language);
|
|
307
269
|
if ("error" in command) {
|
|
308
270
|
if (ctx.hasUI) ctx.ui.notify(command.error, "error");
|
|
309
271
|
return;
|
|
310
272
|
}
|
|
311
|
-
controller = {
|
|
312
|
-
pi,
|
|
273
|
+
rt.controller = {
|
|
313
274
|
ctx,
|
|
314
275
|
config,
|
|
315
276
|
state: initialState(randomUUID()),
|
|
@@ -317,13 +278,10 @@ async function handleCommand(
|
|
|
317
278
|
watchdog: undefined,
|
|
318
279
|
persistedStamp: null,
|
|
319
280
|
pendingCards: [],
|
|
320
|
-
runSession,
|
|
321
|
-
progress: initialProgress(config.reviewers, config.language),
|
|
322
281
|
};
|
|
323
|
-
armWatchdog();
|
|
324
|
-
armStatusTimer();
|
|
282
|
+
armWatchdog(rt);
|
|
325
283
|
// 命令入口也只提出推进请求;真正开审统一经过下一 event-loop 的 idle barrier.
|
|
326
|
-
void dispatch(
|
|
284
|
+
void dispatch(rt, {
|
|
327
285
|
type: "START",
|
|
328
286
|
focus: command.focus,
|
|
329
287
|
busy: true,
|
|
@@ -331,32 +289,18 @@ async function handleCommand(
|
|
|
331
289
|
}
|
|
332
290
|
|
|
333
291
|
/** 重启 / 会话恢复:从 checkpoint 重建 controller 并续跑未完成的环节。
|
|
334
|
-
* reload / new / resume / fork 是运行时替换(新 pi
|
|
335
|
-
*
|
|
336
|
-
function handleSessionStart(
|
|
337
|
-
pi: ExtensionAPI,
|
|
338
|
-
ctx: ExtensionContext,
|
|
339
|
-
runSession: ReviewSessionRunner,
|
|
340
|
-
): Promise<void> | void {
|
|
341
|
-
if (controller && controller.pi !== pi) {
|
|
342
|
-
setOccupancy(controller, false);
|
|
343
|
-
controller = undefined;
|
|
344
|
-
}
|
|
292
|
+
* reload / new / resume / fork 是运行时替换(新 pi、新 runtime),旧 runtime 已在
|
|
293
|
+
* session_shutdown 收口;这里只按新会话的 checkpoint 恢复,quit 之外不 settle。 */
|
|
294
|
+
function handleSessionStart(rt: ReviewRuntime, ctx: ExtensionContext): Promise<void> | void {
|
|
345
295
|
const checkpoint = readCheckpoint(ctx);
|
|
346
|
-
if (!checkpoint || !isActive(checkpoint))
|
|
347
|
-
if (controller && !isActive(controller.state)) controller = undefined;
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
if (controller && isActive(controller.state)) return;
|
|
296
|
+
if (!checkpoint || !isActive(checkpoint)) return;
|
|
351
297
|
const loaded = loadReviewConfig();
|
|
352
298
|
if ("error" in loaded) {
|
|
353
299
|
// session_start 的配置告警由入口统一聚合;这里只保留活动 checkpoint,修好后继续。
|
|
354
300
|
return;
|
|
355
301
|
}
|
|
356
302
|
const config = loaded.config;
|
|
357
|
-
|
|
358
|
-
controller = {
|
|
359
|
-
pi,
|
|
303
|
+
rt.controller = {
|
|
360
304
|
ctx,
|
|
361
305
|
config,
|
|
362
306
|
state: checkpoint,
|
|
@@ -364,26 +308,23 @@ function handleSessionStart(
|
|
|
364
308
|
watchdog: undefined,
|
|
365
309
|
persistedStamp: readStamp(ctx),
|
|
366
310
|
pendingCards: [],
|
|
367
|
-
runSession,
|
|
368
|
-
progress: initialProgress(config.reviewers, config.language),
|
|
369
311
|
};
|
|
370
|
-
armWatchdog();
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
syncUi();
|
|
312
|
+
armWatchdog(rt);
|
|
313
|
+
syncOccupancy(rt, rt.controller);
|
|
314
|
+
syncUi(rt);
|
|
374
315
|
// 恢复只更新持久状态并提出推进请求;绝不在 session_start handler 内起任何工作。
|
|
375
|
-
return dispatch(
|
|
316
|
+
return dispatch(rt, { type: "RECOVER" });
|
|
376
317
|
}
|
|
377
318
|
|
|
378
|
-
async function handleAgentStart(
|
|
379
|
-
const active = controller;
|
|
380
|
-
if (!active
|
|
319
|
+
async function handleAgentStart(rt: ReviewRuntime): Promise<void> {
|
|
320
|
+
const active = rt.controller;
|
|
321
|
+
if (!active) return;
|
|
381
322
|
if (
|
|
382
323
|
active.state.phase === "awaiting_fix" &&
|
|
383
324
|
active.state.repair?.status === "awaiting_start"
|
|
384
325
|
) {
|
|
385
326
|
clearFeedbackStartTimer(active);
|
|
386
|
-
await dispatch(
|
|
327
|
+
await dispatch(rt, { type: "REPAIR_STARTED" });
|
|
387
328
|
return;
|
|
388
329
|
}
|
|
389
330
|
if (
|
|
@@ -391,7 +332,7 @@ async function handleAgentStart(pi: ExtensionAPI): Promise<void> {
|
|
|
391
332
|
active.state.summary?.status === "awaiting_start"
|
|
392
333
|
) {
|
|
393
334
|
clearFeedbackStartTimer(active);
|
|
394
|
-
await dispatch(
|
|
335
|
+
await dispatch(rt, { type: "SUMMARY_STARTED" });
|
|
395
336
|
return;
|
|
396
337
|
}
|
|
397
338
|
// 其他扩展可在我们排队后异步触发执行模型。agent_start 是宿主提供的硬边界:
|
|
@@ -399,20 +340,17 @@ async function handleAgentStart(pi: ExtensionAPI): Promise<void> {
|
|
|
399
340
|
if (!active.actionPromise) return;
|
|
400
341
|
active.actionController?.abort();
|
|
401
342
|
await active.actionPromise;
|
|
402
|
-
if (controller !== active || !isActive(active.state)) return;
|
|
343
|
+
if (rt.controller !== active || !isActive(active.state)) return;
|
|
403
344
|
active.runningAction = undefined;
|
|
404
345
|
active.actionPromise = undefined;
|
|
405
346
|
}
|
|
406
347
|
|
|
407
|
-
function handleAgentEnd(
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
): Promise<void> | void {
|
|
411
|
-
const active = controller;
|
|
412
|
-
if (!active || active.pi !== pi) return;
|
|
348
|
+
function handleAgentEnd(rt: ReviewRuntime, event: { messages: readonly unknown[] }): Promise<void> | void {
|
|
349
|
+
const active = rt.controller;
|
|
350
|
+
if (!active) return;
|
|
413
351
|
// 总结回合任何结局都收尾:裁决已落地,总结失败/中断不重试不升级。
|
|
414
352
|
if (active.state.phase === "summarizing" && active.state.summary?.status === "running")
|
|
415
|
-
return dispatch(
|
|
353
|
+
return dispatch(rt, { type: "SUMMARY_SETTLED" });
|
|
416
354
|
if (
|
|
417
355
|
active.state.phase !== "awaiting_fix" ||
|
|
418
356
|
active.state.repair?.status !== "running"
|
|
@@ -421,77 +359,69 @@ function handleAgentEnd(
|
|
|
421
359
|
.reverse()
|
|
422
360
|
.find((message) => isRecord(message) && message.role === "assistant");
|
|
423
361
|
if (isRecord(assistant) && assistant.stopReason !== "error" && assistant.stopReason !== "aborted")
|
|
424
|
-
return dispatch(
|
|
362
|
+
return dispatch(rt, { type: "REPAIR_COMPLETED" });
|
|
425
363
|
active.signal.abort();
|
|
426
|
-
return dispatch(
|
|
364
|
+
return dispatch(rt, { type: "CANCEL", reason: "user" });
|
|
427
365
|
}
|
|
428
366
|
|
|
429
|
-
function
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
function requestAdvance(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
434
|
-
const active = controller;
|
|
435
|
-
if (!active || active.pi !== pi) return;
|
|
367
|
+
function requestAdvance(rt: ReviewRuntime, ctx: ExtensionContext): void {
|
|
368
|
+
const active = rt.controller;
|
|
369
|
+
if (!active) return;
|
|
436
370
|
const runId = active.state.runId;
|
|
437
|
-
void advanceWhenIdle(
|
|
438
|
-
notifyEffectFailure(error);
|
|
439
|
-
if (controller !== active) return;
|
|
371
|
+
void advanceWhenIdle(rt, ctx, runId).catch((error) => {
|
|
372
|
+
notifyEffectFailure(rt, error);
|
|
373
|
+
if (rt.controller !== active) return;
|
|
440
374
|
active.signal.abort();
|
|
441
|
-
void dispatch(
|
|
375
|
+
void dispatch(rt, { type: "CANCEL", reason: "user" });
|
|
442
376
|
});
|
|
443
377
|
}
|
|
444
378
|
|
|
445
|
-
async function advanceWhenIdle(
|
|
446
|
-
|
|
447
|
-
ctx: ExtensionContext,
|
|
448
|
-
runId: string,
|
|
449
|
-
): Promise<void> {
|
|
450
|
-
const active = controller;
|
|
379
|
+
async function advanceWhenIdle(rt: ReviewRuntime, ctx: ExtensionContext, runId: string): Promise<void> {
|
|
380
|
+
const active = rt.controller;
|
|
451
381
|
// 所有启动入口共享 idle 门;正确性另由 agent_start 的同步停审互锁保证。
|
|
452
382
|
if (
|
|
453
383
|
!active ||
|
|
454
|
-
active.pi !== pi ||
|
|
455
384
|
active.state.runId !== runId ||
|
|
456
385
|
active.signal.signal.aborted ||
|
|
457
386
|
!ctx.isIdle() ||
|
|
458
387
|
ctx.hasPendingMessages()
|
|
459
388
|
) return;
|
|
460
389
|
active.ctx = ctx;
|
|
461
|
-
flushPendingCards(
|
|
390
|
+
flushPendingCards(rt);
|
|
462
391
|
const { state } = active;
|
|
463
392
|
if (state.phase === "queued") {
|
|
464
|
-
await dispatch(
|
|
393
|
+
await dispatch(rt, { type: "ADVANCE" });
|
|
465
394
|
return;
|
|
466
395
|
}
|
|
467
396
|
if (state.phase === "reviewing") {
|
|
468
|
-
startAction(active, `review:${state.round}`, "reviewer", () => startReviewers(
|
|
397
|
+
startAction(rt, active, `review:${state.round}`, "reviewer", () => startReviewers(rt));
|
|
469
398
|
return;
|
|
470
399
|
}
|
|
471
400
|
if (state.phase === "needs_fix") {
|
|
472
|
-
startAction(active, `advisor:${state.round}`, "advisor", () => consultAdvisor(
|
|
401
|
+
startAction(rt, active, `advisor:${state.round}`, "advisor", () => consultAdvisor(rt));
|
|
473
402
|
return;
|
|
474
403
|
}
|
|
475
404
|
if (state.phase === "summarizing") {
|
|
476
405
|
if (state.summary?.status !== "pending") return;
|
|
477
|
-
await dispatch(
|
|
478
|
-
const current = controller?.state;
|
|
479
|
-
if (controller === active && current?.phase === "summarizing" && current.summary?.status === "awaiting_start")
|
|
480
|
-
deliverSummaryNow(
|
|
406
|
+
await dispatch(rt, { type: "SUMMARY_DISPATCHED" });
|
|
407
|
+
const current = rt.controller?.state;
|
|
408
|
+
if (rt.controller === active && current?.phase === "summarizing" && current.summary?.status === "awaiting_start")
|
|
409
|
+
deliverSummaryNow(rt, current);
|
|
481
410
|
return;
|
|
482
411
|
}
|
|
483
412
|
if (state.phase !== "awaiting_fix" || !state.repair) return;
|
|
484
413
|
if (state.repair.status === "pending") {
|
|
485
|
-
await dispatch(
|
|
486
|
-
const repair = controller?.state.repair;
|
|
487
|
-
if (controller === active && repair?.status === "awaiting_start")
|
|
488
|
-
deliverFeedbackNow(
|
|
414
|
+
await dispatch(rt, { type: "FEEDBACK_DISPATCHED" });
|
|
415
|
+
const repair = rt.controller?.state.repair;
|
|
416
|
+
if (rt.controller === active && repair?.status === "awaiting_start")
|
|
417
|
+
deliverFeedbackNow(rt, repair.details, repair.advisor);
|
|
489
418
|
return;
|
|
490
419
|
}
|
|
491
|
-
if (state.repair.status === "completed") await dispatch(
|
|
420
|
+
if (state.repair.status === "completed") await dispatch(rt, { type: "ADVANCE" });
|
|
492
421
|
}
|
|
493
422
|
|
|
494
423
|
function startAction(
|
|
424
|
+
rt: ReviewRuntime,
|
|
495
425
|
active: Controller,
|
|
496
426
|
key: string,
|
|
497
427
|
kind: "reviewer" | "advisor",
|
|
@@ -502,8 +432,8 @@ function startAction(
|
|
|
502
432
|
active.actionController = new AbortController();
|
|
503
433
|
const action = run()
|
|
504
434
|
.catch(async (error) => {
|
|
505
|
-
if (controller !== active || active.actionController?.signal.aborted) return;
|
|
506
|
-
await dispatch(
|
|
435
|
+
if (rt.controller !== active || active.actionController?.signal.aborted) return;
|
|
436
|
+
await dispatch(rt, {
|
|
507
437
|
type: "INFRASTRUCTURE_ERROR",
|
|
508
438
|
details: sessionErrorText(kind, active.config.language, error),
|
|
509
439
|
});
|
|
@@ -517,73 +447,54 @@ function startAction(
|
|
|
517
447
|
active.actionPromise = action;
|
|
518
448
|
}
|
|
519
449
|
|
|
450
|
+
/** 会话离开当前运行时:取消并等待审查会话退出,quit 落终态,其余保留 checkpoint 给新运行时恢复。
|
|
451
|
+
* 收口后清空 controller——宿主随后 dispose 会作废 ctx,迟到回调看到空 controller 直接返回。 */
|
|
520
452
|
async function handleShutdown(
|
|
521
|
-
|
|
453
|
+
rt: ReviewRuntime,
|
|
522
454
|
reason: "quit" | "reload" | "new" | "resume" | "fork",
|
|
523
455
|
ctx: ExtensionContext,
|
|
524
456
|
): Promise<void> {
|
|
525
|
-
const active = controller;
|
|
457
|
+
const active = rt.controller;
|
|
526
458
|
if (!active) return;
|
|
527
459
|
// 会话离开当前运行时就立即释放;reload 恢复会由新 controller 重新配对喊占用。
|
|
528
|
-
setOccupancy(active, false);
|
|
460
|
+
setOccupancy(rt, active, false);
|
|
529
461
|
// 无论何种终止都先取消并等待当前审查会话;旧动作不得泄漏到新运行时。
|
|
530
462
|
active.signal.abort();
|
|
531
463
|
active.actionController?.abort();
|
|
532
|
-
clearWatchdog();
|
|
533
|
-
clearStatusTimer(active);
|
|
464
|
+
clearWatchdog(rt);
|
|
534
465
|
clearFeedbackStartTimer(active);
|
|
535
466
|
await active.actionPromise;
|
|
536
467
|
if (active.ctx !== ctx) active.ctx = ctx;
|
|
537
|
-
if (reason === "quit") {
|
|
538
|
-
|
|
539
|
-
|
|
468
|
+
if (reason === "quit") await dispatch(rt, { type: "CANCEL", reason: "shutdown" });
|
|
469
|
+
else {
|
|
470
|
+
clearUi(active);
|
|
471
|
+
await rt.queue;
|
|
540
472
|
}
|
|
541
|
-
|
|
542
|
-
hideActivity(active.ctx);
|
|
543
|
-
releaseEditor(active);
|
|
544
|
-
await dispatchQueue;
|
|
473
|
+
if (rt.controller === active) rt.controller = undefined;
|
|
545
474
|
}
|
|
546
475
|
|
|
547
|
-
function armWatchdog() {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const remaining = Math.max(0, overallTimeoutMs(controller.config) - elapsed);
|
|
476
|
+
function armWatchdog(rt: ReviewRuntime) {
|
|
477
|
+
const active = rt.controller;
|
|
478
|
+
if (!active) return;
|
|
479
|
+
clearWatchdog(rt);
|
|
480
|
+
const elapsed = active.state.startedAt ? Math.max(0, Date.now() - active.state.startedAt) : 0;
|
|
481
|
+
const remaining = Math.max(0, overallTimeoutMs(active.config) - elapsed);
|
|
554
482
|
if (remaining === 0) {
|
|
555
|
-
|
|
556
|
-
void dispatch(
|
|
483
|
+
active.signal.abort();
|
|
484
|
+
void dispatch(rt, { type: "TIMEOUT" });
|
|
557
485
|
return;
|
|
558
486
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (!active || !isActive(active.state)) return;
|
|
487
|
+
active.watchdog = setTimeout(() => {
|
|
488
|
+
if (rt.controller !== active || !isActive(active.state)) return;
|
|
562
489
|
active.signal.abort();
|
|
563
490
|
active.actionController?.abort();
|
|
564
|
-
dispatch(
|
|
491
|
+
dispatch(rt, { type: "TIMEOUT" });
|
|
565
492
|
}, remaining);
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function clearWatchdog() {
|
|
570
|
-
if (controller?.watchdog) clearTimeout(controller.watchdog);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
function armStatusTimer() {
|
|
574
|
-
const active = controller;
|
|
575
|
-
if (!active || active.statusTimer) return;
|
|
576
|
-
// 耗时显示本身就是定时业务语义;宿主没有可订阅的“整秒变化”事件。
|
|
577
|
-
active.statusTimer = setInterval(() => {
|
|
578
|
-
if (controller !== active || !isActive(active.state)) return;
|
|
579
|
-
renderStatus(active.ctx, active.state, active.config.language);
|
|
580
|
-
}, 1_000);
|
|
581
|
-
active.statusTimer.unref?.();
|
|
493
|
+
active.watchdog.unref?.();
|
|
582
494
|
}
|
|
583
495
|
|
|
584
|
-
function
|
|
585
|
-
if (
|
|
586
|
-
active.statusTimer = undefined;
|
|
496
|
+
function clearWatchdog(rt: ReviewRuntime) {
|
|
497
|
+
if (rt.controller?.watchdog) clearTimeout(rt.controller.watchdog);
|
|
587
498
|
}
|
|
588
499
|
|
|
589
500
|
function clearFeedbackStartTimer(active: Controller): void {
|
|
@@ -591,55 +502,46 @@ function clearFeedbackStartTimer(active: Controller): void {
|
|
|
591
502
|
active.feedbackStartTimer = undefined;
|
|
592
503
|
}
|
|
593
504
|
|
|
594
|
-
// ----
|
|
505
|
+
// ---- 持久化 ----
|
|
595
506
|
|
|
596
507
|
/** 返回是否已可靠落盘;false 时调用方必须停下本次迁移的副作用。 */
|
|
597
|
-
function persist(
|
|
598
|
-
|
|
599
|
-
|
|
508
|
+
function persist(rt: ReviewRuntime, state: ReviewState): boolean {
|
|
509
|
+
const active = rt.controller;
|
|
510
|
+
if (!active) return false;
|
|
511
|
+
const persisted = active.persistedStamp;
|
|
600
512
|
if (persisted === undefined) return false; // 冲突或写入失败后停写
|
|
601
513
|
try {
|
|
602
|
-
|
|
514
|
+
active.persistedStamp =
|
|
603
515
|
persisted === null
|
|
604
|
-
? beginCheckpoint(pi,
|
|
605
|
-
: writeCheckpoint(pi,
|
|
516
|
+
? beginCheckpoint(rt.pi, active.ctx, state)
|
|
517
|
+
: writeCheckpoint(rt.pi, active.ctx, state, persisted);
|
|
606
518
|
return true;
|
|
607
519
|
} catch (error) {
|
|
520
|
+
// 冲突与写入失败共用的收口:停写、释放占用、中止在途动作并告知。
|
|
521
|
+
const halt = (message: string, level: "warning" | "error") => {
|
|
522
|
+
setOccupancy(rt, active, false);
|
|
523
|
+
active.persistedStamp = undefined;
|
|
524
|
+
active.signal.abort();
|
|
525
|
+
active.actionController?.abort();
|
|
526
|
+
if (active.ctx.hasUI) active.ctx.ui.notify(message, level);
|
|
527
|
+
};
|
|
528
|
+
const en = active.config.language === "en";
|
|
608
529
|
if (error instanceof CheckpointConflictError) {
|
|
609
530
|
// 持久化里出现不是本 controller 写的 Run ID:并发冲突,停止审查。
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
controller.signal.abort();
|
|
613
|
-
controller.actionController?.abort();
|
|
614
|
-
if (controller.ctx.hasUI)
|
|
615
|
-
controller.ctx.ui.notify(
|
|
616
|
-
controller.config.language === "en"
|
|
617
|
-
? "fire-review checkpoint conflict; review stopped."
|
|
618
|
-
: "fire-review checkpoint 冲突,已停止审查。",
|
|
619
|
-
"warning",
|
|
620
|
-
);
|
|
621
|
-
void dispatch(pi, { type: "CANCEL", reason: "shutdown" });
|
|
531
|
+
halt(en ? "fire-review checkpoint conflict; review stopped." : "fire-review checkpoint 冲突,已停止审查。", "warning");
|
|
532
|
+
void dispatch(rt, { type: "CANCEL", reason: "shutdown" });
|
|
622
533
|
return false;
|
|
623
534
|
}
|
|
624
535
|
// 普通写入失败(如会话落盘异常):停掉本场审查,不带着不一致状态继续跑。
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if (controller.ctx.hasUI)
|
|
630
|
-
controller.ctx.ui.notify(
|
|
631
|
-
controller.config.language === "en"
|
|
632
|
-
? `fire-review checkpoint write failed; review stopped: ${errorText(error)}`
|
|
633
|
-
: `fire-review checkpoint 写入失败,已停止审查:${errorText(error)}`,
|
|
634
|
-
"error",
|
|
635
|
-
);
|
|
636
|
-
releaseEditor(controller);
|
|
637
|
-
hideActivity(controller.ctx);
|
|
536
|
+
halt(en
|
|
537
|
+
? `fire-review checkpoint write failed; review stopped: ${errorText(error)}`
|
|
538
|
+
: `fire-review checkpoint 写入失败,已停止审查:${errorText(error)}`, "error");
|
|
539
|
+
clearUi(active);
|
|
638
540
|
// 磁盘上可能还留着上一条活动 checkpoint,重启会把它恢复成幽灵审查:
|
|
639
541
|
// 尽力补写一条终态。写不进去时不假装成功,在通知里告知用户。
|
|
640
542
|
let sealed = true;
|
|
641
543
|
try {
|
|
642
|
-
beginCheckpoint(pi,
|
|
544
|
+
beginCheckpoint(rt.pi, active.ctx, {
|
|
643
545
|
...state,
|
|
644
546
|
phase: "settled",
|
|
645
547
|
active: null,
|
|
@@ -652,16 +554,12 @@ function persist(pi: ExtensionAPI, state: ReviewState): boolean {
|
|
|
652
554
|
}
|
|
653
555
|
// 内存态也必须释放:只停会话但留着活动态 controller,会把幽灵审查从磁盘搬到内存——
|
|
654
556
|
// 后续命令永远被「已有审查在进行中」挡住,且无处取消。
|
|
655
|
-
clearWatchdog();
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
controller = undefined;
|
|
662
|
-
if (!sealed && uiCtx.hasUI)
|
|
663
|
-
uiCtx.ui.notify(
|
|
664
|
-
language === "en"
|
|
557
|
+
clearWatchdog(rt);
|
|
558
|
+
clearFeedbackStartTimer(active);
|
|
559
|
+
rt.controller = undefined;
|
|
560
|
+
if (!sealed && active.ctx.hasUI)
|
|
561
|
+
active.ctx.ui.notify(
|
|
562
|
+
active.config.language === "en"
|
|
665
563
|
? "fire-review could not seal the checkpoint; a restart may resume this review — cancel it with esc."
|
|
666
564
|
: "fire-review 无法写入终态,重启后可能恢复这场审查,到时按 esc 取消。",
|
|
667
565
|
"warning",
|
|
@@ -674,8 +572,8 @@ function errorText(error: unknown) {
|
|
|
674
572
|
return error instanceof Error ? error.message : String(error);
|
|
675
573
|
}
|
|
676
574
|
|
|
677
|
-
function syncOccupancy(active: Controller): void {
|
|
678
|
-
setOccupancy(active, isActive(active.state));
|
|
575
|
+
function syncOccupancy(rt: ReviewRuntime, active: Controller): void {
|
|
576
|
+
setOccupancy(rt, active, isActive(active.state));
|
|
679
577
|
}
|
|
680
578
|
|
|
681
579
|
/**
|
|
@@ -684,7 +582,7 @@ function syncOccupancy(active: Controller): void {
|
|
|
684
582
|
* crash/kill 后无 TTL 的标签永驻会让 Master 把 Worker 的真提问误判为审查占用;
|
|
685
583
|
* 续约同时充当首次投递失败的重试。
|
|
686
584
|
*/
|
|
687
|
-
function publishOccupancyLabel(active: Controller, held: boolean): void {
|
|
585
|
+
function publishOccupancyLabel(rt: ReviewRuntime, active: Controller, held: boolean): void {
|
|
688
586
|
if (held) {
|
|
689
587
|
void sendOccupancyLabel();
|
|
690
588
|
if (!active.occupancyTimer) {
|
|
@@ -697,7 +595,7 @@ function publishOccupancyLabel(active: Controller, held: boolean): void {
|
|
|
697
595
|
clearInterval(active.occupancyTimer);
|
|
698
596
|
active.occupancyTimer = undefined;
|
|
699
597
|
}
|
|
700
|
-
void clearOccupancyLabel();
|
|
598
|
+
void clearOccupancyLabel(rt);
|
|
701
599
|
}
|
|
702
600
|
|
|
703
601
|
function sendOccupancyLabel(): Promise<boolean> {
|
|
@@ -712,12 +610,12 @@ function sendOccupancyLabel(): Promise<boolean> {
|
|
|
712
610
|
});
|
|
713
611
|
}
|
|
714
612
|
|
|
715
|
-
async function clearOccupancyLabel(): Promise<void> {
|
|
613
|
+
async function clearOccupancyLabel(rt: ReviewRuntime): Promise<void> {
|
|
716
614
|
const env = herdrPaneEnv();
|
|
717
615
|
if (!env) return;
|
|
718
616
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
719
617
|
// 新审查已重新持有时中止重试:迟到的清除会撤掉新租约的标签。
|
|
720
|
-
if (controller?.occupancyHeld) return;
|
|
618
|
+
if (rt.controller?.occupancyHeld) return;
|
|
721
619
|
const delivered = await herdrRequest(OCCUPANCY_SOURCE, "pane.report_metadata", {
|
|
722
620
|
pane_id: env.paneId,
|
|
723
621
|
source: OCCUPANCY_SOURCE,
|
|
@@ -729,18 +627,18 @@ async function clearOccupancyLabel(): Promise<void> {
|
|
|
729
627
|
// 两次未送达:标签带 TTL,最迟 60s 自行过期,不会永久残留。
|
|
730
628
|
}
|
|
731
629
|
|
|
732
|
-
function setOccupancy(active: Controller, held: boolean): void {
|
|
630
|
+
function setOccupancy(rt: ReviewRuntime, active: Controller, held: boolean): void {
|
|
733
631
|
if (Boolean(active.occupancyHeld) === held) return;
|
|
734
632
|
active.occupancyHeld = held;
|
|
735
633
|
// 标签走 metadata state_labels:herdr 会丢弃 report_agent 的 message(实测),
|
|
736
634
|
// 只有这条通道能同时到达 Master(state_labels 判定)与侧边栏(state_text token)。
|
|
737
635
|
// 频道仍要发:它驱动 herdr 集成的 blocked 状态本身。占用信号不伤审查。
|
|
738
|
-
publishOccupancyLabel(active, held);
|
|
636
|
+
publishOccupancyLabel(rt, active, held);
|
|
739
637
|
try {
|
|
740
|
-
|
|
741
|
-
active:
|
|
742
|
-
|
|
743
|
-
|
|
638
|
+
const payload: OccupancyPayload = held
|
|
639
|
+
? { active: true, label: OCCUPANCY_LABEL, progress: () => reviewProgress(rt) }
|
|
640
|
+
: { active: false };
|
|
641
|
+
rt.pi.events.emit(OCCUPANCY_CHANNEL, payload);
|
|
744
642
|
} catch (error) {
|
|
745
643
|
// 占用信号只对齐 Herdr 展示;集成故障不能改变审查状态机或会话生命周期。
|
|
746
644
|
try {
|
|
@@ -752,144 +650,81 @@ function setOccupancy(active: Controller, held: boolean): void {
|
|
|
752
650
|
}
|
|
753
651
|
}
|
|
754
652
|
|
|
755
|
-
/**
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
*/
|
|
759
|
-
function syncUi(): void {
|
|
760
|
-
const active = controller;
|
|
653
|
+
/** UI 投影:终端标题 + esc 接管,全部从当前状态派生;审查进度经占用频道由输入框外壳显示。 */
|
|
654
|
+
function syncUi(rt: ReviewRuntime): void {
|
|
655
|
+
const active = rt.controller;
|
|
761
656
|
if (!active) return;
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
} else releaseEditor(active);
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
clearStatusTimer(active);
|
|
775
|
-
hideActivity(active.ctx);
|
|
657
|
+
if (!isActive(active.state) || !active.ctx.hasUI) return clearUi(active);
|
|
658
|
+
showReviewTitle(active.ctx, active.state.round, active.config.language);
|
|
659
|
+
// 只在等模型结论时接管编辑器;awaiting_fix 相把输入交还用户。
|
|
660
|
+
if (!canCancelWithKey(rt)) return releaseEditor(active);
|
|
661
|
+
active.unlockEditor ??= lockEditor(active.ctx, () => cancelByUser(rt));
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function clearUi(active: Controller) {
|
|
665
|
+
hideReviewTitle(active.ctx);
|
|
776
666
|
releaseEditor(active);
|
|
777
667
|
}
|
|
778
668
|
|
|
779
669
|
function releaseEditor(active: Controller) {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
active.editorLocked = false;
|
|
670
|
+
active.unlockEditor?.();
|
|
671
|
+
active.unlockEditor = undefined;
|
|
783
672
|
}
|
|
784
673
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
advisorRunning: active.state.phase === "needs_fix",
|
|
798
|
-
consecutiveFailures: active.state.consecutiveFailures,
|
|
799
|
-
cwd: active.ctx.cwd,
|
|
800
|
-
language: active.config.language,
|
|
801
|
-
};
|
|
674
|
+
const STAGE: Partial<Record<Phase, ReviewStage>> = {
|
|
675
|
+
queued: "queued", reviewing: "reviewing", needs_fix: "advisor", awaiting_fix: "fixing", summarizing: "summarizing",
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
/** 审查进度只读 reducer 的当前状态;只有审查相的票数可数。 */
|
|
679
|
+
function reviewProgress(rt: ReviewRuntime): ReviewProgress | undefined {
|
|
680
|
+
const state = rt.controller?.state;
|
|
681
|
+
const stage = state && isActive(state) ? STAGE[state.phase] : undefined;
|
|
682
|
+
if (!state || !stage) return undefined;
|
|
683
|
+
const reviewers = stage === "reviewing" ? state.active?.reviewers ?? [] : [];
|
|
684
|
+
const count = (status: string) => reviewers.filter((reviewer) => reviewer.status === status).length;
|
|
685
|
+
return { stage, round: state.round, passed: count("passed"), blocked: count("failed"), total: reviewers.length };
|
|
802
686
|
}
|
|
803
687
|
|
|
804
|
-
function canCancelWithKey() {
|
|
805
|
-
const phase = controller?.state.phase;
|
|
688
|
+
function canCancelWithKey(rt: ReviewRuntime) {
|
|
689
|
+
const phase = rt.controller?.state.phase;
|
|
806
690
|
return phase === "queued" || phase === "reviewing" || phase === "needs_fix";
|
|
807
691
|
}
|
|
808
692
|
|
|
809
|
-
function cancelByUser() {
|
|
810
|
-
const active = controller;
|
|
693
|
+
function cancelByUser(rt: ReviewRuntime) {
|
|
694
|
+
const active = rt.controller;
|
|
811
695
|
if (!active) return;
|
|
812
696
|
if (active.state.phase === "needs_fix") {
|
|
813
697
|
// pi-flow 语义:顾问阶段的 Esc 只跳过本次咨询,不取消整场审查。
|
|
814
698
|
active.actionController?.abort();
|
|
815
699
|
void (active.actionPromise ?? Promise.resolve()).then(() =>
|
|
816
|
-
dispatch(
|
|
700
|
+
dispatch(rt, { type: "ADVISOR_SKIPPED" }),
|
|
817
701
|
);
|
|
818
702
|
return;
|
|
819
703
|
}
|
|
820
704
|
active.signal.abort();
|
|
821
705
|
active.actionController?.abort();
|
|
822
|
-
void dispatch(
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
function renderStatus(
|
|
826
|
-
ctx: ExtensionContext,
|
|
827
|
-
state: ReviewState,
|
|
828
|
-
language: Language,
|
|
829
|
-
) {
|
|
830
|
-
if (!ctx.hasUI) return;
|
|
831
|
-
let prefix: string | undefined;
|
|
832
|
-
if (state.phase === "queued")
|
|
833
|
-
prefix = language === "en"
|
|
834
|
-
? "🔥 running · review when done"
|
|
835
|
-
: "🔥 执行中 · 完成后自动审查";
|
|
836
|
-
else if (state.phase === "reviewing" || state.phase === "needs_fix")
|
|
837
|
-
prefix = `🔥 ${roundStatus(state.round, language === "en" ? "review" : "审查", language)}`;
|
|
838
|
-
else if (state.phase === "awaiting_fix")
|
|
839
|
-
prefix = `🔥 ${roundStatus(state.round, language === "en" ? "repair" : "修复中", language)}`;
|
|
840
|
-
else if (state.phase === "summarizing")
|
|
841
|
-
prefix = language === "en" ? "🔥 summarizing" : "🔥 总结中";
|
|
842
|
-
if (!prefix) {
|
|
843
|
-
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
const stepStartedAt = state.phase === "awaiting_fix"
|
|
847
|
-
? state.updatedAt
|
|
848
|
-
: state.roundStartedAt || state.startedAt;
|
|
849
|
-
const stepMs = stepStartedAt ? Date.now() - stepStartedAt : 0;
|
|
850
|
-
const totalMs = state.startedAt ? Date.now() - state.startedAt : 0;
|
|
851
|
-
const showTotal = state.round > 1 || state.history.some((round) => round.result === "failed");
|
|
852
|
-
const elapsed = showTotal
|
|
853
|
-
? `${formatDuration(stepMs)} / ${language === "en" ? "total" : "总"} ${formatDuration(totalMs)}`
|
|
854
|
-
: formatDuration(stepMs);
|
|
855
|
-
ctx.ui.setStatus(STATUS_KEY, `${prefix} · ${elapsed}`);
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
function roundStatus(round: number, phase: string, language: Language) {
|
|
860
|
-
if (round <= 1) return phase;
|
|
861
|
-
return language === "en" ? `Round ${round} ${phase}` : `第 ${round} 轮${phase}`;
|
|
706
|
+
void dispatch(rt, { type: "CANCEL", reason: "user" });
|
|
862
707
|
}
|
|
863
708
|
|
|
864
709
|
// ---- 副作用执行器 ----
|
|
865
710
|
|
|
866
711
|
/** reducer 只发卡或请求推进;所有会启动工作的动作统一经过 idle barrier。 */
|
|
867
|
-
async function runEffects(effects: ReviewEffect[]) {
|
|
712
|
+
async function runEffects(rt: ReviewRuntime, effects: ReviewEffect[]) {
|
|
868
713
|
for (const effect of effects) {
|
|
869
|
-
const active = controller;
|
|
714
|
+
const active = rt.controller;
|
|
870
715
|
if (!active) return;
|
|
871
716
|
if (effect.kind === "advance") {
|
|
872
|
-
requestAdvance(
|
|
717
|
+
requestAdvance(rt, active.ctx);
|
|
873
718
|
continue;
|
|
874
719
|
}
|
|
875
720
|
try {
|
|
876
|
-
sendCard(
|
|
721
|
+
sendCard(rt, effect.card);
|
|
877
722
|
} catch (error) {
|
|
878
|
-
notifyEffectFailure(error);
|
|
723
|
+
notifyEffectFailure(rt, error);
|
|
879
724
|
}
|
|
880
725
|
}
|
|
881
726
|
}
|
|
882
727
|
|
|
883
|
-
async function resolveConfiguredModel(id: string): Promise<Model<any>> {
|
|
884
|
-
const runtime = await ModelRuntime.create({
|
|
885
|
-
authPath: `${getAgentDir()}/auth.json`,
|
|
886
|
-
modelsPath: `${getAgentDir()}/models.json`,
|
|
887
|
-
});
|
|
888
|
-
const slash = id.indexOf("/");
|
|
889
|
-
const model = slash > 0 ? runtime.getModel(id.slice(0, slash), id.slice(slash + 1)) : undefined;
|
|
890
|
-
if (!model) throw new Error(`找不到模型:${id};审查会话只能使用内置 provider 的模型`);
|
|
891
|
-
return model;
|
|
892
|
-
}
|
|
893
728
|
|
|
894
729
|
function reviewerModelConfig(model: ReviewConfig["advisor"], config: ReviewConfig): ReviewModelConfig {
|
|
895
730
|
return {
|
|
@@ -901,30 +736,14 @@ function reviewerModelConfig(model: ReviewConfig["advisor"], config: ReviewConfi
|
|
|
901
736
|
}
|
|
902
737
|
|
|
903
738
|
/** 开审那一刻取会话分支快照构造 prompt,所有审查者共用同一 prompt。 */
|
|
904
|
-
async function startReviewers(
|
|
905
|
-
const active = controller;
|
|
739
|
+
async function startReviewers(rt: ReviewRuntime): Promise<void> {
|
|
740
|
+
const active = rt.controller;
|
|
906
741
|
if (!active) return;
|
|
907
742
|
const { state, config } = active;
|
|
908
743
|
if (!state.active) return;
|
|
909
744
|
const currentActive = state.active;
|
|
910
745
|
const actionSignal = active.actionController?.signal ?? active.signal.signal;
|
|
911
|
-
|
|
912
|
-
active.progressKind = "reviewers";
|
|
913
|
-
active.progress = initialProgress(
|
|
914
|
-
currentActive.reviewers.map((item) => ({ model: item.model })),
|
|
915
|
-
config.language,
|
|
916
|
-
);
|
|
917
|
-
for (const reviewer of currentActive.reviewers)
|
|
918
|
-
if (reviewer.status !== "running" && reviewer.result)
|
|
919
|
-
active.progress = settleProgress(
|
|
920
|
-
active.progress,
|
|
921
|
-
reviewer.index,
|
|
922
|
-
reviewer.status,
|
|
923
|
-
config.language,
|
|
924
|
-
reviewer.result.summary,
|
|
925
|
-
reviewer.result.details,
|
|
926
|
-
);
|
|
927
|
-
const evidence = buildEvidence(sessionEntries(), config.language);
|
|
746
|
+
const evidence = buildEvidence(sessionEntries(rt), config.language, { sessionFile: active.ctx.sessionManager.getSessionFile() });
|
|
928
747
|
const prompt = buildReviewPrompt(readPrompt("review", config.language), {
|
|
929
748
|
language: config.language,
|
|
930
749
|
scope: scopeText(config.language),
|
|
@@ -944,31 +763,13 @@ async function startReviewers(pi: ExtensionAPI): Promise<void> {
|
|
|
944
763
|
cwd: active.ctx.cwd,
|
|
945
764
|
language: config.language,
|
|
946
765
|
signal: actionSignal,
|
|
947
|
-
runSession:
|
|
948
|
-
onEvent: (event) => {
|
|
949
|
-
if (controller !== active) return;
|
|
950
|
-
active.progress = applySessionEvent(
|
|
951
|
-
active.progress,
|
|
952
|
-
reviewer.index,
|
|
953
|
-
event,
|
|
954
|
-
config.language,
|
|
955
|
-
);
|
|
956
|
-
},
|
|
766
|
+
runSession: rt.runSession,
|
|
957
767
|
});
|
|
958
|
-
if (controller === active)
|
|
959
|
-
active.progress = settleProgress(
|
|
960
|
-
active.progress,
|
|
961
|
-
result.index,
|
|
962
|
-
result.status,
|
|
963
|
-
config.language,
|
|
964
|
-
result.summary,
|
|
965
|
-
result.details,
|
|
966
|
-
);
|
|
967
768
|
if (!actionSignal.aborted)
|
|
968
|
-
await dispatch(
|
|
769
|
+
await dispatch(rt, { type: "REVIEWER_SETTLED", index: result.index, result });
|
|
969
770
|
} catch (error) {
|
|
970
771
|
if (actionSignal.aborted) return;
|
|
971
|
-
await dispatch(
|
|
772
|
+
await dispatch(rt, {
|
|
972
773
|
type: "REVIEWER_SETTLED",
|
|
973
774
|
index: reviewer.index,
|
|
974
775
|
result: {
|
|
@@ -985,16 +786,13 @@ async function startReviewers(pi: ExtensionAPI): Promise<void> {
|
|
|
985
786
|
await Promise.all(tasks);
|
|
986
787
|
}
|
|
987
788
|
|
|
988
|
-
async function consultAdvisor(
|
|
989
|
-
const active = controller;
|
|
789
|
+
async function consultAdvisor(rt: ReviewRuntime): Promise<void> {
|
|
790
|
+
const active = rt.controller;
|
|
990
791
|
if (!active) return;
|
|
991
792
|
const { state, config } = active;
|
|
992
793
|
if (!state.pending) return;
|
|
993
794
|
const pending = state.pending;
|
|
994
795
|
const actionSignal = active.actionController?.signal ?? active.signal.signal;
|
|
995
|
-
active.progressStartedAt = Date.now();
|
|
996
|
-
active.progressKind = "advisor";
|
|
997
|
-
active.progress = initialProgress([{ model: config.advisor.model }], config.language);
|
|
998
796
|
const prompt = buildAdvisorPrompt(readPrompt("advisor", config.language), {
|
|
999
797
|
language: config.language,
|
|
1000
798
|
focus: state.focus,
|
|
@@ -1009,57 +807,19 @@ async function consultAdvisor(pi: ExtensionAPI): Promise<void> {
|
|
|
1009
807
|
cwd: active.ctx.cwd,
|
|
1010
808
|
language: config.language,
|
|
1011
809
|
signal: actionSignal,
|
|
1012
|
-
runSession:
|
|
1013
|
-
onEvent: (event) => {
|
|
1014
|
-
if (controller !== active) return;
|
|
1015
|
-
active.progress = applySessionEvent(active.progress, 0, event, config.language);
|
|
1016
|
-
},
|
|
810
|
+
runSession: rt.runSession,
|
|
1017
811
|
});
|
|
1018
|
-
if (controller === active)
|
|
1019
|
-
active.progress = settleProgress(
|
|
1020
|
-
active.progress,
|
|
1021
|
-
0,
|
|
1022
|
-
"passed",
|
|
1023
|
-
config.language,
|
|
1024
|
-
advisorSummary(result, config.language),
|
|
1025
|
-
result.advice,
|
|
1026
|
-
);
|
|
1027
812
|
if (!actionSignal.aborted)
|
|
1028
|
-
await dispatch(
|
|
813
|
+
await dispatch(rt, { type: "ADVISOR_SETTLED", result });
|
|
1029
814
|
} catch (error) {
|
|
1030
815
|
if (actionSignal.aborted) return;
|
|
1031
|
-
await dispatch(
|
|
816
|
+
await dispatch(rt, {
|
|
1032
817
|
type: "INFRASTRUCTURE_ERROR",
|
|
1033
818
|
details: sessionErrorText("advisor", active.config.language, error),
|
|
1034
819
|
});
|
|
1035
820
|
}
|
|
1036
821
|
}
|
|
1037
822
|
|
|
1038
|
-
/** 顾问落定后的一行摘要:裁决 + 「下一步方向」首句,与审查者摘要同一展示通道。
|
|
1039
|
-
* 建议正文以「核实结论」段开头,直取首行只会露出段标题星号,预览零信息量。 */
|
|
1040
|
-
function advisorSummary(result: AdvisorResult, language: Language) {
|
|
1041
|
-
const label = decisionText(result.verdict, language);
|
|
1042
|
-
const first = adviceHighlight(result.advice);
|
|
1043
|
-
if (!first) return label;
|
|
1044
|
-
return language === "en" ? `${label}: ${first}` : `${label}:${first}`;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
const NEXT_DIRECTION_LABEL = /^\*{0,2}(?:下一步方向|Next direction)\*{0,2}\s*[::]\s*/iu;
|
|
1048
|
-
|
|
1049
|
-
function adviceHighlight(advice: string) {
|
|
1050
|
-
const lines = advice.split(/\r?\n/u);
|
|
1051
|
-
const index = lines.findIndex((line) => NEXT_DIRECTION_LABEL.test(line.trim()));
|
|
1052
|
-
const source = index >= 0
|
|
1053
|
-
? [lines[index].trim().replace(NEXT_DIRECTION_LABEL, ""), ...lines.slice(index + 1)]
|
|
1054
|
-
: lines;
|
|
1055
|
-
const first = source.map(stripBold).find((line) => line.trim())?.trim() ?? "";
|
|
1056
|
-
return first.replace(/^[-*+]\s*/u, "").trim();
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
function stripBold(text: string) {
|
|
1060
|
-
return text.replace(/\*\*([^*]+)\*\*/gu, "$1");
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
823
|
function sessionErrorText(kind: "reviewer" | "advisor", language: Language, error: unknown) {
|
|
1064
824
|
const message = error instanceof Error ? error.message : String(error);
|
|
1065
825
|
if (kind === "reviewer")
|
|
@@ -1067,13 +827,9 @@ function sessionErrorText(kind: "reviewer" | "advisor", language: Language, erro
|
|
|
1067
827
|
return language === "en" ? `advisor session error: ${message}` : `顾问会话异常:${message}`;
|
|
1068
828
|
}
|
|
1069
829
|
|
|
1070
|
-
function deliverFeedbackNow(
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
advisor: AdvisorResult | null,
|
|
1074
|
-
) {
|
|
1075
|
-
const active = controller;
|
|
1076
|
-
if (!active || active.pi !== pi) return;
|
|
830
|
+
function deliverFeedbackNow(rt: ReviewRuntime, details: string, advisor: AdvisorResult | null) {
|
|
831
|
+
const active = rt.controller;
|
|
832
|
+
if (!active) return;
|
|
1077
833
|
const feedback = buildFixFeedback({
|
|
1078
834
|
language: active.config.language,
|
|
1079
835
|
details,
|
|
@@ -1083,7 +839,7 @@ function deliverFeedbackNow(
|
|
|
1083
839
|
// API 返回 void,真实异步失败不会进 try/catch;持久化状态等待 agent_start 回执。
|
|
1084
840
|
active.feedbackStartTimer = setTimeout(() => {
|
|
1085
841
|
if (
|
|
1086
|
-
controller !== active ||
|
|
842
|
+
rt.controller !== active ||
|
|
1087
843
|
active.state.phase !== "awaiting_fix" ||
|
|
1088
844
|
active.state.repair?.status !== "awaiting_start"
|
|
1089
845
|
) return;
|
|
@@ -1096,12 +852,12 @@ function deliverFeedbackNow(
|
|
|
1096
852
|
"error",
|
|
1097
853
|
);
|
|
1098
854
|
active.signal.abort();
|
|
1099
|
-
void dispatch(
|
|
855
|
+
void dispatch(rt, { type: "CANCEL", reason: "user" });
|
|
1100
856
|
}, FEEDBACK_START_TIMEOUT_MS);
|
|
1101
857
|
active.feedbackStartTimer.unref?.();
|
|
1102
858
|
// display:false 的消息进 LLM 上下文但不渲染;triggerTurn 让执行模型开始修复回合。
|
|
1103
859
|
try {
|
|
1104
|
-
pi.sendMessage(
|
|
860
|
+
rt.pi.sendMessage(
|
|
1105
861
|
{ customType: FEEDBACK_TYPE, content: feedback, display: false },
|
|
1106
862
|
{ deliverAs: "followUp", triggerTurn: true },
|
|
1107
863
|
);
|
|
@@ -1112,9 +868,9 @@ function deliverFeedbackNow(
|
|
|
1112
868
|
}
|
|
1113
869
|
|
|
1114
870
|
/** 总结提示投递:与修复反馈同一套 agent_start 回执机制;失败不升级,静默收尾。 */
|
|
1115
|
-
function deliverSummaryNow(
|
|
1116
|
-
const active = controller;
|
|
1117
|
-
if (!active ||
|
|
871
|
+
function deliverSummaryNow(rt: ReviewRuntime, state: ReviewState): void {
|
|
872
|
+
const active = rt.controller;
|
|
873
|
+
if (!active || !state.summary) return;
|
|
1118
874
|
const last = state.history.at(-1);
|
|
1119
875
|
const material = state.summary.kind === "advisor_stop"
|
|
1120
876
|
? last?.advisor?.advice ?? last?.details ?? ""
|
|
@@ -1128,7 +884,7 @@ function deliverSummaryNow(pi: ExtensionAPI, state: ReviewState): void {
|
|
|
1128
884
|
clearFeedbackStartTimer(active);
|
|
1129
885
|
active.feedbackStartTimer = setTimeout(() => {
|
|
1130
886
|
if (
|
|
1131
|
-
controller !== active ||
|
|
887
|
+
rt.controller !== active ||
|
|
1132
888
|
active.state.phase !== "summarizing" ||
|
|
1133
889
|
active.state.summary?.status !== "awaiting_start"
|
|
1134
890
|
) return;
|
|
@@ -1141,62 +897,62 @@ function deliverSummaryNow(pi: ExtensionAPI, state: ReviewState): void {
|
|
|
1141
897
|
: "fire-review 总结回合未能启动,已直接收尾。",
|
|
1142
898
|
"warning",
|
|
1143
899
|
);
|
|
1144
|
-
void dispatch(
|
|
900
|
+
void dispatch(rt, { type: "SUMMARY_SETTLED" });
|
|
1145
901
|
}, FEEDBACK_START_TIMEOUT_MS);
|
|
1146
902
|
active.feedbackStartTimer.unref?.();
|
|
1147
903
|
try {
|
|
1148
|
-
pi.sendMessage(
|
|
904
|
+
rt.pi.sendMessage(
|
|
1149
905
|
{ customType: SUMMARY_REQUEST_TYPE, content: prompt, display: false },
|
|
1150
906
|
{ deliverAs: "followUp", triggerTurn: true },
|
|
1151
907
|
);
|
|
1152
908
|
} catch (error) {
|
|
1153
909
|
clearFeedbackStartTimer(active);
|
|
1154
|
-
notifyEffectFailure(error);
|
|
1155
|
-
void dispatch(
|
|
910
|
+
notifyEffectFailure(rt, error);
|
|
911
|
+
void dispatch(rt, { type: "SUMMARY_SETTLED" });
|
|
1156
912
|
}
|
|
1157
913
|
}
|
|
1158
914
|
|
|
1159
|
-
function sendCard(
|
|
1160
|
-
|
|
915
|
+
function sendCard(rt: ReviewRuntime, card: CardData) {
|
|
916
|
+
const active = rt.controller;
|
|
917
|
+
if (!active) return;
|
|
1161
918
|
// pi-flow 的用户取消是即时临时通知,不进会话;shutdown 静默收口。
|
|
1162
919
|
if (card.kind === "cancel") {
|
|
1163
|
-
if (card.reason === "user" &&
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
? "
|
|
1167
|
-
: "
|
|
920
|
+
if (card.reason === "user" && active.ctx.hasUI)
|
|
921
|
+
active.ctx.ui.notify(
|
|
922
|
+
active.config.language === "en"
|
|
923
|
+
? "Review cancelled\nStopped by user"
|
|
924
|
+
: "审查已取消\n已按你的操作停止",
|
|
1168
925
|
"info",
|
|
1169
926
|
);
|
|
1170
927
|
return;
|
|
1171
928
|
}
|
|
1172
929
|
// 宿主在 streaming 时会把无 options 的 sendMessage 当 steer 塞进当前模型回合。
|
|
1173
930
|
// 卡片只是 UI 投影,绝不能因此唤醒或打断执行模型。
|
|
1174
|
-
if (!
|
|
1175
|
-
|
|
931
|
+
if (!active.ctx.isIdle()) {
|
|
932
|
+
active.pendingCards.push(card);
|
|
1176
933
|
return;
|
|
1177
934
|
}
|
|
1178
|
-
sendCardNow(
|
|
935
|
+
sendCardNow(rt, active, card);
|
|
1179
936
|
}
|
|
1180
937
|
|
|
1181
|
-
function flushPendingCards(
|
|
1182
|
-
const active = controller;
|
|
938
|
+
function flushPendingCards(rt: ReviewRuntime): void {
|
|
939
|
+
const active = rt.controller;
|
|
1183
940
|
if (!active || !active.ctx.isIdle() || active.pendingCards.length === 0) return;
|
|
1184
941
|
const cards = active.pendingCards.splice(0);
|
|
1185
942
|
for (const card of cards) {
|
|
1186
943
|
try {
|
|
1187
|
-
sendCardNow(
|
|
944
|
+
sendCardNow(rt, active, card);
|
|
1188
945
|
} catch (error) {
|
|
1189
|
-
notifyEffectFailure(error);
|
|
946
|
+
notifyEffectFailure(rt, error);
|
|
1190
947
|
}
|
|
1191
948
|
}
|
|
1192
949
|
}
|
|
1193
950
|
|
|
1194
|
-
function sendCardNow(
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
pi.sendMessage({
|
|
951
|
+
function sendCardNow(rt: ReviewRuntime, active: Controller, card: CardData): void {
|
|
952
|
+
const built = buildCard(card, active.config.language);
|
|
953
|
+
rt.pi.sendMessage({
|
|
1198
954
|
customType: CARD_TYPE,
|
|
1199
|
-
content:
|
|
955
|
+
content: wrapEnvelope("firecode_review", built.content),
|
|
1200
956
|
display: true,
|
|
1201
957
|
details: built.details,
|
|
1202
958
|
});
|
|
@@ -1210,8 +966,8 @@ function parseCommand(args: string, language: Language): { focus: string } | { e
|
|
|
1210
966
|
}
|
|
1211
967
|
|
|
1212
968
|
/** 会话分支 entries(供证据组装);本插件的卡与反馈消息不参与证据,避免自指。 */
|
|
1213
|
-
function sessionEntries() {
|
|
1214
|
-
const manager = controller?.ctx.sessionManager as
|
|
969
|
+
function sessionEntries(rt: ReviewRuntime) {
|
|
970
|
+
const manager = rt.controller?.ctx.sessionManager as
|
|
1215
971
|
| { getBranch?: () => unknown[] }
|
|
1216
972
|
| undefined;
|
|
1217
973
|
const entries = manager?.getBranch?.() ?? [];
|
|
@@ -1234,13 +990,3 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
1234
990
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1235
991
|
}
|
|
1236
992
|
|
|
1237
|
-
// 导出供测试用(纯函数 / 类型)
|
|
1238
|
-
export { CHECKPOINT_TYPE };
|
|
1239
|
-
|
|
1240
|
-
/** 测试专用:等待 event-loop barrier 与其产生的状态迁移排空。 */
|
|
1241
|
-
export async function __reviewFlushForTests(): Promise<void> {
|
|
1242
|
-
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
1243
|
-
await dispatchQueue;
|
|
1244
|
-
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
1245
|
-
await dispatchQueue;
|
|
1246
|
-
}
|