pi-firecode 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -41
- package/README.zh-CN.md +78 -0
- package/config.example.jsonc +21 -22
- package/dist/config.example.jsonc +90 -0
- package/dist/index.js +11443 -0
- package/dist/master/prompts/master.en.md +22 -0
- package/{master → dist/master}/prompts/master.zh.md +3 -3
- package/dist/master/prompts/worker.en.md +1 -0
- package/dist/watcher/prompts/watch.en.md +43 -0
- package/package.json +10 -27
- package/activity.ts +0 -92
- package/busy.ts +0 -188
- package/config.ts +0 -500
- package/deliver.ts +0 -119
- package/flame.ts +0 -149
- package/format.ts +0 -133
- package/header.ts +0 -218
- package/herdr-client.ts +0 -60
- package/index.ts +0 -68
- package/jsonc.ts +0 -34
- package/master/actions.ts +0 -293
- package/master/activity-list.ts +0 -281
- package/master/event-card.ts +0 -87
- package/master/event-format.ts +0 -83
- package/master/guard.ts +0 -46
- package/master/index.ts +0 -252
- package/master/list-view.ts +0 -129
- package/master/outbox.ts +0 -188
- package/master/prompt.ts +0 -26
- package/master/role.ts +0 -18
- package/master/run.ts +0 -315
- package/master/runtime.ts +0 -259
- package/master/spawn.ts +0 -234
- package/master/state.ts +0 -225
- package/master/worker-view.ts +0 -517
- package/provider/claude-sub.ts +0 -151
- package/provider/openai-native/index.ts +0 -8
- package/provider/openai-native/src/compact-client.ts +0 -350
- package/provider/openai-native/src/config.ts +0 -297
- package/provider/openai-native/src/extension.ts +0 -93
- package/provider/openai-native/src/native-compaction.ts +0 -151
- package/provider/openai-native/src/native-details.ts +0 -157
- package/provider/openai-native/src/native-replay.ts +0 -253
- package/provider/openai-native/src/native-runtime.ts +0 -144
- package/provider/openai-native/src/options.ts +0 -87
- package/provider/openai-native/src/request-pipeline.ts +0 -21
- package/provider/openai-native/src/responses-input.ts +0 -433
- package/review/advisor.ts +0 -110
- package/review/card.ts +0 -398
- package/review/checkpoint.ts +0 -378
- package/review/evidence.ts +0 -230
- package/review/index.ts +0 -992
- package/review/occupancy.ts +0 -25
- package/review/outcome.ts +0 -98
- package/review/prompt.ts +0 -209
- package/review/reviewer.ts +0 -353
- package/review/session.ts +0 -110
- package/review/state.ts +0 -877
- package/review/ui.ts +0 -89
- package/round-recorder.ts +0 -18
- package/session/herdr-display.ts +0 -87
- package/session/presets.ts +0 -300
- package/session/quota.ts +0 -123
- package/session/rename.ts +0 -46
- package/session/stats.ts +0 -313
- package/statusbar/index.ts +0 -256
- package/statusbar/render.ts +0 -113
- package/theme.ts +0 -45
- package/tools/actions.ts +0 -53
- package/tools/assistant-view.ts +0 -64
- package/tools/click-anchor.ts +0 -56
- package/tools/group-view.ts +0 -420
- package/tools/grouping.ts +0 -158
- package/tools/host.ts +0 -369
- package/tools/index.ts +0 -186
- package/tools/line.ts +0 -231
- package/tools/machine.ts +0 -78
- package/tools/parts.ts +0 -143
- package/tools/round.ts +0 -95
- package/tools/timing.ts +0 -22
- package/tools/turn-clock.ts +0 -41
- package/tools/turn-summary.ts +0 -109
- package/truncated-write.ts +0 -18
- package/watcher/card.ts +0 -91
- package/watcher/index.ts +0 -176
- package/watcher/observer.ts +0 -75
- package/watcher/transcript.ts +0 -56
- /package/{master → dist/master}/prompts/worker.zh.md +0 -0
- /package/{review → dist/review}/prompts/advisor.en.md +0 -0
- /package/{review → dist/review}/prompts/advisor.zh.md +0 -0
- /package/{review → dist/review}/prompts/review.en.md +0 -0
- /package/{review → dist/review}/prompts/review.zh.md +0 -0
- /package/{watcher → dist/watcher}/prompts/watch.zh.md +0 -0
package/review/index.ts
DELETED
|
@@ -1,992 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* /fire-review:对抗性审查插件的执行器与入口。
|
|
3
|
-
*
|
|
4
|
-
* 职责分界:
|
|
5
|
-
* - 领域状态只活在纯 reducer(state.ts)里,所有迁移经 reduce() 计算;
|
|
6
|
-
* 本文件是唯一执行器,只做副作用(起审查会话、投递反馈、发卡、持久化、审查活动行),
|
|
7
|
-
* 会话结果一律回灌成事件交给 reducer。
|
|
8
|
-
* - 运行时状态按会话隔离:pi 在同一进程内对同一 cwd 复用扩展模块实例,主会话与每个
|
|
9
|
-
* Worker 子会话共用本文件;`registerReview(pi)` 各自持有一份 ReviewRuntime,模块级不留
|
|
10
|
-
* 任何会话状态,一个会话被 dispose 不会拖累另一个。
|
|
11
|
-
* - 渲染器在此顶层无条件注册(不懒加载),live 与 reload 外观一致。
|
|
12
|
-
*/
|
|
13
|
-
import { randomUUID } from "node:crypto";
|
|
14
|
-
import { wrapEnvelope } from "../deliver.js";
|
|
15
|
-
import {
|
|
16
|
-
type ExtensionAPI,
|
|
17
|
-
type ExtensionContext,
|
|
18
|
-
} from "@earendil-works/pi-coding-agent";
|
|
19
|
-
import { loadConfig, type Language, type ReviewConfig, type Section } from "../config.js";
|
|
20
|
-
import { herdrPaneEnv, herdrRequest } from "../herdr-client.js";
|
|
21
|
-
import { InProcessSessionPool } from "../master/spawn.js";
|
|
22
|
-
import { buildCard, CARD_TYPE, registerCardRenderer } from "./card.js";
|
|
23
|
-
import {
|
|
24
|
-
beginCheckpoint,
|
|
25
|
-
CheckpointConflictError,
|
|
26
|
-
type CheckpointStamp,
|
|
27
|
-
readCheckpoint,
|
|
28
|
-
readStamp,
|
|
29
|
-
writeCheckpoint,
|
|
30
|
-
} from "./checkpoint.js";
|
|
31
|
-
import { buildEvidence } from "./evidence.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";
|
|
35
|
-
import { runAdvisor } from "./advisor.js";
|
|
36
|
-
import { runReviewer, type ReviewModelConfig } from "./reviewer.js";
|
|
37
|
-
import { createReviewSessionRunner, type ReviewSessionRunner } from "./session.js";
|
|
38
|
-
import {
|
|
39
|
-
type AdvisorResult,
|
|
40
|
-
type CardData,
|
|
41
|
-
type Phase,
|
|
42
|
-
type ReviewEffect,
|
|
43
|
-
type ReviewEvent,
|
|
44
|
-
type ReviewLimits,
|
|
45
|
-
type ReviewState,
|
|
46
|
-
initialState,
|
|
47
|
-
reduce,
|
|
48
|
-
} from "./state.js";
|
|
49
|
-
|
|
50
|
-
export const FEEDBACK_TYPE = "firecode-review-feedback";
|
|
51
|
-
/** 总结回合提示:与修复反馈同通道(进上下文不渲染),不参与证据自指。 */
|
|
52
|
-
export const SUMMARY_REQUEST_TYPE = "firecode-review-summary";
|
|
53
|
-
const OCCUPANCY_SOURCE = "firecode-review";
|
|
54
|
-
/** herdr 按 seq 丢弃过期上报;同一 source 单调递增。 */
|
|
55
|
-
let occupancySeq = Date.now() * 1000;
|
|
56
|
-
/** 标签租约:TTL 要容得下至少两次续约失败,否则瞬断会让 Master 误读。 */
|
|
57
|
-
const OCCUPANCY_TTL_MS = 60_000;
|
|
58
|
-
const OCCUPANCY_REFRESH_MS = 20_000;
|
|
59
|
-
/** sendMessage 没有 Promise/错误回调;用 agent_start 作为反馈已启动的回执。 */
|
|
60
|
-
const FEEDBACK_START_TIMEOUT_MS = 2_000;
|
|
61
|
-
/** 总体超时:maxRounds 轮 × 每轮 2 倍单进程超时,最低 30 分钟。 */
|
|
62
|
-
function overallTimeoutMs(config: ReviewConfig) {
|
|
63
|
-
return Math.max(
|
|
64
|
-
30 * 60_000,
|
|
65
|
-
config.maxRounds * config.timeoutMinutes * 2 * 60_000,
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function isActive(state: ReviewState) {
|
|
70
|
-
return (
|
|
71
|
-
state.phase === "queued" ||
|
|
72
|
-
state.phase === "reviewing" ||
|
|
73
|
-
state.phase === "needs_fix" ||
|
|
74
|
-
state.phase === "awaiting_fix" ||
|
|
75
|
-
// 总结回合仍属审查生命周期:占用标签持有到总结完成,Master 才不会在
|
|
76
|
-
// 结果卡与总结之间的窗口提前结算、漏掉总结回复。
|
|
77
|
-
state.phase === "summarizing"
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
interface Controller {
|
|
82
|
-
ctx: ExtensionContext;
|
|
83
|
-
config: ReviewConfig;
|
|
84
|
-
state: ReviewState;
|
|
85
|
-
signal: AbortController;
|
|
86
|
-
watchdog: ReturnType<typeof setTimeout> | undefined;
|
|
87
|
-
/** 本 controller 上一次写入的凭证;null=本审查还没写过,undefined=冲突/失败后停写。 */
|
|
88
|
-
persistedStamp: CheckpointStamp | null | undefined;
|
|
89
|
-
/** streaming 时 sendMessage 会变成 steer;展示卡必须等 settled 后再发。 */
|
|
90
|
-
pendingCards: CardData[];
|
|
91
|
-
/** 本运行时已启动的阶段;reload 后新 controller 会重启被中断的审查会话。 */
|
|
92
|
-
runningAction?: string;
|
|
93
|
-
/** 当前审查者/顾问任务;执行模型 agent_start 必须 await 它退出后才能继续。 */
|
|
94
|
-
actionPromise?: Promise<void>;
|
|
95
|
-
actionController?: AbortController;
|
|
96
|
-
/** 反馈 sendMessage 已调用,等待 agent_start 回执。 */
|
|
97
|
-
feedbackStartTimer?: ReturnType<typeof setTimeout>;
|
|
98
|
-
/** 审查接管了编辑器(禁输入 + esc 取消)时的解锁函数,还原成接管前的编辑器。 */
|
|
99
|
-
unlockEditor?: () => void;
|
|
100
|
-
/** Herdr blocked 频道采用计数语义,每个 true 必须由同一 controller 配对 false。 */
|
|
101
|
-
occupancyHeld?: boolean;
|
|
102
|
-
/** 占用标签租约续期计时器;释放与 shutdown 时清除。 */
|
|
103
|
-
occupancyTimer?: ReturnType<typeof setInterval>;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** 一个会话的审查运行时:controller 是当前这场审查,queue 串行化它的状态迁移。 */
|
|
107
|
-
interface ReviewRuntime {
|
|
108
|
-
readonly pi: ExtensionAPI;
|
|
109
|
-
readonly runSession: ReviewSessionRunner;
|
|
110
|
-
controller: Controller | undefined;
|
|
111
|
-
queue: Promise<void>;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
interface ReviewDependencies {
|
|
115
|
-
pool?: InProcessSessionPool;
|
|
116
|
-
runSession?: ReviewSessionRunner;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export interface ReviewHandle {
|
|
120
|
-
/** 等待 event-loop barrier 与其产生的状态迁移排空(测试用)。 */
|
|
121
|
-
settled(): Promise<void>;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function registerReview(
|
|
125
|
-
pi: ExtensionAPI,
|
|
126
|
-
enabled = true,
|
|
127
|
-
configBroken = false,
|
|
128
|
-
dependencies: ReviewDependencies = {},
|
|
129
|
-
): ReviewHandle {
|
|
130
|
-
// 渲染器与开关解耦:关闭 review 后历史卡 reload 仍使用原生结果卡样式。
|
|
131
|
-
registerCardRenderer(pi);
|
|
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
|
-
};
|
|
146
|
-
if (!enabled) {
|
|
147
|
-
// 只有用户明确关闭才封存活动 checkpoint(防重新启用后恢复幽灵审查);
|
|
148
|
-
// features 配置坏掉不是关闭:保留 checkpoint,修好配置重启后继续恢复。
|
|
149
|
-
if (!configBroken) pi.on("session_start", (_event, ctx) => settleDisabledCheckpoint(pi, ctx));
|
|
150
|
-
return handle;
|
|
151
|
-
}
|
|
152
|
-
pi.registerCommand("fire-review", {
|
|
153
|
-
description: "对抗性审查:审这个会话到目前为止做完的事",
|
|
154
|
-
handler: (args, ctx) => handleCommand(rt, args, ctx),
|
|
155
|
-
});
|
|
156
|
-
pi.on("session_start", (_event, ctx) => handleSessionStart(rt, ctx));
|
|
157
|
-
// 宿主保证 resources_discover 在整次 session_start(含所有异步 handler)完成后发出。
|
|
158
|
-
pi.on("resources_discover", (_event, ctx) => requestAdvance(rt, ctx));
|
|
159
|
-
pi.on("agent_start", () => handleAgentStart(rt));
|
|
160
|
-
// agent_end 只记录修复/总结回合的结局,不在此推进审查。
|
|
161
|
-
pi.on("agent_end", (event) => handleAgentEnd(rt, event));
|
|
162
|
-
// settled 后尝试恢复;后续异步 handler 若再触发模型,agent_start 互锁会先停审查。
|
|
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;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function settleDisabledCheckpoint(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
169
|
-
const checkpoint = readCheckpoint(ctx);
|
|
170
|
-
if (!checkpoint || !isActive(checkpoint)) return;
|
|
171
|
-
settleUnavailableCheckpoint(pi, ctx, checkpoint);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function settleUnavailableCheckpoint(
|
|
175
|
-
pi: ExtensionAPI,
|
|
176
|
-
ctx: ExtensionContext,
|
|
177
|
-
checkpoint: ReviewState,
|
|
178
|
-
): void {
|
|
179
|
-
try {
|
|
180
|
-
beginCheckpoint(pi, ctx, {
|
|
181
|
-
...checkpoint,
|
|
182
|
-
phase: "settled",
|
|
183
|
-
active: null,
|
|
184
|
-
pending: null,
|
|
185
|
-
repair: null,
|
|
186
|
-
summary: null,
|
|
187
|
-
updatedAt: Date.now(),
|
|
188
|
-
});
|
|
189
|
-
} catch (error) {
|
|
190
|
-
if (ctx.hasUI)
|
|
191
|
-
ctx.ui.notify(`fire-review 无法收口旧 checkpoint:${errorText(error)}`, "error");
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** 串行化状态迁移:reducer 同步执行,副作用排队;同一时刻只有一个迁移在跑。
|
|
196
|
-
* 返回队尾 Promise,让 pi 的事件处理器(session_shutdown 等)可 await 持久化落盘。 */
|
|
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;
|
|
204
|
-
// 持久化失败不能当成功继续:否则会拿不一致的状态去起会话、投反馈,
|
|
205
|
-
// 重启后又从旧 checkpoint 恢复,重现幽灵审查与重复反馈。
|
|
206
|
-
if (!persist(rt, state)) return;
|
|
207
|
-
if (!isActive(state)) clearWatchdog(rt);
|
|
208
|
-
syncOccupancy(rt, active);
|
|
209
|
-
syncUi(rt);
|
|
210
|
-
}
|
|
211
|
-
await runEffects(rt, effects);
|
|
212
|
-
});
|
|
213
|
-
// 队列一旦 rejected 就再也不会执行后续迁移(连 esc 取消也会失效):
|
|
214
|
-
// 副作用异常只能到此为止,不得杀死状态机。
|
|
215
|
-
rt.queue = run.catch((error) => {
|
|
216
|
-
notifyEffectFailure(rt, error);
|
|
217
|
-
});
|
|
218
|
-
return rt.queue;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function notifyEffectFailure(rt: ReviewRuntime, error: unknown) {
|
|
222
|
-
const active = rt.controller;
|
|
223
|
-
if (!active?.ctx.hasUI) return;
|
|
224
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
225
|
-
active.ctx.ui.notify(
|
|
226
|
-
active.config.language === "en"
|
|
227
|
-
? `fire-review step failed: ${message}`
|
|
228
|
-
: `fire-review 步骤失败:${message}`,
|
|
229
|
-
"warning",
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** 命令与恢复两个入口共用同一判定:任何一个静默回退默认模型都会花真钱跑错模型。 */
|
|
234
|
-
function loadReviewConfig(): Section<ReviewConfig> {
|
|
235
|
-
return loadConfig().review;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function limitsOf(config: ReviewConfig): ReviewLimits {
|
|
239
|
-
return {
|
|
240
|
-
maxRounds: config.maxRounds,
|
|
241
|
-
advisorAfterFailures: config.advisorAfterFailures,
|
|
242
|
-
advisorModel: config.advisor.model,
|
|
243
|
-
language: config.language,
|
|
244
|
-
reviewers: config.reviewers.map((item) => ({
|
|
245
|
-
model: item.model,
|
|
246
|
-
thinking: item.thinking,
|
|
247
|
-
})),
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async function handleCommand(rt: ReviewRuntime, args: string, ctx: ExtensionContext) {
|
|
252
|
-
// 配置解析失败不能让命令无声失败:pi 会捕获 handler 异常,用户只会看到什么都没发生。
|
|
253
|
-
const loaded = loadReviewConfig();
|
|
254
|
-
if ("error" in loaded) {
|
|
255
|
-
if (ctx.hasUI) ctx.ui.notify(loaded.error, "error");
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
const config = loaded.config;
|
|
259
|
-
if (rt.controller && isActive(rt.controller.state)) {
|
|
260
|
-
if (ctx.hasUI) ctx.ui.notify(
|
|
261
|
-
config.language === "en"
|
|
262
|
-
? "A review is already running."
|
|
263
|
-
: "已有审查在进行中。",
|
|
264
|
-
"info",
|
|
265
|
-
);
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
const command = parseCommand(args, config.language);
|
|
269
|
-
if ("error" in command) {
|
|
270
|
-
if (ctx.hasUI) ctx.ui.notify(command.error, "error");
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
rt.controller = {
|
|
274
|
-
ctx,
|
|
275
|
-
config,
|
|
276
|
-
state: initialState(randomUUID()),
|
|
277
|
-
signal: new AbortController(),
|
|
278
|
-
watchdog: undefined,
|
|
279
|
-
persistedStamp: null,
|
|
280
|
-
pendingCards: [],
|
|
281
|
-
};
|
|
282
|
-
armWatchdog(rt);
|
|
283
|
-
// 命令入口也只提出推进请求;真正开审统一经过下一 event-loop 的 idle barrier.
|
|
284
|
-
void dispatch(rt, {
|
|
285
|
-
type: "START",
|
|
286
|
-
focus: command.focus,
|
|
287
|
-
busy: true,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/** 重启 / 会话恢复:从 checkpoint 重建 controller 并续跑未完成的环节。
|
|
292
|
-
* reload / new / resume / fork 是运行时替换(新 pi、新 runtime),旧 runtime 已在
|
|
293
|
-
* session_shutdown 收口;这里只按新会话的 checkpoint 恢复,quit 之外不 settle。 */
|
|
294
|
-
function handleSessionStart(rt: ReviewRuntime, ctx: ExtensionContext): Promise<void> | void {
|
|
295
|
-
const checkpoint = readCheckpoint(ctx);
|
|
296
|
-
if (!checkpoint || !isActive(checkpoint)) return;
|
|
297
|
-
const loaded = loadReviewConfig();
|
|
298
|
-
if ("error" in loaded) {
|
|
299
|
-
// session_start 的配置告警由入口统一聚合;这里只保留活动 checkpoint,修好后继续。
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
const config = loaded.config;
|
|
303
|
-
rt.controller = {
|
|
304
|
-
ctx,
|
|
305
|
-
config,
|
|
306
|
-
state: checkpoint,
|
|
307
|
-
signal: new AbortController(),
|
|
308
|
-
watchdog: undefined,
|
|
309
|
-
persistedStamp: readStamp(ctx),
|
|
310
|
-
pendingCards: [],
|
|
311
|
-
};
|
|
312
|
-
armWatchdog(rt);
|
|
313
|
-
syncOccupancy(rt, rt.controller);
|
|
314
|
-
syncUi(rt);
|
|
315
|
-
// 恢复只更新持久状态并提出推进请求;绝不在 session_start handler 内起任何工作。
|
|
316
|
-
return dispatch(rt, { type: "RECOVER" });
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
async function handleAgentStart(rt: ReviewRuntime): Promise<void> {
|
|
320
|
-
const active = rt.controller;
|
|
321
|
-
if (!active) return;
|
|
322
|
-
if (
|
|
323
|
-
active.state.phase === "awaiting_fix" &&
|
|
324
|
-
active.state.repair?.status === "awaiting_start"
|
|
325
|
-
) {
|
|
326
|
-
clearFeedbackStartTimer(active);
|
|
327
|
-
await dispatch(rt, { type: "REPAIR_STARTED" });
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
if (
|
|
331
|
-
active.state.phase === "summarizing" &&
|
|
332
|
-
active.state.summary?.status === "awaiting_start"
|
|
333
|
-
) {
|
|
334
|
-
clearFeedbackStartTimer(active);
|
|
335
|
-
await dispatch(rt, { type: "SUMMARY_STARTED" });
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
// 其他扩展可在我们排队后异步触发执行模型。agent_start 是宿主提供的硬边界:
|
|
339
|
-
// 宿主会 await 本 handler,因此先 abort 并等所有审查会话真正退出,再允许模型 turn_start。
|
|
340
|
-
if (!active.actionPromise) return;
|
|
341
|
-
active.actionController?.abort();
|
|
342
|
-
await active.actionPromise;
|
|
343
|
-
if (rt.controller !== active || !isActive(active.state)) return;
|
|
344
|
-
active.runningAction = undefined;
|
|
345
|
-
active.actionPromise = undefined;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function handleAgentEnd(rt: ReviewRuntime, event: { messages: readonly unknown[] }): Promise<void> | void {
|
|
349
|
-
const active = rt.controller;
|
|
350
|
-
if (!active) return;
|
|
351
|
-
// 总结回合任何结局都收尾:裁决已落地,总结失败/中断不重试不升级。
|
|
352
|
-
if (active.state.phase === "summarizing" && active.state.summary?.status === "running")
|
|
353
|
-
return dispatch(rt, { type: "SUMMARY_SETTLED" });
|
|
354
|
-
if (
|
|
355
|
-
active.state.phase !== "awaiting_fix" ||
|
|
356
|
-
active.state.repair?.status !== "running"
|
|
357
|
-
) return;
|
|
358
|
-
const assistant = [...event.messages]
|
|
359
|
-
.reverse()
|
|
360
|
-
.find((message) => isRecord(message) && message.role === "assistant");
|
|
361
|
-
if (isRecord(assistant) && assistant.stopReason !== "error" && assistant.stopReason !== "aborted")
|
|
362
|
-
return dispatch(rt, { type: "REPAIR_COMPLETED" });
|
|
363
|
-
active.signal.abort();
|
|
364
|
-
return dispatch(rt, { type: "CANCEL", reason: "user" });
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function requestAdvance(rt: ReviewRuntime, ctx: ExtensionContext): void {
|
|
368
|
-
const active = rt.controller;
|
|
369
|
-
if (!active) return;
|
|
370
|
-
const runId = active.state.runId;
|
|
371
|
-
void advanceWhenIdle(rt, ctx, runId).catch((error) => {
|
|
372
|
-
notifyEffectFailure(rt, error);
|
|
373
|
-
if (rt.controller !== active) return;
|
|
374
|
-
active.signal.abort();
|
|
375
|
-
void dispatch(rt, { type: "CANCEL", reason: "user" });
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
async function advanceWhenIdle(rt: ReviewRuntime, ctx: ExtensionContext, runId: string): Promise<void> {
|
|
380
|
-
const active = rt.controller;
|
|
381
|
-
// 所有启动入口共享 idle 门;正确性另由 agent_start 的同步停审互锁保证。
|
|
382
|
-
if (
|
|
383
|
-
!active ||
|
|
384
|
-
active.state.runId !== runId ||
|
|
385
|
-
active.signal.signal.aborted ||
|
|
386
|
-
!ctx.isIdle() ||
|
|
387
|
-
ctx.hasPendingMessages()
|
|
388
|
-
) return;
|
|
389
|
-
active.ctx = ctx;
|
|
390
|
-
flushPendingCards(rt);
|
|
391
|
-
const { state } = active;
|
|
392
|
-
if (state.phase === "queued") {
|
|
393
|
-
await dispatch(rt, { type: "ADVANCE" });
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
if (state.phase === "reviewing") {
|
|
397
|
-
startAction(rt, active, `review:${state.round}`, "reviewer", () => startReviewers(rt));
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
if (state.phase === "needs_fix") {
|
|
401
|
-
startAction(rt, active, `advisor:${state.round}`, "advisor", () => consultAdvisor(rt));
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
if (state.phase === "summarizing") {
|
|
405
|
-
if (state.summary?.status !== "pending") return;
|
|
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);
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
if (state.phase !== "awaiting_fix" || !state.repair) return;
|
|
413
|
-
if (state.repair.status === "pending") {
|
|
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);
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
if (state.repair.status === "completed") await dispatch(rt, { type: "ADVANCE" });
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function startAction(
|
|
424
|
-
rt: ReviewRuntime,
|
|
425
|
-
active: Controller,
|
|
426
|
-
key: string,
|
|
427
|
-
kind: "reviewer" | "advisor",
|
|
428
|
-
run: () => Promise<void>,
|
|
429
|
-
): void {
|
|
430
|
-
if (active.runningAction === key) return;
|
|
431
|
-
active.runningAction = key;
|
|
432
|
-
active.actionController = new AbortController();
|
|
433
|
-
const action = run()
|
|
434
|
-
.catch(async (error) => {
|
|
435
|
-
if (rt.controller !== active || active.actionController?.signal.aborted) return;
|
|
436
|
-
await dispatch(rt, {
|
|
437
|
-
type: "INFRASTRUCTURE_ERROR",
|
|
438
|
-
details: sessionErrorText(kind, active.config.language, error),
|
|
439
|
-
});
|
|
440
|
-
})
|
|
441
|
-
.finally(() => {
|
|
442
|
-
if (active.actionPromise !== action) return;
|
|
443
|
-
active.actionPromise = undefined;
|
|
444
|
-
active.actionController = undefined;
|
|
445
|
-
active.runningAction = undefined;
|
|
446
|
-
});
|
|
447
|
-
active.actionPromise = action;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/** 会话离开当前运行时:取消并等待审查会话退出,quit 落终态,其余保留 checkpoint 给新运行时恢复。
|
|
451
|
-
* 收口后清空 controller——宿主随后 dispose 会作废 ctx,迟到回调看到空 controller 直接返回。 */
|
|
452
|
-
async function handleShutdown(
|
|
453
|
-
rt: ReviewRuntime,
|
|
454
|
-
reason: "quit" | "reload" | "new" | "resume" | "fork",
|
|
455
|
-
ctx: ExtensionContext,
|
|
456
|
-
): Promise<void> {
|
|
457
|
-
const active = rt.controller;
|
|
458
|
-
if (!active) return;
|
|
459
|
-
// 会话离开当前运行时就立即释放;reload 恢复会由新 controller 重新配对喊占用。
|
|
460
|
-
setOccupancy(rt, active, false);
|
|
461
|
-
// 无论何种终止都先取消并等待当前审查会话;旧动作不得泄漏到新运行时。
|
|
462
|
-
active.signal.abort();
|
|
463
|
-
active.actionController?.abort();
|
|
464
|
-
clearWatchdog(rt);
|
|
465
|
-
clearFeedbackStartTimer(active);
|
|
466
|
-
await active.actionPromise;
|
|
467
|
-
if (active.ctx !== ctx) active.ctx = ctx;
|
|
468
|
-
if (reason === "quit") await dispatch(rt, { type: "CANCEL", reason: "shutdown" });
|
|
469
|
-
else {
|
|
470
|
-
clearUi(active);
|
|
471
|
-
await rt.queue;
|
|
472
|
-
}
|
|
473
|
-
if (rt.controller === active) rt.controller = undefined;
|
|
474
|
-
}
|
|
475
|
-
|
|
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);
|
|
482
|
-
if (remaining === 0) {
|
|
483
|
-
active.signal.abort();
|
|
484
|
-
void dispatch(rt, { type: "TIMEOUT" });
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
active.watchdog = setTimeout(() => {
|
|
488
|
-
if (rt.controller !== active || !isActive(active.state)) return;
|
|
489
|
-
active.signal.abort();
|
|
490
|
-
active.actionController?.abort();
|
|
491
|
-
dispatch(rt, { type: "TIMEOUT" });
|
|
492
|
-
}, remaining);
|
|
493
|
-
active.watchdog.unref?.();
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function clearWatchdog(rt: ReviewRuntime) {
|
|
497
|
-
if (rt.controller?.watchdog) clearTimeout(rt.controller.watchdog);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function clearFeedbackStartTimer(active: Controller): void {
|
|
501
|
-
if (active.feedbackStartTimer) clearTimeout(active.feedbackStartTimer);
|
|
502
|
-
active.feedbackStartTimer = undefined;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// ---- 持久化 ----
|
|
506
|
-
|
|
507
|
-
/** 返回是否已可靠落盘;false 时调用方必须停下本次迁移的副作用。 */
|
|
508
|
-
function persist(rt: ReviewRuntime, state: ReviewState): boolean {
|
|
509
|
-
const active = rt.controller;
|
|
510
|
-
if (!active) return false;
|
|
511
|
-
const persisted = active.persistedStamp;
|
|
512
|
-
if (persisted === undefined) return false; // 冲突或写入失败后停写
|
|
513
|
-
try {
|
|
514
|
-
active.persistedStamp =
|
|
515
|
-
persisted === null
|
|
516
|
-
? beginCheckpoint(rt.pi, active.ctx, state)
|
|
517
|
-
: writeCheckpoint(rt.pi, active.ctx, state, persisted);
|
|
518
|
-
return true;
|
|
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";
|
|
529
|
-
if (error instanceof CheckpointConflictError) {
|
|
530
|
-
// 持久化里出现不是本 controller 写的 Run ID:并发冲突,停止审查。
|
|
531
|
-
halt(en ? "fire-review checkpoint conflict; review stopped." : "fire-review checkpoint 冲突,已停止审查。", "warning");
|
|
532
|
-
void dispatch(rt, { type: "CANCEL", reason: "shutdown" });
|
|
533
|
-
return false;
|
|
534
|
-
}
|
|
535
|
-
// 普通写入失败(如会话落盘异常):停掉本场审查,不带着不一致状态继续跑。
|
|
536
|
-
halt(en
|
|
537
|
-
? `fire-review checkpoint write failed; review stopped: ${errorText(error)}`
|
|
538
|
-
: `fire-review checkpoint 写入失败,已停止审查:${errorText(error)}`, "error");
|
|
539
|
-
clearUi(active);
|
|
540
|
-
// 磁盘上可能还留着上一条活动 checkpoint,重启会把它恢复成幽灵审查:
|
|
541
|
-
// 尽力补写一条终态。写不进去时不假装成功,在通知里告知用户。
|
|
542
|
-
let sealed = true;
|
|
543
|
-
try {
|
|
544
|
-
beginCheckpoint(rt.pi, active.ctx, {
|
|
545
|
-
...state,
|
|
546
|
-
phase: "settled",
|
|
547
|
-
active: null,
|
|
548
|
-
pending: null,
|
|
549
|
-
repair: null,
|
|
550
|
-
summary: null,
|
|
551
|
-
});
|
|
552
|
-
} catch {
|
|
553
|
-
sealed = false;
|
|
554
|
-
}
|
|
555
|
-
// 内存态也必须释放:只停会话但留着活动态 controller,会把幽灵审查从磁盘搬到内存——
|
|
556
|
-
// 后续命令永远被「已有审查在进行中」挡住,且无处取消。
|
|
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"
|
|
563
|
-
? "fire-review could not seal the checkpoint; a restart may resume this review — cancel it with esc."
|
|
564
|
-
: "fire-review 无法写入终态,重启后可能恢复这场审查,到时按 esc 取消。",
|
|
565
|
-
"warning",
|
|
566
|
-
);
|
|
567
|
-
return false;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
function errorText(error: unknown) {
|
|
572
|
-
return error instanceof Error ? error.message : String(error);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function syncOccupancy(rt: ReviewRuntime, active: Controller): void {
|
|
576
|
-
setOccupancy(rt, active, isActive(active.state));
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* 标签租约:持有期带 TTL 定时续约,释放时清除失败重试一次、再失败由 TTL 到期兜底。
|
|
581
|
-
* 定时续约是租约业务语义:herdr 没有“进程退出即清 metadata”的接口(源码核实),
|
|
582
|
-
* crash/kill 后无 TTL 的标签永驻会让 Master 把 Worker 的真提问误判为审查占用;
|
|
583
|
-
* 续约同时充当首次投递失败的重试。
|
|
584
|
-
*/
|
|
585
|
-
function publishOccupancyLabel(rt: ReviewRuntime, active: Controller, held: boolean): void {
|
|
586
|
-
if (held) {
|
|
587
|
-
void sendOccupancyLabel();
|
|
588
|
-
if (!active.occupancyTimer) {
|
|
589
|
-
active.occupancyTimer = setInterval(() => void sendOccupancyLabel(), OCCUPANCY_REFRESH_MS);
|
|
590
|
-
active.occupancyTimer.unref?.();
|
|
591
|
-
}
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
if (active.occupancyTimer) {
|
|
595
|
-
clearInterval(active.occupancyTimer);
|
|
596
|
-
active.occupancyTimer = undefined;
|
|
597
|
-
}
|
|
598
|
-
void clearOccupancyLabel(rt);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function sendOccupancyLabel(): Promise<boolean> {
|
|
602
|
-
const env = herdrPaneEnv();
|
|
603
|
-
if (!env) return Promise.resolve(false);
|
|
604
|
-
return herdrRequest(OCCUPANCY_SOURCE, "pane.report_metadata", {
|
|
605
|
-
pane_id: env.paneId,
|
|
606
|
-
source: OCCUPANCY_SOURCE,
|
|
607
|
-
state_labels: { blocked: OCCUPANCY_LABEL },
|
|
608
|
-
ttl_ms: OCCUPANCY_TTL_MS,
|
|
609
|
-
seq: (occupancySeq += 1),
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
async function clearOccupancyLabel(rt: ReviewRuntime): Promise<void> {
|
|
614
|
-
const env = herdrPaneEnv();
|
|
615
|
-
if (!env) return;
|
|
616
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
617
|
-
// 新审查已重新持有时中止重试:迟到的清除会撤掉新租约的标签。
|
|
618
|
-
if (rt.controller?.occupancyHeld) return;
|
|
619
|
-
const delivered = await herdrRequest(OCCUPANCY_SOURCE, "pane.report_metadata", {
|
|
620
|
-
pane_id: env.paneId,
|
|
621
|
-
source: OCCUPANCY_SOURCE,
|
|
622
|
-
clear_state_labels: true,
|
|
623
|
-
seq: (occupancySeq += 1),
|
|
624
|
-
});
|
|
625
|
-
if (delivered) return;
|
|
626
|
-
}
|
|
627
|
-
// 两次未送达:标签带 TTL,最迟 60s 自行过期,不会永久残留。
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function setOccupancy(rt: ReviewRuntime, active: Controller, held: boolean): void {
|
|
631
|
-
if (Boolean(active.occupancyHeld) === held) return;
|
|
632
|
-
active.occupancyHeld = held;
|
|
633
|
-
// 标签走 metadata state_labels:herdr 会丢弃 report_agent 的 message(实测),
|
|
634
|
-
// 只有这条通道能同时到达 Master(state_labels 判定)与侧边栏(state_text token)。
|
|
635
|
-
// 频道仍要发:它驱动 herdr 集成的 blocked 状态本身。占用信号不伤审查。
|
|
636
|
-
publishOccupancyLabel(rt, active, held);
|
|
637
|
-
try {
|
|
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);
|
|
642
|
-
} catch (error) {
|
|
643
|
-
// 占用信号只对齐 Herdr 展示;集成故障不能改变审查状态机或会话生命周期。
|
|
644
|
-
try {
|
|
645
|
-
if (active.ctx.hasUI)
|
|
646
|
-
active.ctx.ui.notify(`fire-review 无法同步 Herdr 占用状态:${errorText(error)}`, "warning");
|
|
647
|
-
} catch {
|
|
648
|
-
// 通知本身同样只是展示,不能反向打断审查。
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
/** UI 投影:终端标题 + esc 接管,全部从当前状态派生;审查进度经占用频道由输入框外壳显示。 */
|
|
654
|
-
function syncUi(rt: ReviewRuntime): void {
|
|
655
|
-
const active = rt.controller;
|
|
656
|
-
if (!active) return;
|
|
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);
|
|
666
|
-
releaseEditor(active);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
function releaseEditor(active: Controller) {
|
|
670
|
-
active.unlockEditor?.();
|
|
671
|
-
active.unlockEditor = undefined;
|
|
672
|
-
}
|
|
673
|
-
|
|
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 };
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function canCancelWithKey(rt: ReviewRuntime) {
|
|
689
|
-
const phase = rt.controller?.state.phase;
|
|
690
|
-
return phase === "queued" || phase === "reviewing" || phase === "needs_fix";
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function cancelByUser(rt: ReviewRuntime) {
|
|
694
|
-
const active = rt.controller;
|
|
695
|
-
if (!active) return;
|
|
696
|
-
if (active.state.phase === "needs_fix") {
|
|
697
|
-
// pi-flow 语义:顾问阶段的 Esc 只跳过本次咨询,不取消整场审查。
|
|
698
|
-
active.actionController?.abort();
|
|
699
|
-
void (active.actionPromise ?? Promise.resolve()).then(() =>
|
|
700
|
-
dispatch(rt, { type: "ADVISOR_SKIPPED" }),
|
|
701
|
-
);
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
active.signal.abort();
|
|
705
|
-
active.actionController?.abort();
|
|
706
|
-
void dispatch(rt, { type: "CANCEL", reason: "user" });
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// ---- 副作用执行器 ----
|
|
710
|
-
|
|
711
|
-
/** reducer 只发卡或请求推进;所有会启动工作的动作统一经过 idle barrier。 */
|
|
712
|
-
async function runEffects(rt: ReviewRuntime, effects: ReviewEffect[]) {
|
|
713
|
-
for (const effect of effects) {
|
|
714
|
-
const active = rt.controller;
|
|
715
|
-
if (!active) return;
|
|
716
|
-
if (effect.kind === "advance") {
|
|
717
|
-
requestAdvance(rt, active.ctx);
|
|
718
|
-
continue;
|
|
719
|
-
}
|
|
720
|
-
try {
|
|
721
|
-
sendCard(rt, effect.card);
|
|
722
|
-
} catch (error) {
|
|
723
|
-
notifyEffectFailure(rt, error);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
function reviewerModelConfig(model: ReviewConfig["advisor"], config: ReviewConfig): ReviewModelConfig {
|
|
730
|
-
return {
|
|
731
|
-
model: model.model,
|
|
732
|
-
thinking: model.thinking,
|
|
733
|
-
tools: config.tools,
|
|
734
|
-
timeoutMs: config.timeoutMinutes * 60_000,
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
/** 开审那一刻取会话分支快照构造 prompt,所有审查者共用同一 prompt。 */
|
|
739
|
-
async function startReviewers(rt: ReviewRuntime): Promise<void> {
|
|
740
|
-
const active = rt.controller;
|
|
741
|
-
if (!active) return;
|
|
742
|
-
const { state, config } = active;
|
|
743
|
-
if (!state.active) return;
|
|
744
|
-
const currentActive = state.active;
|
|
745
|
-
const actionSignal = active.actionController?.signal ?? active.signal.signal;
|
|
746
|
-
const evidence = buildEvidence(sessionEntries(rt), config.language, { sessionFile: active.ctx.sessionManager.getSessionFile() });
|
|
747
|
-
const prompt = buildReviewPrompt(readPrompt("review", config.language), {
|
|
748
|
-
language: config.language,
|
|
749
|
-
scope: scopeText(config.language),
|
|
750
|
-
focus: state.focus,
|
|
751
|
-
evidence: evidence.text,
|
|
752
|
-
history: state.history,
|
|
753
|
-
round: state.round,
|
|
754
|
-
});
|
|
755
|
-
const tasks = currentActive.reviewers
|
|
756
|
-
.filter((reviewer) => reviewer.status === "running")
|
|
757
|
-
.map(async (reviewer) => {
|
|
758
|
-
try {
|
|
759
|
-
const result = await runReviewer({
|
|
760
|
-
index: reviewer.index,
|
|
761
|
-
config: reviewerModelConfig(reviewer, config),
|
|
762
|
-
prompt,
|
|
763
|
-
cwd: active.ctx.cwd,
|
|
764
|
-
language: config.language,
|
|
765
|
-
signal: actionSignal,
|
|
766
|
-
runSession: rt.runSession,
|
|
767
|
-
});
|
|
768
|
-
if (!actionSignal.aborted)
|
|
769
|
-
await dispatch(rt, { type: "REVIEWER_SETTLED", index: result.index, result });
|
|
770
|
-
} catch (error) {
|
|
771
|
-
if (actionSignal.aborted) return;
|
|
772
|
-
await dispatch(rt, {
|
|
773
|
-
type: "REVIEWER_SETTLED",
|
|
774
|
-
index: reviewer.index,
|
|
775
|
-
result: {
|
|
776
|
-
index: reviewer.index,
|
|
777
|
-
model: reviewer.model,
|
|
778
|
-
thinking: reviewer.thinking,
|
|
779
|
-
status: "error",
|
|
780
|
-
summary: "",
|
|
781
|
-
details: sessionErrorText("reviewer", active.config.language, error),
|
|
782
|
-
},
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
});
|
|
786
|
-
await Promise.all(tasks);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
async function consultAdvisor(rt: ReviewRuntime): Promise<void> {
|
|
790
|
-
const active = rt.controller;
|
|
791
|
-
if (!active) return;
|
|
792
|
-
const { state, config } = active;
|
|
793
|
-
if (!state.pending) return;
|
|
794
|
-
const pending = state.pending;
|
|
795
|
-
const actionSignal = active.actionController?.signal ?? active.signal.signal;
|
|
796
|
-
const prompt = buildAdvisorPrompt(readPrompt("advisor", config.language), {
|
|
797
|
-
language: config.language,
|
|
798
|
-
focus: state.focus,
|
|
799
|
-
details: pending.details,
|
|
800
|
-
history: state.history,
|
|
801
|
-
round: pending.round,
|
|
802
|
-
});
|
|
803
|
-
try {
|
|
804
|
-
const result = await runAdvisor({
|
|
805
|
-
config: reviewerModelConfig(config.advisor, config),
|
|
806
|
-
prompt,
|
|
807
|
-
cwd: active.ctx.cwd,
|
|
808
|
-
language: config.language,
|
|
809
|
-
signal: actionSignal,
|
|
810
|
-
runSession: rt.runSession,
|
|
811
|
-
});
|
|
812
|
-
if (!actionSignal.aborted)
|
|
813
|
-
await dispatch(rt, { type: "ADVISOR_SETTLED", result });
|
|
814
|
-
} catch (error) {
|
|
815
|
-
if (actionSignal.aborted) return;
|
|
816
|
-
await dispatch(rt, {
|
|
817
|
-
type: "INFRASTRUCTURE_ERROR",
|
|
818
|
-
details: sessionErrorText("advisor", active.config.language, error),
|
|
819
|
-
});
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function sessionErrorText(kind: "reviewer" | "advisor", language: Language, error: unknown) {
|
|
824
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
825
|
-
if (kind === "reviewer")
|
|
826
|
-
return language === "en" ? `reviewer session error: ${message}` : `审查会话异常:${message}`;
|
|
827
|
-
return language === "en" ? `advisor session error: ${message}` : `顾问会话异常:${message}`;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function deliverFeedbackNow(rt: ReviewRuntime, details: string, advisor: AdvisorResult | null) {
|
|
831
|
-
const active = rt.controller;
|
|
832
|
-
if (!active) return;
|
|
833
|
-
const feedback = buildFixFeedback({
|
|
834
|
-
language: active.config.language,
|
|
835
|
-
details,
|
|
836
|
-
advisor,
|
|
837
|
-
});
|
|
838
|
-
clearFeedbackStartTimer(active);
|
|
839
|
-
// API 返回 void,真实异步失败不会进 try/catch;持久化状态等待 agent_start 回执。
|
|
840
|
-
active.feedbackStartTimer = setTimeout(() => {
|
|
841
|
-
if (
|
|
842
|
-
rt.controller !== active ||
|
|
843
|
-
active.state.phase !== "awaiting_fix" ||
|
|
844
|
-
active.state.repair?.status !== "awaiting_start"
|
|
845
|
-
) return;
|
|
846
|
-
active.feedbackStartTimer = undefined;
|
|
847
|
-
if (active.ctx.hasUI)
|
|
848
|
-
active.ctx.ui.notify(
|
|
849
|
-
active.config.language === "en"
|
|
850
|
-
? "fire-review feedback did not start a repair turn; review stopped."
|
|
851
|
-
: "fire-review 修复反馈未能启动回合,审查已停止。",
|
|
852
|
-
"error",
|
|
853
|
-
);
|
|
854
|
-
active.signal.abort();
|
|
855
|
-
void dispatch(rt, { type: "CANCEL", reason: "user" });
|
|
856
|
-
}, FEEDBACK_START_TIMEOUT_MS);
|
|
857
|
-
active.feedbackStartTimer.unref?.();
|
|
858
|
-
// display:false 的消息进 LLM 上下文但不渲染;triggerTurn 让执行模型开始修复回合。
|
|
859
|
-
try {
|
|
860
|
-
rt.pi.sendMessage(
|
|
861
|
-
{ customType: FEEDBACK_TYPE, content: feedback, display: false },
|
|
862
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
863
|
-
);
|
|
864
|
-
} catch (error) {
|
|
865
|
-
clearFeedbackStartTimer(active);
|
|
866
|
-
throw error;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
/** 总结提示投递:与修复反馈同一套 agent_start 回执机制;失败不升级,静默收尾。 */
|
|
871
|
-
function deliverSummaryNow(rt: ReviewRuntime, state: ReviewState): void {
|
|
872
|
-
const active = rt.controller;
|
|
873
|
-
if (!active || !state.summary) return;
|
|
874
|
-
const last = state.history.at(-1);
|
|
875
|
-
const material = state.summary.kind === "advisor_stop"
|
|
876
|
-
? last?.advisor?.advice ?? last?.details ?? ""
|
|
877
|
-
: last?.details ?? "";
|
|
878
|
-
const prompt = buildSummaryPrompt({
|
|
879
|
-
language: active.config.language,
|
|
880
|
-
kind: state.summary.kind,
|
|
881
|
-
rounds: state.history.length,
|
|
882
|
-
material,
|
|
883
|
-
});
|
|
884
|
-
clearFeedbackStartTimer(active);
|
|
885
|
-
active.feedbackStartTimer = setTimeout(() => {
|
|
886
|
-
if (
|
|
887
|
-
rt.controller !== active ||
|
|
888
|
-
active.state.phase !== "summarizing" ||
|
|
889
|
-
active.state.summary?.status !== "awaiting_start"
|
|
890
|
-
) return;
|
|
891
|
-
active.feedbackStartTimer = undefined;
|
|
892
|
-
// 总结是尽力而非必须:未能启动回合就静默收尾,裁决与结果卡已落地。
|
|
893
|
-
if (active.ctx.hasUI)
|
|
894
|
-
active.ctx.ui.notify(
|
|
895
|
-
active.config.language === "en"
|
|
896
|
-
? "fire-review summary turn did not start; finishing without it."
|
|
897
|
-
: "fire-review 总结回合未能启动,已直接收尾。",
|
|
898
|
-
"warning",
|
|
899
|
-
);
|
|
900
|
-
void dispatch(rt, { type: "SUMMARY_SETTLED" });
|
|
901
|
-
}, FEEDBACK_START_TIMEOUT_MS);
|
|
902
|
-
active.feedbackStartTimer.unref?.();
|
|
903
|
-
try {
|
|
904
|
-
rt.pi.sendMessage(
|
|
905
|
-
{ customType: SUMMARY_REQUEST_TYPE, content: prompt, display: false },
|
|
906
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
907
|
-
);
|
|
908
|
-
} catch (error) {
|
|
909
|
-
clearFeedbackStartTimer(active);
|
|
910
|
-
notifyEffectFailure(rt, error);
|
|
911
|
-
void dispatch(rt, { type: "SUMMARY_SETTLED" });
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
function sendCard(rt: ReviewRuntime, card: CardData) {
|
|
916
|
-
const active = rt.controller;
|
|
917
|
-
if (!active) return;
|
|
918
|
-
// pi-flow 的用户取消是即时临时通知,不进会话;shutdown 静默收口。
|
|
919
|
-
if (card.kind === "cancel") {
|
|
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已按你的操作停止",
|
|
925
|
-
"info",
|
|
926
|
-
);
|
|
927
|
-
return;
|
|
928
|
-
}
|
|
929
|
-
// 宿主在 streaming 时会把无 options 的 sendMessage 当 steer 塞进当前模型回合。
|
|
930
|
-
// 卡片只是 UI 投影,绝不能因此唤醒或打断执行模型。
|
|
931
|
-
if (!active.ctx.isIdle()) {
|
|
932
|
-
active.pendingCards.push(card);
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
sendCardNow(rt, active, card);
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function flushPendingCards(rt: ReviewRuntime): void {
|
|
939
|
-
const active = rt.controller;
|
|
940
|
-
if (!active || !active.ctx.isIdle() || active.pendingCards.length === 0) return;
|
|
941
|
-
const cards = active.pendingCards.splice(0);
|
|
942
|
-
for (const card of cards) {
|
|
943
|
-
try {
|
|
944
|
-
sendCardNow(rt, active, card);
|
|
945
|
-
} catch (error) {
|
|
946
|
-
notifyEffectFailure(rt, error);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
function sendCardNow(rt: ReviewRuntime, active: Controller, card: CardData): void {
|
|
952
|
-
const built = buildCard(card, active.config.language);
|
|
953
|
-
rt.pi.sendMessage({
|
|
954
|
-
customType: CARD_TYPE,
|
|
955
|
-
content: wrapEnvelope("firecode_review", built.content),
|
|
956
|
-
display: true,
|
|
957
|
-
details: built.details,
|
|
958
|
-
});
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
function parseCommand(args: string, language: Language): { focus: string } | { error: string } {
|
|
962
|
-
const input = args.trim();
|
|
963
|
-
return input.startsWith("--")
|
|
964
|
-
? { error: language === "en" ? "Invalid fire-review arguments." : "fire-review 参数无效" }
|
|
965
|
-
: { focus: input };
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
/** 会话分支 entries(供证据组装);本插件的卡与反馈消息不参与证据,避免自指。 */
|
|
969
|
-
function sessionEntries(rt: ReviewRuntime) {
|
|
970
|
-
const manager = rt.controller?.ctx.sessionManager as
|
|
971
|
-
| { getBranch?: () => unknown[] }
|
|
972
|
-
| undefined;
|
|
973
|
-
const entries = manager?.getBranch?.() ?? [];
|
|
974
|
-
return entries.filter(
|
|
975
|
-
(entry) =>
|
|
976
|
-
!isRecord(entry) ||
|
|
977
|
-
entry.type !== "custom_message" ||
|
|
978
|
-
(entry.customType !== CARD_TYPE && entry.customType !== FEEDBACK_TYPE &&
|
|
979
|
-
entry.customType !== SUMMARY_REQUEST_TYPE),
|
|
980
|
-
);
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
function scopeText(language: Language) {
|
|
984
|
-
return language === "en"
|
|
985
|
-
? "Delivery quality of the current task in this conversation. The first user message is the original-request anchor; later user messages may override, narrow, or correct it."
|
|
986
|
-
: "当前会话当前任务的交付质量。首条用户消息是原始需求锚点;后续用户消息可能覆盖、缩小或修正,以后者为准。";
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
990
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
991
|
-
}
|
|
992
|
-
|