pi-firecode 0.8.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/LICENSE +21 -0
- package/README.md +40 -0
- package/config.example.jsonc +100 -0
- package/config.ts +474 -0
- package/deliver.ts +32 -0
- package/flame-frames.ts +460 -0
- package/format.ts +80 -0
- package/header.ts +92 -0
- package/herdr-client.ts +60 -0
- package/index.ts +62 -0
- package/jsonc.ts +34 -0
- package/master/event-card.ts +106 -0
- package/master/event-format.ts +52 -0
- package/master/index.ts +1200 -0
- package/master/prompt.ts +26 -0
- package/master/prompts/master.zh.md +17 -0
- package/master/prompts/worker.zh.md +1 -0
- package/master/role.ts +13 -0
- package/master/spawn.ts +193 -0
- package/master/state.ts +201 -0
- package/package.json +59 -0
- package/provider/claude-sub.ts +129 -0
- package/provider/openai-native/index.ts +8 -0
- package/provider/openai-native/src/compact-client.ts +362 -0
- package/provider/openai-native/src/config.ts +297 -0
- package/provider/openai-native/src/extension.ts +93 -0
- package/provider/openai-native/src/native-compaction.ts +151 -0
- package/provider/openai-native/src/native-details.ts +157 -0
- package/provider/openai-native/src/native-replay.ts +253 -0
- package/provider/openai-native/src/native-runtime.ts +144 -0
- package/provider/openai-native/src/options.ts +85 -0
- package/provider/openai-native/src/request-pipeline.ts +21 -0
- package/provider/openai-native/src/responses-input.ts +433 -0
- package/review/advisor.ts +112 -0
- package/review/card.ts +385 -0
- package/review/checkpoint.ts +384 -0
- package/review/evidence.ts +232 -0
- package/review/index.ts +1246 -0
- package/review/outcome.ts +74 -0
- package/review/progress.ts +324 -0
- package/review/prompt.ts +210 -0
- package/review/prompts/advisor.en.md +41 -0
- package/review/prompts/advisor.zh.md +42 -0
- package/review/prompts/review.en.md +75 -0
- package/review/prompts/review.zh.md +75 -0
- package/review/reviewer.ts +356 -0
- package/review/session.ts +129 -0
- package/review/state.ts +905 -0
- package/review/ui.ts +528 -0
- package/session/bark.ts +157 -0
- package/session/herdr-display.ts +87 -0
- package/session/presets.ts +257 -0
- package/session/rename.ts +46 -0
- package/session/stats.ts +310 -0
- package/session/working-flame.ts +116 -0
- package/statusbar/index.ts +118 -0
- package/statusbar/quota-cache.ts +60 -0
- package/statusbar/quota-parse.ts +85 -0
- package/statusbar/quota.ts +203 -0
- package/statusbar/render.ts +181 -0
- package/statusbar/tps.ts +87 -0
- package/theme.ts +84 -0
- package/tools/grouping.ts +233 -0
- package/tools/index.ts +183 -0
- package/tools/line.ts +194 -0
- package/tools/parts.ts +143 -0
- package/tools/timing.ts +22 -0
- package/watcher/card.ts +85 -0
- package/watcher/index.ts +204 -0
- package/watcher/observer.ts +75 -0
- package/watcher/prompts/watch.zh.md +46 -0
- package/watcher/transcript.ts +64 -0
package/review/index.ts
ADDED
|
@@ -0,0 +1,1246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /fire-review:对抗性审查插件的执行器与入口。
|
|
3
|
+
*
|
|
4
|
+
* 职责分界:
|
|
5
|
+
* - 领域状态只活在纯 reducer(state.ts)里,所有迁移经 reduce() 计算;
|
|
6
|
+
* 本文件是唯一执行器,只做副作用(起审查会话、投递反馈、发卡、持久化、状态栏),
|
|
7
|
+
* 会话结果一律回灌成事件交给 reducer。模块级只有一个 controller。
|
|
8
|
+
* - 渲染器在此顶层无条件注册(不懒加载),live 与 reload 外观一致。
|
|
9
|
+
*/
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
12
|
+
import {
|
|
13
|
+
getAgentDir,
|
|
14
|
+
ModelRuntime,
|
|
15
|
+
type ExtensionAPI,
|
|
16
|
+
type ExtensionContext,
|
|
17
|
+
} from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { loadConfig, type Language, type ReviewConfig } from "../config.js";
|
|
19
|
+
import { formatDuration } from "../format.js";
|
|
20
|
+
import { herdrPaneEnv, herdrRequest } from "../herdr-client.js";
|
|
21
|
+
import { InProcessSessionPool } from "../master/spawn.js";
|
|
22
|
+
import { buildCard, CARD_TYPE, decisionText, registerCardRenderer } from "./card.js";
|
|
23
|
+
import {
|
|
24
|
+
beginCheckpoint,
|
|
25
|
+
CHECKPOINT_TYPE,
|
|
26
|
+
CheckpointConflictError,
|
|
27
|
+
type CheckpointStamp,
|
|
28
|
+
readCheckpoint,
|
|
29
|
+
readStamp,
|
|
30
|
+
writeCheckpoint,
|
|
31
|
+
} from "./checkpoint.js";
|
|
32
|
+
import { buildEvidence } from "./evidence.js";
|
|
33
|
+
import {
|
|
34
|
+
applySessionEvent,
|
|
35
|
+
initialProgress,
|
|
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";
|
|
48
|
+
import { runAdvisor } from "./advisor.js";
|
|
49
|
+
import { runReviewer, type ReviewModelConfig } from "./reviewer.js";
|
|
50
|
+
import { createReviewSessionRunner, type ReviewSessionRunner } from "./session.js";
|
|
51
|
+
import {
|
|
52
|
+
type AdvisorResult,
|
|
53
|
+
type CardData,
|
|
54
|
+
type ReviewEffect,
|
|
55
|
+
type ReviewEvent,
|
|
56
|
+
type ReviewLimits,
|
|
57
|
+
type ReviewState,
|
|
58
|
+
initialState,
|
|
59
|
+
reduce,
|
|
60
|
+
} from "./state.js";
|
|
61
|
+
|
|
62
|
+
export const FEEDBACK_TYPE = "firecode-review-feedback";
|
|
63
|
+
/** 总结回合提示:与修复反馈同通道(进上下文不渲染),不参与证据自指。 */
|
|
64
|
+
export const SUMMARY_REQUEST_TYPE = "firecode-review-summary";
|
|
65
|
+
const STATUS_KEY = "fire-review";
|
|
66
|
+
const OCCUPANCY_CHANNEL = "herdr:blocked";
|
|
67
|
+
const OCCUPANCY_SOURCE = "firecode-review";
|
|
68
|
+
/** herdr 按 seq 丢弃过期上报;同一 source 单调递增。 */
|
|
69
|
+
let occupancySeq = Date.now() * 1000;
|
|
70
|
+
/** 标签租约:TTL 要容得下至少两次续约失败,否则瞬断会让 Master 误读。 */
|
|
71
|
+
const OCCUPANCY_TTL_MS = 60_000;
|
|
72
|
+
const OCCUPANCY_REFRESH_MS = 20_000;
|
|
73
|
+
/** sendMessage 没有 Promise/错误回调;用 agent_start 作为反馈已启动的回执。 */
|
|
74
|
+
const FEEDBACK_START_TIMEOUT_MS = 2_000;
|
|
75
|
+
/** 总体超时:maxRounds 轮 × 每轮 2 倍单进程超时,最低 30 分钟。 */
|
|
76
|
+
function overallTimeoutMs(config: ReviewConfig) {
|
|
77
|
+
return Math.max(
|
|
78
|
+
30 * 60_000,
|
|
79
|
+
config.maxRounds * config.timeoutMinutes * 2 * 60_000,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isActive(state: ReviewState) {
|
|
84
|
+
return (
|
|
85
|
+
state.phase === "queued" ||
|
|
86
|
+
state.phase === "reviewing" ||
|
|
87
|
+
state.phase === "needs_fix" ||
|
|
88
|
+
state.phase === "awaiting_fix" ||
|
|
89
|
+
// 总结回合仍属审查生命周期:占用标签持有到总结完成,Master 才不会在
|
|
90
|
+
// 结果卡与总结之间的窗口提前结算、漏掉总结回复。
|
|
91
|
+
state.phase === "summarizing"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface Controller {
|
|
96
|
+
pi: ExtensionAPI;
|
|
97
|
+
ctx: ExtensionContext;
|
|
98
|
+
config: ReviewConfig;
|
|
99
|
+
state: ReviewState;
|
|
100
|
+
signal: AbortController;
|
|
101
|
+
watchdog: ReturnType<typeof setTimeout> | undefined;
|
|
102
|
+
statusTimer?: ReturnType<typeof setInterval>;
|
|
103
|
+
/** 本 controller 上一次写入的凭证;null=本审查还没写过,undefined=冲突/失败后停写。 */
|
|
104
|
+
persistedStamp: CheckpointStamp | null | undefined;
|
|
105
|
+
/** streaming 时 sendMessage 会变成 steer;展示卡必须等 settled 后再发。 */
|
|
106
|
+
pendingCards: CardData[];
|
|
107
|
+
/** 本运行时已启动的阶段;reload 后新 controller 会重启被中断的审查会话。 */
|
|
108
|
+
runningAction?: string;
|
|
109
|
+
/** 当前审查者/顾问任务;执行模型 agent_start 必须 await 它退出后才能继续。 */
|
|
110
|
+
actionPromise?: Promise<void>;
|
|
111
|
+
runSession: ReviewSessionRunner;
|
|
112
|
+
actionController?: AbortController;
|
|
113
|
+
/** 反馈 sendMessage 已调用,等待 agent_start 回执。 */
|
|
114
|
+
feedbackStartTimer?: ReturnType<typeof setTimeout>;
|
|
115
|
+
/** 审查会话实时进度:纯 UI 态,高频更新,不入 checkpoint。 */
|
|
116
|
+
progress: readonly ReviewerProgress[];
|
|
117
|
+
/** 当前 progress 属于审查者还是顾问:修复相据此决定是否展示裁决摘要。 */
|
|
118
|
+
progressKind?: "reviewers" | "advisor";
|
|
119
|
+
progressStartedAt?: number;
|
|
120
|
+
/** 编辑器是否已被审查接管(禁输入 + esc 取消)。 */
|
|
121
|
+
editorLocked?: boolean;
|
|
122
|
+
/** Herdr blocked 频道采用计数语义,每个 true 必须由同一 controller 配对 false。 */
|
|
123
|
+
occupancyHeld?: boolean;
|
|
124
|
+
/** 占用标签租约续期计时器;释放与 shutdown 时清除。 */
|
|
125
|
+
occupancyTimer?: ReturnType<typeof setInterval>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let controller: Controller | undefined;
|
|
129
|
+
let dispatchQueue: Promise<void> = Promise.resolve();
|
|
130
|
+
|
|
131
|
+
interface ReviewDependencies {
|
|
132
|
+
pool?: InProcessSessionPool;
|
|
133
|
+
resolveModel?: (id: string) => Promise<Model<any>>;
|
|
134
|
+
runSession?: ReviewSessionRunner;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function registerReview(
|
|
138
|
+
pi: ExtensionAPI,
|
|
139
|
+
enabled = true,
|
|
140
|
+
configBroken = false,
|
|
141
|
+
dependencies: ReviewDependencies = {},
|
|
142
|
+
): void {
|
|
143
|
+
// 渲染器与开关解耦:关闭 review 后历史卡 reload 仍使用原生结果卡样式。
|
|
144
|
+
registerCardRenderer(pi);
|
|
145
|
+
const runSession = dependencies.runSession ?? createReviewSessionRunner(
|
|
146
|
+
dependencies.pool ?? new InProcessSessionPool(),
|
|
147
|
+
dependencies.resolveModel ?? resolveConfiguredModel,
|
|
148
|
+
);
|
|
149
|
+
if (!enabled) {
|
|
150
|
+
// 只有用户明确关闭才封存活动 checkpoint(防重新启用后恢复幽灵审查);
|
|
151
|
+
// features 配置坏掉不是关闭:保留 checkpoint,修好配置重启后继续恢复。
|
|
152
|
+
if (!configBroken) pi.on("session_start", (_event, ctx) => settleDisabledCheckpoint(pi, ctx));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
pi.registerCommand("fire-review", {
|
|
156
|
+
description: "对抗性审查:审这个会话到目前为止做完的事",
|
|
157
|
+
handler: (args, ctx) => handleCommand(pi, args, ctx, runSession),
|
|
158
|
+
});
|
|
159
|
+
pi.on("session_start", (_event, ctx) => handleSessionStart(pi, ctx, runSession));
|
|
160
|
+
// 宿主保证 resources_discover 在整次 session_start(含所有异步 handler)完成后发出。
|
|
161
|
+
pi.on("resources_discover", (_event, ctx) => requestAdvance(pi, ctx));
|
|
162
|
+
pi.on("agent_start", () => handleAgentStart(pi));
|
|
163
|
+
// agent_end 只记录修复/总结回合的结局,不在此推进审查。
|
|
164
|
+
pi.on("agent_end", (event) => handleAgentEnd(pi, event));
|
|
165
|
+
// settled 后尝试恢复;后续异步 handler 若再触发模型,agent_start 互锁会先停审查。
|
|
166
|
+
pi.on("agent_settled", (_event, ctx) => scheduleAfterAgentSettled(pi, ctx));
|
|
167
|
+
pi.on("session_shutdown", (event, ctx) => handleShutdown(pi, event.reason, ctx));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function settleDisabledCheckpoint(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
171
|
+
const checkpoint = readCheckpoint(ctx);
|
|
172
|
+
if (!checkpoint || !isActive(checkpoint)) return;
|
|
173
|
+
settleUnavailableCheckpoint(pi, ctx, checkpoint);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function settleUnavailableCheckpoint(
|
|
177
|
+
pi: ExtensionAPI,
|
|
178
|
+
ctx: ExtensionContext,
|
|
179
|
+
checkpoint: ReviewState,
|
|
180
|
+
): void {
|
|
181
|
+
try {
|
|
182
|
+
beginCheckpoint(pi, ctx, {
|
|
183
|
+
...checkpoint,
|
|
184
|
+
phase: "settled",
|
|
185
|
+
active: null,
|
|
186
|
+
pending: null,
|
|
187
|
+
repair: null,
|
|
188
|
+
summary: null,
|
|
189
|
+
updatedAt: Date.now(),
|
|
190
|
+
});
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (ctx.hasUI)
|
|
193
|
+
ctx.ui.notify(`fire-review 无法收口旧 checkpoint:${errorText(error)}`, "error");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 串行化状态迁移:reducer 同步执行,副作用排队;同一时刻只有一个迁移在跑。
|
|
198
|
+
* 返回队尾 Promise,让 pi 的事件处理器(session_shutdown 等)可 await 持久化落盘。 */
|
|
199
|
+
function dispatch(pi: ExtensionAPI, event: ReviewEvent): Promise<void> {
|
|
200
|
+
const run = dispatchQueue.then(async () => {
|
|
201
|
+
if (!controller || controller.pi !== pi) return;
|
|
202
|
+
const { state, effects } = reduce(
|
|
203
|
+
controller.state,
|
|
204
|
+
event,
|
|
205
|
+
limitsOf(controller.config),
|
|
206
|
+
Date.now(),
|
|
207
|
+
);
|
|
208
|
+
if (state !== controller.state) {
|
|
209
|
+
controller.state = state;
|
|
210
|
+
// 持久化失败不能当成功继续:否则会拿不一致的状态去起会话、投反馈,
|
|
211
|
+
// 重启后又从旧 checkpoint 恢复,重现幽灵审查与重复反馈。
|
|
212
|
+
const persisted = persist(pi, state);
|
|
213
|
+
if (!persisted) return;
|
|
214
|
+
syncOccupancy(controller);
|
|
215
|
+
syncUi();
|
|
216
|
+
}
|
|
217
|
+
await runEffects(effects);
|
|
218
|
+
});
|
|
219
|
+
// 队列一旦 rejected 就再也不会执行后续迁移(连 esc 取消也会失效):
|
|
220
|
+
// 副作用异常只能到此为止,不得杀死状态机。
|
|
221
|
+
dispatchQueue = run.catch((error) => {
|
|
222
|
+
notifyEffectFailure(error);
|
|
223
|
+
});
|
|
224
|
+
return dispatchQueue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function notifyEffectFailure(error: unknown) {
|
|
228
|
+
const active = controller;
|
|
229
|
+
if (!active?.ctx.hasUI) return;
|
|
230
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
231
|
+
active.ctx.ui.notify(
|
|
232
|
+
active.config.language === "en"
|
|
233
|
+
? `fire-review step failed: ${message}`
|
|
234
|
+
: `fire-review 步骤失败:${message}`,
|
|
235
|
+
"warning",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 读 review 配置;存在配置问题就不交出可用配置。
|
|
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 };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function limitsOf(config: ReviewConfig): ReviewLimits {
|
|
269
|
+
return {
|
|
270
|
+
maxRounds: config.maxRounds,
|
|
271
|
+
advisorAfterFailures: config.advisorAfterFailures,
|
|
272
|
+
advisorModel: config.advisor.model,
|
|
273
|
+
language: config.language,
|
|
274
|
+
reviewers: config.reviewers.map((item) => ({
|
|
275
|
+
model: item.model,
|
|
276
|
+
thinking: item.thinking,
|
|
277
|
+
})),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function handleCommand(
|
|
282
|
+
pi: ExtensionAPI,
|
|
283
|
+
args: string,
|
|
284
|
+
ctx: ExtensionContext,
|
|
285
|
+
runSession: ReviewSessionRunner,
|
|
286
|
+
) {
|
|
287
|
+
// 配置解析失败不能让命令无声失败:pi 会捕获 handler 异常,用户只会看到什么都没发生。
|
|
288
|
+
const loaded = loadReviewConfig();
|
|
289
|
+
if ("error" in loaded) {
|
|
290
|
+
if (ctx.hasUI) ctx.ui.notify(loaded.error, "error");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const config = loaded.config;
|
|
294
|
+
if (controller && isActive(controller.state)) {
|
|
295
|
+
if (ctx.hasUI) ctx.ui.notify(
|
|
296
|
+
config.language === "en"
|
|
297
|
+
? "A review is already running."
|
|
298
|
+
: "已有审查在进行中。",
|
|
299
|
+
"info",
|
|
300
|
+
);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// 旧审查的看门狗必须在覆盖 controller 前停掉:它的回调读的是全局 controller,
|
|
304
|
+
// 否则旧超时到点时会把新一场审查中止。
|
|
305
|
+
clearWatchdog();
|
|
306
|
+
const command = parseCommand(args, config.language);
|
|
307
|
+
if ("error" in command) {
|
|
308
|
+
if (ctx.hasUI) ctx.ui.notify(command.error, "error");
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
controller = {
|
|
312
|
+
pi,
|
|
313
|
+
ctx,
|
|
314
|
+
config,
|
|
315
|
+
state: initialState(randomUUID()),
|
|
316
|
+
signal: new AbortController(),
|
|
317
|
+
watchdog: undefined,
|
|
318
|
+
persistedStamp: null,
|
|
319
|
+
pendingCards: [],
|
|
320
|
+
runSession,
|
|
321
|
+
progress: initialProgress(config.reviewers, config.language),
|
|
322
|
+
};
|
|
323
|
+
armWatchdog();
|
|
324
|
+
armStatusTimer();
|
|
325
|
+
// 命令入口也只提出推进请求;真正开审统一经过下一 event-loop 的 idle barrier.
|
|
326
|
+
void dispatch(pi, {
|
|
327
|
+
type: "START",
|
|
328
|
+
focus: command.focus,
|
|
329
|
+
busy: true,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** 重启 / 会话恢复:从 checkpoint 重建 controller 并续跑未完成的环节。
|
|
334
|
+
* reload / new / resume / fork 是运行时替换(新 pi),先清掉旧 controller,
|
|
335
|
+
* 再按新会话的 checkpoint 恢复;quit 之外不 settle,审查能在重启后继续。 */
|
|
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
|
+
}
|
|
345
|
+
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;
|
|
351
|
+
const loaded = loadReviewConfig();
|
|
352
|
+
if ("error" in loaded) {
|
|
353
|
+
// session_start 的配置告警由入口统一聚合;这里只保留活动 checkpoint,修好后继续。
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const config = loaded.config;
|
|
357
|
+
clearWatchdog();
|
|
358
|
+
controller = {
|
|
359
|
+
pi,
|
|
360
|
+
ctx,
|
|
361
|
+
config,
|
|
362
|
+
state: checkpoint,
|
|
363
|
+
signal: new AbortController(),
|
|
364
|
+
watchdog: undefined,
|
|
365
|
+
persistedStamp: readStamp(ctx),
|
|
366
|
+
pendingCards: [],
|
|
367
|
+
runSession,
|
|
368
|
+
progress: initialProgress(config.reviewers, config.language),
|
|
369
|
+
};
|
|
370
|
+
armWatchdog();
|
|
371
|
+
armStatusTimer();
|
|
372
|
+
syncOccupancy(controller);
|
|
373
|
+
syncUi();
|
|
374
|
+
// 恢复只更新持久状态并提出推进请求;绝不在 session_start handler 内起任何工作。
|
|
375
|
+
return dispatch(pi, { type: "RECOVER" });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function handleAgentStart(pi: ExtensionAPI): Promise<void> {
|
|
379
|
+
const active = controller;
|
|
380
|
+
if (!active || active.pi !== pi) return;
|
|
381
|
+
if (
|
|
382
|
+
active.state.phase === "awaiting_fix" &&
|
|
383
|
+
active.state.repair?.status === "awaiting_start"
|
|
384
|
+
) {
|
|
385
|
+
clearFeedbackStartTimer(active);
|
|
386
|
+
await dispatch(pi, { type: "REPAIR_STARTED" });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (
|
|
390
|
+
active.state.phase === "summarizing" &&
|
|
391
|
+
active.state.summary?.status === "awaiting_start"
|
|
392
|
+
) {
|
|
393
|
+
clearFeedbackStartTimer(active);
|
|
394
|
+
await dispatch(pi, { type: "SUMMARY_STARTED" });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
// 其他扩展可在我们排队后异步触发执行模型。agent_start 是宿主提供的硬边界:
|
|
398
|
+
// 宿主会 await 本 handler,因此先 abort 并等所有审查会话真正退出,再允许模型 turn_start。
|
|
399
|
+
if (!active.actionPromise) return;
|
|
400
|
+
active.actionController?.abort();
|
|
401
|
+
await active.actionPromise;
|
|
402
|
+
if (controller !== active || !isActive(active.state)) return;
|
|
403
|
+
active.runningAction = undefined;
|
|
404
|
+
active.actionPromise = undefined;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function handleAgentEnd(
|
|
408
|
+
pi: ExtensionAPI,
|
|
409
|
+
event: { messages: readonly unknown[] },
|
|
410
|
+
): Promise<void> | void {
|
|
411
|
+
const active = controller;
|
|
412
|
+
if (!active || active.pi !== pi) return;
|
|
413
|
+
// 总结回合任何结局都收尾:裁决已落地,总结失败/中断不重试不升级。
|
|
414
|
+
if (active.state.phase === "summarizing" && active.state.summary?.status === "running")
|
|
415
|
+
return dispatch(pi, { type: "SUMMARY_SETTLED" });
|
|
416
|
+
if (
|
|
417
|
+
active.state.phase !== "awaiting_fix" ||
|
|
418
|
+
active.state.repair?.status !== "running"
|
|
419
|
+
) return;
|
|
420
|
+
const assistant = [...event.messages]
|
|
421
|
+
.reverse()
|
|
422
|
+
.find((message) => isRecord(message) && message.role === "assistant");
|
|
423
|
+
if (isRecord(assistant) && assistant.stopReason !== "error" && assistant.stopReason !== "aborted")
|
|
424
|
+
return dispatch(pi, { type: "REPAIR_COMPLETED" });
|
|
425
|
+
active.signal.abort();
|
|
426
|
+
return dispatch(pi, { type: "CANCEL", reason: "user" });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function scheduleAfterAgentSettled(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
430
|
+
requestAdvance(pi, ctx);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function requestAdvance(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
434
|
+
const active = controller;
|
|
435
|
+
if (!active || active.pi !== pi) return;
|
|
436
|
+
const runId = active.state.runId;
|
|
437
|
+
void advanceWhenIdle(pi, ctx, runId).catch((error) => {
|
|
438
|
+
notifyEffectFailure(error);
|
|
439
|
+
if (controller !== active) return;
|
|
440
|
+
active.signal.abort();
|
|
441
|
+
void dispatch(pi, { type: "CANCEL", reason: "user" });
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function advanceWhenIdle(
|
|
446
|
+
pi: ExtensionAPI,
|
|
447
|
+
ctx: ExtensionContext,
|
|
448
|
+
runId: string,
|
|
449
|
+
): Promise<void> {
|
|
450
|
+
const active = controller;
|
|
451
|
+
// 所有启动入口共享 idle 门;正确性另由 agent_start 的同步停审互锁保证。
|
|
452
|
+
if (
|
|
453
|
+
!active ||
|
|
454
|
+
active.pi !== pi ||
|
|
455
|
+
active.state.runId !== runId ||
|
|
456
|
+
active.signal.signal.aborted ||
|
|
457
|
+
!ctx.isIdle() ||
|
|
458
|
+
ctx.hasPendingMessages()
|
|
459
|
+
) return;
|
|
460
|
+
active.ctx = ctx;
|
|
461
|
+
flushPendingCards(pi);
|
|
462
|
+
const { state } = active;
|
|
463
|
+
if (state.phase === "queued") {
|
|
464
|
+
await dispatch(pi, { type: "ADVANCE" });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (state.phase === "reviewing") {
|
|
468
|
+
startAction(active, `review:${state.round}`, "reviewer", () => startReviewers(pi));
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (state.phase === "needs_fix") {
|
|
472
|
+
startAction(active, `advisor:${state.round}`, "advisor", () => consultAdvisor(pi));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (state.phase === "summarizing") {
|
|
476
|
+
if (state.summary?.status !== "pending") return;
|
|
477
|
+
await dispatch(pi, { type: "SUMMARY_DISPATCHED" });
|
|
478
|
+
const current = controller?.state;
|
|
479
|
+
if (controller === active && current?.phase === "summarizing" && current.summary?.status === "awaiting_start")
|
|
480
|
+
deliverSummaryNow(pi, current);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (state.phase !== "awaiting_fix" || !state.repair) return;
|
|
484
|
+
if (state.repair.status === "pending") {
|
|
485
|
+
await dispatch(pi, { type: "FEEDBACK_DISPATCHED" });
|
|
486
|
+
const repair = controller?.state.repair;
|
|
487
|
+
if (controller === active && repair?.status === "awaiting_start")
|
|
488
|
+
deliverFeedbackNow(pi, repair.details, repair.advisor);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (state.repair.status === "completed") await dispatch(pi, { type: "ADVANCE" });
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function startAction(
|
|
495
|
+
active: Controller,
|
|
496
|
+
key: string,
|
|
497
|
+
kind: "reviewer" | "advisor",
|
|
498
|
+
run: () => Promise<void>,
|
|
499
|
+
): void {
|
|
500
|
+
if (active.runningAction === key) return;
|
|
501
|
+
active.runningAction = key;
|
|
502
|
+
active.actionController = new AbortController();
|
|
503
|
+
const action = run()
|
|
504
|
+
.catch(async (error) => {
|
|
505
|
+
if (controller !== active || active.actionController?.signal.aborted) return;
|
|
506
|
+
await dispatch(active.pi, {
|
|
507
|
+
type: "INFRASTRUCTURE_ERROR",
|
|
508
|
+
details: sessionErrorText(kind, active.config.language, error),
|
|
509
|
+
});
|
|
510
|
+
})
|
|
511
|
+
.finally(() => {
|
|
512
|
+
if (active.actionPromise !== action) return;
|
|
513
|
+
active.actionPromise = undefined;
|
|
514
|
+
active.actionController = undefined;
|
|
515
|
+
active.runningAction = undefined;
|
|
516
|
+
});
|
|
517
|
+
active.actionPromise = action;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function handleShutdown(
|
|
521
|
+
pi: ExtensionAPI,
|
|
522
|
+
reason: "quit" | "reload" | "new" | "resume" | "fork",
|
|
523
|
+
ctx: ExtensionContext,
|
|
524
|
+
): Promise<void> {
|
|
525
|
+
const active = controller;
|
|
526
|
+
if (!active) return;
|
|
527
|
+
// 会话离开当前运行时就立即释放;reload 恢复会由新 controller 重新配对喊占用。
|
|
528
|
+
setOccupancy(active, false);
|
|
529
|
+
// 无论何种终止都先取消并等待当前审查会话;旧动作不得泄漏到新运行时。
|
|
530
|
+
active.signal.abort();
|
|
531
|
+
active.actionController?.abort();
|
|
532
|
+
clearWatchdog();
|
|
533
|
+
clearStatusTimer(active);
|
|
534
|
+
clearFeedbackStartTimer(active);
|
|
535
|
+
await active.actionPromise;
|
|
536
|
+
if (active.ctx !== ctx) active.ctx = ctx;
|
|
537
|
+
if (reason === "quit") {
|
|
538
|
+
await dispatch(pi, { type: "CANCEL", reason: "shutdown" });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
// reload / new / resume / fork 保留 checkpoint,由新运行时在 post-session 边界恢复。
|
|
542
|
+
hideActivity(active.ctx);
|
|
543
|
+
releaseEditor(active);
|
|
544
|
+
await dispatchQueue;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function armWatchdog() {
|
|
548
|
+
if (!controller) return;
|
|
549
|
+
clearWatchdog();
|
|
550
|
+
const elapsed = controller.state.startedAt
|
|
551
|
+
? Math.max(0, Date.now() - controller.state.startedAt)
|
|
552
|
+
: 0;
|
|
553
|
+
const remaining = Math.max(0, overallTimeoutMs(controller.config) - elapsed);
|
|
554
|
+
if (remaining === 0) {
|
|
555
|
+
controller.signal.abort();
|
|
556
|
+
void dispatch(controller.pi, { type: "TIMEOUT" });
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
controller.watchdog = setTimeout(() => {
|
|
560
|
+
const active = controller;
|
|
561
|
+
if (!active || !isActive(active.state)) return;
|
|
562
|
+
active.signal.abort();
|
|
563
|
+
active.actionController?.abort();
|
|
564
|
+
dispatch(active.pi, { type: "TIMEOUT" });
|
|
565
|
+
}, remaining);
|
|
566
|
+
controller.watchdog.unref?.();
|
|
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?.();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function clearStatusTimer(active: Controller) {
|
|
585
|
+
if (active.statusTimer) clearInterval(active.statusTimer);
|
|
586
|
+
active.statusTimer = undefined;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function clearFeedbackStartTimer(active: Controller): void {
|
|
590
|
+
if (active.feedbackStartTimer) clearTimeout(active.feedbackStartTimer);
|
|
591
|
+
active.feedbackStartTimer = undefined;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ---- 持久化与状态栏(状态的投影)----
|
|
595
|
+
|
|
596
|
+
/** 返回是否已可靠落盘;false 时调用方必须停下本次迁移的副作用。 */
|
|
597
|
+
function persist(pi: ExtensionAPI, state: ReviewState): boolean {
|
|
598
|
+
if (!controller) return false;
|
|
599
|
+
const persisted = controller.persistedStamp;
|
|
600
|
+
if (persisted === undefined) return false; // 冲突或写入失败后停写
|
|
601
|
+
try {
|
|
602
|
+
controller.persistedStamp =
|
|
603
|
+
persisted === null
|
|
604
|
+
? beginCheckpoint(pi, controller.ctx, state)
|
|
605
|
+
: writeCheckpoint(pi, controller.ctx, state, persisted);
|
|
606
|
+
return true;
|
|
607
|
+
} catch (error) {
|
|
608
|
+
if (error instanceof CheckpointConflictError) {
|
|
609
|
+
// 持久化里出现不是本 controller 写的 Run ID:并发冲突,停止审查。
|
|
610
|
+
setOccupancy(controller, false);
|
|
611
|
+
controller.persistedStamp = undefined;
|
|
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" });
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
// 普通写入失败(如会话落盘异常):停掉本场审查,不带着不一致状态继续跑。
|
|
625
|
+
setOccupancy(controller, false);
|
|
626
|
+
controller.persistedStamp = undefined;
|
|
627
|
+
controller.signal.abort();
|
|
628
|
+
controller.actionController?.abort();
|
|
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);
|
|
638
|
+
// 磁盘上可能还留着上一条活动 checkpoint,重启会把它恢复成幽灵审查:
|
|
639
|
+
// 尽力补写一条终态。写不进去时不假装成功,在通知里告知用户。
|
|
640
|
+
let sealed = true;
|
|
641
|
+
try {
|
|
642
|
+
beginCheckpoint(pi, controller.ctx, {
|
|
643
|
+
...state,
|
|
644
|
+
phase: "settled",
|
|
645
|
+
active: null,
|
|
646
|
+
pending: null,
|
|
647
|
+
repair: null,
|
|
648
|
+
summary: null,
|
|
649
|
+
});
|
|
650
|
+
} catch {
|
|
651
|
+
sealed = false;
|
|
652
|
+
}
|
|
653
|
+
// 内存态也必须释放:只停会话但留着活动态 controller,会把幽灵审查从磁盘搬到内存——
|
|
654
|
+
// 后续命令永远被「已有审查在进行中」挡住,且无处取消。
|
|
655
|
+
clearWatchdog();
|
|
656
|
+
clearStatusTimer(controller);
|
|
657
|
+
clearFeedbackStartTimer(controller);
|
|
658
|
+
const uiCtx = controller.ctx;
|
|
659
|
+
const language = controller.config.language;
|
|
660
|
+
if (uiCtx.hasUI) uiCtx.ui.setStatus(STATUS_KEY, undefined);
|
|
661
|
+
controller = undefined;
|
|
662
|
+
if (!sealed && uiCtx.hasUI)
|
|
663
|
+
uiCtx.ui.notify(
|
|
664
|
+
language === "en"
|
|
665
|
+
? "fire-review could not seal the checkpoint; a restart may resume this review — cancel it with esc."
|
|
666
|
+
: "fire-review 无法写入终态,重启后可能恢复这场审查,到时按 esc 取消。",
|
|
667
|
+
"warning",
|
|
668
|
+
);
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function errorText(error: unknown) {
|
|
674
|
+
return error instanceof Error ? error.message : String(error);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function syncOccupancy(active: Controller): void {
|
|
678
|
+
setOccupancy(active, isActive(active.state));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* 标签租约:持有期带 TTL 定时续约,释放时清除失败重试一次、再失败由 TTL 到期兜底。
|
|
683
|
+
* 定时续约是租约业务语义:herdr 没有“进程退出即清 metadata”的接口(源码核实),
|
|
684
|
+
* crash/kill 后无 TTL 的标签永驻会让 Master 把 Worker 的真提问误判为审查占用;
|
|
685
|
+
* 续约同时充当首次投递失败的重试。
|
|
686
|
+
*/
|
|
687
|
+
function publishOccupancyLabel(active: Controller, held: boolean): void {
|
|
688
|
+
if (held) {
|
|
689
|
+
void sendOccupancyLabel();
|
|
690
|
+
if (!active.occupancyTimer) {
|
|
691
|
+
active.occupancyTimer = setInterval(() => void sendOccupancyLabel(), OCCUPANCY_REFRESH_MS);
|
|
692
|
+
active.occupancyTimer.unref?.();
|
|
693
|
+
}
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (active.occupancyTimer) {
|
|
697
|
+
clearInterval(active.occupancyTimer);
|
|
698
|
+
active.occupancyTimer = undefined;
|
|
699
|
+
}
|
|
700
|
+
void clearOccupancyLabel();
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function sendOccupancyLabel(): Promise<boolean> {
|
|
704
|
+
const env = herdrPaneEnv();
|
|
705
|
+
if (!env) return Promise.resolve(false);
|
|
706
|
+
return herdrRequest(OCCUPANCY_SOURCE, "pane.report_metadata", {
|
|
707
|
+
pane_id: env.paneId,
|
|
708
|
+
source: OCCUPANCY_SOURCE,
|
|
709
|
+
state_labels: { blocked: OCCUPANCY_LABEL },
|
|
710
|
+
ttl_ms: OCCUPANCY_TTL_MS,
|
|
711
|
+
seq: (occupancySeq += 1),
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function clearOccupancyLabel(): Promise<void> {
|
|
716
|
+
const env = herdrPaneEnv();
|
|
717
|
+
if (!env) return;
|
|
718
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
719
|
+
// 新审查已重新持有时中止重试:迟到的清除会撤掉新租约的标签。
|
|
720
|
+
if (controller?.occupancyHeld) return;
|
|
721
|
+
const delivered = await herdrRequest(OCCUPANCY_SOURCE, "pane.report_metadata", {
|
|
722
|
+
pane_id: env.paneId,
|
|
723
|
+
source: OCCUPANCY_SOURCE,
|
|
724
|
+
clear_state_labels: true,
|
|
725
|
+
seq: (occupancySeq += 1),
|
|
726
|
+
});
|
|
727
|
+
if (delivered) return;
|
|
728
|
+
}
|
|
729
|
+
// 两次未送达:标签带 TTL,最迟 60s 自行过期,不会永久残留。
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function setOccupancy(active: Controller, held: boolean): void {
|
|
733
|
+
if (Boolean(active.occupancyHeld) === held) return;
|
|
734
|
+
active.occupancyHeld = held;
|
|
735
|
+
// 标签走 metadata state_labels:herdr 会丢弃 report_agent 的 message(实测),
|
|
736
|
+
// 只有这条通道能同时到达 Master(state_labels 判定)与侧边栏(state_text token)。
|
|
737
|
+
// 频道仍要发:它驱动 herdr 集成的 blocked 状态本身。占用信号不伤审查。
|
|
738
|
+
publishOccupancyLabel(active, held);
|
|
739
|
+
try {
|
|
740
|
+
active.pi.events.emit(OCCUPANCY_CHANNEL, {
|
|
741
|
+
active: held,
|
|
742
|
+
...(held ? { label: OCCUPANCY_LABEL } : {}),
|
|
743
|
+
});
|
|
744
|
+
} catch (error) {
|
|
745
|
+
// 占用信号只对齐 Herdr 展示;集成故障不能改变审查状态机或会话生命周期。
|
|
746
|
+
try {
|
|
747
|
+
if (active.ctx.hasUI)
|
|
748
|
+
active.ctx.ui.notify(`fire-review 无法同步 Herdr 占用状态:${errorText(error)}`, "warning");
|
|
749
|
+
} catch {
|
|
750
|
+
// 通知本身同样只是展示,不能反向打断审查。
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* UI 投影:状态栏一行 + 编辑器上方活动条 + esc 接管,全部从当前状态派生。
|
|
757
|
+
* 活动条自己按帧重绘,因此进度变化不需要在这里通知。
|
|
758
|
+
*/
|
|
759
|
+
function syncUi(): void {
|
|
760
|
+
const active = controller;
|
|
761
|
+
if (!active) return;
|
|
762
|
+
renderStatus(active.ctx, active.state, active.config.language);
|
|
763
|
+
if (activityView()) {
|
|
764
|
+
showActivity(active.ctx, activityView);
|
|
765
|
+
// 只在等模型结论时接管编辑器;awaiting_fix 相把输入交还用户。
|
|
766
|
+
if (canCancelWithKey()) {
|
|
767
|
+
if (!active.editorLocked) {
|
|
768
|
+
lockEditor(active.ctx, activityView, cancelByUser);
|
|
769
|
+
active.editorLocked = true;
|
|
770
|
+
}
|
|
771
|
+
} else releaseEditor(active);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
clearStatusTimer(active);
|
|
775
|
+
hideActivity(active.ctx);
|
|
776
|
+
releaseEditor(active);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function releaseEditor(active: Controller) {
|
|
780
|
+
if (!active.editorLocked) return;
|
|
781
|
+
unlockEditor(active.ctx);
|
|
782
|
+
active.editorLocked = false;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** 活动条快照;非活动相与总结回合返回 undefined(总结是普通可见回合,不需要活动框)。 */
|
|
786
|
+
function activityView(): ActivityView | undefined {
|
|
787
|
+
const active = controller;
|
|
788
|
+
if (!active || !isActive(active.state) || active.state.phase === "summarizing" || !active.ctx.hasUI) return undefined;
|
|
789
|
+
return {
|
|
790
|
+
phase: active.state.phase,
|
|
791
|
+
round: active.state.round,
|
|
792
|
+
focus: active.state.focus,
|
|
793
|
+
roundStartedAt: active.state.roundStartedAt,
|
|
794
|
+
progressStartedAt: active.progressStartedAt,
|
|
795
|
+
reviewers: active.progress,
|
|
796
|
+
progressKind: active.progressKind,
|
|
797
|
+
advisorRunning: active.state.phase === "needs_fix",
|
|
798
|
+
consecutiveFailures: active.state.consecutiveFailures,
|
|
799
|
+
cwd: active.ctx.cwd,
|
|
800
|
+
language: active.config.language,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function canCancelWithKey() {
|
|
805
|
+
const phase = controller?.state.phase;
|
|
806
|
+
return phase === "queued" || phase === "reviewing" || phase === "needs_fix";
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function cancelByUser() {
|
|
810
|
+
const active = controller;
|
|
811
|
+
if (!active) return;
|
|
812
|
+
if (active.state.phase === "needs_fix") {
|
|
813
|
+
// pi-flow 语义:顾问阶段的 Esc 只跳过本次咨询,不取消整场审查。
|
|
814
|
+
active.actionController?.abort();
|
|
815
|
+
void (active.actionPromise ?? Promise.resolve()).then(() =>
|
|
816
|
+
dispatch(active.pi, { type: "ADVISOR_SKIPPED" }),
|
|
817
|
+
);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
active.signal.abort();
|
|
821
|
+
active.actionController?.abort();
|
|
822
|
+
void dispatch(active.pi, { type: "CANCEL", reason: "user" });
|
|
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}`;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// ---- 副作用执行器 ----
|
|
865
|
+
|
|
866
|
+
/** reducer 只发卡或请求推进;所有会启动工作的动作统一经过 idle barrier。 */
|
|
867
|
+
async function runEffects(effects: ReviewEffect[]) {
|
|
868
|
+
for (const effect of effects) {
|
|
869
|
+
const active = controller;
|
|
870
|
+
if (!active) return;
|
|
871
|
+
if (effect.kind === "advance") {
|
|
872
|
+
requestAdvance(active.pi, active.ctx);
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
try {
|
|
876
|
+
sendCard(active.pi, effect.card);
|
|
877
|
+
} catch (error) {
|
|
878
|
+
notifyEffectFailure(error);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
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
|
+
|
|
894
|
+
function reviewerModelConfig(model: ReviewConfig["advisor"], config: ReviewConfig): ReviewModelConfig {
|
|
895
|
+
return {
|
|
896
|
+
model: model.model,
|
|
897
|
+
thinking: model.thinking,
|
|
898
|
+
tools: config.tools,
|
|
899
|
+
timeoutMs: config.timeoutMinutes * 60_000,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** 开审那一刻取会话分支快照构造 prompt,所有审查者共用同一 prompt。 */
|
|
904
|
+
async function startReviewers(pi: ExtensionAPI): Promise<void> {
|
|
905
|
+
const active = controller;
|
|
906
|
+
if (!active) return;
|
|
907
|
+
const { state, config } = active;
|
|
908
|
+
if (!state.active) return;
|
|
909
|
+
const currentActive = state.active;
|
|
910
|
+
const actionSignal = active.actionController?.signal ?? active.signal.signal;
|
|
911
|
+
active.progressStartedAt = Date.now();
|
|
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);
|
|
928
|
+
const prompt = buildReviewPrompt(readPrompt("review", config.language), {
|
|
929
|
+
language: config.language,
|
|
930
|
+
scope: scopeText(config.language),
|
|
931
|
+
focus: state.focus,
|
|
932
|
+
evidence: evidence.text,
|
|
933
|
+
history: state.history,
|
|
934
|
+
round: state.round,
|
|
935
|
+
});
|
|
936
|
+
const tasks = currentActive.reviewers
|
|
937
|
+
.filter((reviewer) => reviewer.status === "running")
|
|
938
|
+
.map(async (reviewer) => {
|
|
939
|
+
try {
|
|
940
|
+
const result = await runReviewer({
|
|
941
|
+
index: reviewer.index,
|
|
942
|
+
config: reviewerModelConfig(reviewer, config),
|
|
943
|
+
prompt,
|
|
944
|
+
cwd: active.ctx.cwd,
|
|
945
|
+
language: config.language,
|
|
946
|
+
signal: actionSignal,
|
|
947
|
+
runSession: active.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
|
+
},
|
|
957
|
+
});
|
|
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
|
+
if (!actionSignal.aborted)
|
|
968
|
+
await dispatch(pi, { type: "REVIEWER_SETTLED", index: result.index, result });
|
|
969
|
+
} catch (error) {
|
|
970
|
+
if (actionSignal.aborted) return;
|
|
971
|
+
await dispatch(pi, {
|
|
972
|
+
type: "REVIEWER_SETTLED",
|
|
973
|
+
index: reviewer.index,
|
|
974
|
+
result: {
|
|
975
|
+
index: reviewer.index,
|
|
976
|
+
model: reviewer.model,
|
|
977
|
+
thinking: reviewer.thinking,
|
|
978
|
+
status: "error",
|
|
979
|
+
summary: "",
|
|
980
|
+
details: sessionErrorText("reviewer", active.config.language, error),
|
|
981
|
+
},
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
await Promise.all(tasks);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
async function consultAdvisor(pi: ExtensionAPI): Promise<void> {
|
|
989
|
+
const active = controller;
|
|
990
|
+
if (!active) return;
|
|
991
|
+
const { state, config } = active;
|
|
992
|
+
if (!state.pending) return;
|
|
993
|
+
const pending = state.pending;
|
|
994
|
+
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
|
+
const prompt = buildAdvisorPrompt(readPrompt("advisor", config.language), {
|
|
999
|
+
language: config.language,
|
|
1000
|
+
focus: state.focus,
|
|
1001
|
+
details: pending.details,
|
|
1002
|
+
history: state.history,
|
|
1003
|
+
round: pending.round,
|
|
1004
|
+
});
|
|
1005
|
+
try {
|
|
1006
|
+
const result = await runAdvisor({
|
|
1007
|
+
config: reviewerModelConfig(config.advisor, config),
|
|
1008
|
+
prompt,
|
|
1009
|
+
cwd: active.ctx.cwd,
|
|
1010
|
+
language: config.language,
|
|
1011
|
+
signal: actionSignal,
|
|
1012
|
+
runSession: active.runSession,
|
|
1013
|
+
onEvent: (event) => {
|
|
1014
|
+
if (controller !== active) return;
|
|
1015
|
+
active.progress = applySessionEvent(active.progress, 0, event, config.language);
|
|
1016
|
+
},
|
|
1017
|
+
});
|
|
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
|
+
if (!actionSignal.aborted)
|
|
1028
|
+
await dispatch(pi, { type: "ADVISOR_SETTLED", result });
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
if (actionSignal.aborted) return;
|
|
1031
|
+
await dispatch(pi, {
|
|
1032
|
+
type: "INFRASTRUCTURE_ERROR",
|
|
1033
|
+
details: sessionErrorText("advisor", active.config.language, error),
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
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
|
+
function sessionErrorText(kind: "reviewer" | "advisor", language: Language, error: unknown) {
|
|
1064
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1065
|
+
if (kind === "reviewer")
|
|
1066
|
+
return language === "en" ? `reviewer session error: ${message}` : `审查会话异常:${message}`;
|
|
1067
|
+
return language === "en" ? `advisor session error: ${message}` : `顾问会话异常:${message}`;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function deliverFeedbackNow(
|
|
1071
|
+
pi: ExtensionAPI,
|
|
1072
|
+
details: string,
|
|
1073
|
+
advisor: AdvisorResult | null,
|
|
1074
|
+
) {
|
|
1075
|
+
const active = controller;
|
|
1076
|
+
if (!active || active.pi !== pi) return;
|
|
1077
|
+
const feedback = buildFixFeedback({
|
|
1078
|
+
language: active.config.language,
|
|
1079
|
+
details,
|
|
1080
|
+
advisor,
|
|
1081
|
+
});
|
|
1082
|
+
clearFeedbackStartTimer(active);
|
|
1083
|
+
// API 返回 void,真实异步失败不会进 try/catch;持久化状态等待 agent_start 回执。
|
|
1084
|
+
active.feedbackStartTimer = setTimeout(() => {
|
|
1085
|
+
if (
|
|
1086
|
+
controller !== active ||
|
|
1087
|
+
active.state.phase !== "awaiting_fix" ||
|
|
1088
|
+
active.state.repair?.status !== "awaiting_start"
|
|
1089
|
+
) return;
|
|
1090
|
+
active.feedbackStartTimer = undefined;
|
|
1091
|
+
if (active.ctx.hasUI)
|
|
1092
|
+
active.ctx.ui.notify(
|
|
1093
|
+
active.config.language === "en"
|
|
1094
|
+
? "fire-review feedback did not start a repair turn; review stopped."
|
|
1095
|
+
: "fire-review 修复反馈未能启动回合,审查已停止。",
|
|
1096
|
+
"error",
|
|
1097
|
+
);
|
|
1098
|
+
active.signal.abort();
|
|
1099
|
+
void dispatch(pi, { type: "CANCEL", reason: "user" });
|
|
1100
|
+
}, FEEDBACK_START_TIMEOUT_MS);
|
|
1101
|
+
active.feedbackStartTimer.unref?.();
|
|
1102
|
+
// display:false 的消息进 LLM 上下文但不渲染;triggerTurn 让执行模型开始修复回合。
|
|
1103
|
+
try {
|
|
1104
|
+
pi.sendMessage(
|
|
1105
|
+
{ customType: FEEDBACK_TYPE, content: feedback, display: false },
|
|
1106
|
+
{ deliverAs: "followUp", triggerTurn: true },
|
|
1107
|
+
);
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
clearFeedbackStartTimer(active);
|
|
1110
|
+
throw error;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/** 总结提示投递:与修复反馈同一套 agent_start 回执机制;失败不升级,静默收尾。 */
|
|
1115
|
+
function deliverSummaryNow(pi: ExtensionAPI, state: ReviewState): void {
|
|
1116
|
+
const active = controller;
|
|
1117
|
+
if (!active || active.pi !== pi || !state.summary) return;
|
|
1118
|
+
const last = state.history.at(-1);
|
|
1119
|
+
const material = state.summary.kind === "advisor_stop"
|
|
1120
|
+
? last?.advisor?.advice ?? last?.details ?? ""
|
|
1121
|
+
: last?.details ?? "";
|
|
1122
|
+
const prompt = buildSummaryPrompt({
|
|
1123
|
+
language: active.config.language,
|
|
1124
|
+
kind: state.summary.kind,
|
|
1125
|
+
rounds: state.history.length,
|
|
1126
|
+
material,
|
|
1127
|
+
});
|
|
1128
|
+
clearFeedbackStartTimer(active);
|
|
1129
|
+
active.feedbackStartTimer = setTimeout(() => {
|
|
1130
|
+
if (
|
|
1131
|
+
controller !== active ||
|
|
1132
|
+
active.state.phase !== "summarizing" ||
|
|
1133
|
+
active.state.summary?.status !== "awaiting_start"
|
|
1134
|
+
) return;
|
|
1135
|
+
active.feedbackStartTimer = undefined;
|
|
1136
|
+
// 总结是尽力而非必须:未能启动回合就静默收尾,裁决与结果卡已落地。
|
|
1137
|
+
if (active.ctx.hasUI)
|
|
1138
|
+
active.ctx.ui.notify(
|
|
1139
|
+
active.config.language === "en"
|
|
1140
|
+
? "fire-review summary turn did not start; finishing without it."
|
|
1141
|
+
: "fire-review 总结回合未能启动,已直接收尾。",
|
|
1142
|
+
"warning",
|
|
1143
|
+
);
|
|
1144
|
+
void dispatch(pi, { type: "SUMMARY_SETTLED" });
|
|
1145
|
+
}, FEEDBACK_START_TIMEOUT_MS);
|
|
1146
|
+
active.feedbackStartTimer.unref?.();
|
|
1147
|
+
try {
|
|
1148
|
+
pi.sendMessage(
|
|
1149
|
+
{ customType: SUMMARY_REQUEST_TYPE, content: prompt, display: false },
|
|
1150
|
+
{ deliverAs: "followUp", triggerTurn: true },
|
|
1151
|
+
);
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
clearFeedbackStartTimer(active);
|
|
1154
|
+
notifyEffectFailure(error);
|
|
1155
|
+
void dispatch(pi, { type: "SUMMARY_SETTLED" });
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function sendCard(pi: ExtensionAPI, card: CardData) {
|
|
1160
|
+
if (!controller) return;
|
|
1161
|
+
// pi-flow 的用户取消是即时临时通知,不进会话;shutdown 静默收口。
|
|
1162
|
+
if (card.kind === "cancel") {
|
|
1163
|
+
if (card.reason === "user" && controller.ctx.hasUI)
|
|
1164
|
+
controller.ctx.ui.notify(
|
|
1165
|
+
controller.config.language === "en"
|
|
1166
|
+
? "⏸ Review cancelled\nStopped by user"
|
|
1167
|
+
: "⏸ 审查已取消\n已按你的操作停止",
|
|
1168
|
+
"info",
|
|
1169
|
+
);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
// 宿主在 streaming 时会把无 options 的 sendMessage 当 steer 塞进当前模型回合。
|
|
1173
|
+
// 卡片只是 UI 投影,绝不能因此唤醒或打断执行模型。
|
|
1174
|
+
if (!controller.ctx.isIdle()) {
|
|
1175
|
+
controller.pendingCards.push(card);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
sendCardNow(pi, card);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function flushPendingCards(pi: ExtensionAPI): void {
|
|
1182
|
+
const active = controller;
|
|
1183
|
+
if (!active || !active.ctx.isIdle() || active.pendingCards.length === 0) return;
|
|
1184
|
+
const cards = active.pendingCards.splice(0);
|
|
1185
|
+
for (const card of cards) {
|
|
1186
|
+
try {
|
|
1187
|
+
sendCardNow(pi, card);
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
notifyEffectFailure(error);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function sendCardNow(pi: ExtensionAPI, card: CardData): void {
|
|
1195
|
+
if (!controller) return;
|
|
1196
|
+
const built = buildCard(card, controller.config.language);
|
|
1197
|
+
pi.sendMessage({
|
|
1198
|
+
customType: CARD_TYPE,
|
|
1199
|
+
content: reviewEnvelope(built.content),
|
|
1200
|
+
display: true,
|
|
1201
|
+
details: built.details,
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function parseCommand(args: string, language: Language): { focus: string } | { error: string } {
|
|
1206
|
+
const input = args.trim();
|
|
1207
|
+
return input.startsWith("--")
|
|
1208
|
+
? { error: language === "en" ? "Invalid fire-review arguments." : "fire-review 参数无效" }
|
|
1209
|
+
: { focus: input };
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/** 会话分支 entries(供证据组装);本插件的卡与反馈消息不参与证据,避免自指。 */
|
|
1213
|
+
function sessionEntries() {
|
|
1214
|
+
const manager = controller?.ctx.sessionManager as
|
|
1215
|
+
| { getBranch?: () => unknown[] }
|
|
1216
|
+
| undefined;
|
|
1217
|
+
const entries = manager?.getBranch?.() ?? [];
|
|
1218
|
+
return entries.filter(
|
|
1219
|
+
(entry) =>
|
|
1220
|
+
!isRecord(entry) ||
|
|
1221
|
+
entry.type !== "custom_message" ||
|
|
1222
|
+
(entry.customType !== CARD_TYPE && entry.customType !== FEEDBACK_TYPE &&
|
|
1223
|
+
entry.customType !== SUMMARY_REQUEST_TYPE),
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function scopeText(language: Language) {
|
|
1228
|
+
return language === "en"
|
|
1229
|
+
? "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."
|
|
1230
|
+
: "当前会话当前任务的交付质量。首条用户消息是原始需求锚点;后续用户消息可能覆盖、缩小或修正,以后者为准。";
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1234
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
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
|
+
}
|