diffsplain 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -5
- package/dist/assets/{angular-html-e4D_THVy.js → angular-html-m_m91ZYB.js} +1 -1
- package/dist/assets/{angular-ts-Cx3870JF.js → angular-ts-CO5Z--Rb.js} +1 -1
- package/dist/assets/{apl-DZuu22Wh.js → apl-ogYYgLUu.js} +1 -1
- package/dist/assets/{astro-K-NemFAk.js → astro-DAfY3cNR.js} +1 -1
- package/dist/assets/{blade-ConbVu3U.js → blade-E6RzsLUV.js} +1 -1
- package/dist/assets/{c-DtmaBqln.js → c-kKdxhtHF.js} +1 -1
- package/dist/assets/{cobol-BMHoPOfB.js → cobol-3oDDyFNf.js} +1 -1
- package/dist/assets/{coffee-DfweJ19Y.js → coffee-dsiILuWe.js} +1 -1
- package/dist/assets/{cpp-CPh_zIOW.js → cpp-DYyNFq__.js} +1 -1
- package/dist/assets/{crystal-CAEX7C1J.js → crystal-B7dVp9TF.js} +1 -1
- package/dist/assets/{css-C8ihGqCA.js → css-DlxJ0aex.js} +1 -1
- package/dist/assets/{edge-DVZPwQ9j.js → edge-BjEVphkC.js} +1 -1
- package/dist/assets/{elixir-D03TKulP.js → elixir-0xTOtMcM.js} +1 -1
- package/dist/assets/{elm-JUX-2xVV.js → elm-iQk_Wrxq.js} +1 -1
- package/dist/assets/{erb-HbktAFZJ.js → erb-BO74ztiE.js} +1 -1
- package/dist/assets/{git-rebase-Ccd_ELAP.js → git-rebase-BN667QRY.js} +1 -1
- package/dist/assets/{glimmer-js-BcinUfi2.js → glimmer-js-DTGUIHeS.js} +1 -1
- package/dist/assets/{glimmer-ts-DuN2UkN9.js → glimmer-ts-BU3oQn2d.js} +1 -1
- package/dist/assets/{glsl-BWmCEPsR.js → glsl-KLiqLJi2.js} +1 -1
- package/dist/assets/{graphql-DJGYoBFW.js → graphql-D_idcsoJ.js} +1 -1
- package/dist/assets/{hack-ZPqBE9w3.js → hack-BXlsgPIr.js} +1 -1
- package/dist/assets/{haml-CZDZk6Gw.js → haml-fP0aUg4-.js} +1 -1
- package/dist/assets/{handlebars-d23PDS0n.js → handlebars-C1ZEaot-.js} +1 -1
- package/dist/assets/{html-BgzX2DyB.js → html-D04qIuup.js} +1 -1
- package/dist/assets/{html-derivative-Bq6M-ZFw.js → html-derivative-C6ai-9QL.js} +1 -1
- package/dist/assets/{http-DVhMl_cW.js → http-C7bz3ZvQ.js} +1 -1
- package/dist/assets/{hurl-CbspBAqF.js → hurl-jL6B6Ahr.js} +1 -1
- package/dist/assets/index-BPaJGi-m.js +1574 -0
- package/dist/assets/index-VAyBpb3u.css +2 -0
- package/dist/assets/{java--YZbq0z2.js → java-BVzvLCON.js} +1 -1
- package/dist/assets/{javascript-cvRuifRJ.js → javascript-UDbZHxCI.js} +1 -1
- package/dist/assets/{jinja-BeZmr3LP.js → jinja-DB-QKo2A.js} +1 -1
- package/dist/assets/{jison-qrpGZaxs.js → jison-B1ZbR2FF.js} +1 -1
- package/dist/assets/{json-4xUzsNJn.js → json-DV4WwJzT.js} +1 -1
- package/dist/assets/{jsx-BlXrTlSg.js → jsx-DS6_VTvW.js} +1 -1
- package/dist/assets/{julia-IN3jzmBc.js → julia-C4tevET0.js} +1 -1
- package/dist/assets/{just-C_50nLQH.js → just-CWMjefzB.js} +1 -1
- package/dist/assets/{latex-Cd-_2IS1.js → latex-tyEFf_Af.js} +1 -1
- package/dist/assets/{liquid-CEJf6Xg5.js → liquid-C3d1X1rL.js} +1 -1
- package/dist/assets/{lua-BlaV_n8i.js → lua-sXvrzLQY.js} +1 -1
- package/dist/assets/{marko-Cb-NPdc_.js → marko-CQweucif.js} +1 -1
- package/dist/assets/{mdc-Dt4ywVAS.js → mdc-TrxLWF7s.js} +1 -1
- package/dist/assets/{nginx-C_6SKViP.js → nginx-ChE2nSNJ.js} +1 -1
- package/dist/assets/{nim-BbkBjoeZ.js → nim-BY1CTRfx.js} +1 -1
- package/dist/assets/{perl-CYPU6TVa.js → perl-Cvj2PQQH.js} +1 -1
- package/dist/assets/{php-B6Oqilrc.js → php-CGmHp8HW.js} +1 -1
- package/dist/assets/{pug-C9Poo2KU.js → pug-DM4S0bRr.js} +1 -1
- package/dist/assets/{qml-DkMURMPv.js → qml-DI89Al9K.js} +1 -1
- package/dist/assets/{r-Bm-Gfi0p.js → r-Bs5_4yYh.js} +1 -1
- package/dist/assets/{razor-B0LqOok3.js → razor-DimzayhZ.js} +1 -1
- package/dist/assets/{regexp-jAbMHzwe.js → regexp-BLACz0cx.js} +1 -1
- package/dist/assets/{rst-BlWHfg8c.js → rst-DcWzxQh2.js} +1 -1
- package/dist/assets/{ruby-Bt5lKwPL.js → ruby-Dlat5E8d.js} +1 -1
- package/dist/assets/{sas-TuY2-8EF.js → sas-CCDmMaBY.js} +1 -1
- package/dist/assets/{scss-Dh1SsWlK.js → scss-BPiU2nu6.js} +1 -1
- package/dist/assets/{shellscript-CThiwmnp.js → shellscript-CKinVNrN.js} +1 -1
- package/dist/assets/{shellsession-DQ5iRUWd.js → shellsession-Cea8Pu5d.js} +1 -1
- package/dist/assets/{soy-BfM-BrZG.js → soy--BRAPY4-.js} +1 -1
- package/dist/assets/{sql-F617W1uN.js → sql-DjKiCyWK.js} +1 -1
- package/dist/assets/{stata-Ctov3ki2.js → stata-CDvo3J6y.js} +1 -1
- package/dist/assets/{surrealql-C-i2bJGB.js → surrealql-C7b7E5sh.js} +1 -1
- package/dist/assets/{svelte-oZT41pTl.js → svelte-DO0tKtiK.js} +1 -1
- package/dist/assets/{templ-CxX9Hf3e.js → templ-DhhmR_rv.js} +1 -1
- package/dist/assets/{tex-GyzJq8GJ.js → tex-Cn_V-Gg4.js} +1 -1
- package/dist/assets/{ts-tags-BugzKu0x.js → ts-tags-B6c-FTmA.js} +1 -1
- package/dist/assets/{tsx-DLqwDEUq.js → tsx-Cvtt2sJ-.js} +1 -1
- package/dist/assets/{twig-DADiR9Vq.js → twig-B7ZbCUmB.js} +1 -1
- package/dist/assets/{typescript-BlDIG_FO.js → typescript-i3C6um2B.js} +1 -1
- package/dist/assets/{vue-COXmmg2A.js → vue-DyPX0Vum.js} +1 -1
- package/dist/assets/{vue-html-Cy_3KYj1.js → vue-html-D362KpK3.js} +1 -1
- package/dist/assets/{vue-vine-CUqOY8dY.js → vue-vine-D34cBbwO.js} +1 -1
- package/dist/assets/{xml-C9JY7Wf6.js → xml-DJEk7u4n.js} +1 -1
- package/dist/assets/{xsl-DBCl8hSu.js → xsl-CoTtf90f.js} +1 -1
- package/dist/assets/{yaml-BbMebKkS.js → yaml-Cv5epQ4-.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +11 -3
- package/scripts/agent-exclusions.mjs +10 -0
- package/scripts/agent-review.mjs +94 -0
- package/scripts/build-diff-data.mjs +211 -49
- package/scripts/cli-args.mjs +36 -9
- package/scripts/coding-agents.mjs +206 -79
- package/scripts/generate-summaries.mjs +864 -425
- package/scripts/local-target.mjs +106 -0
- package/scripts/present.mjs +205 -72
- package/scripts/review-chat-context.mjs +510 -0
- package/scripts/review-chat-controller.mjs +568 -0
- package/scripts/review-chat-provider.mjs +352 -0
- package/scripts/review-chat.mjs +8 -0
- package/scripts/serve-built.mjs +206 -24
- package/scripts/summary-path.mjs +32 -11
- package/dist/assets/index-C2p_1iAk.css +0 -2
- package/dist/assets/index-divlsuKs.js +0 -1568
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import {
|
|
3
|
+
ReviewChatError,
|
|
4
|
+
chatInputLimit,
|
|
5
|
+
chatSnapshotPath,
|
|
6
|
+
clone,
|
|
7
|
+
commandInput,
|
|
8
|
+
currentPaths,
|
|
9
|
+
inputBytes,
|
|
10
|
+
makeInput,
|
|
11
|
+
newThread,
|
|
12
|
+
providerExecution,
|
|
13
|
+
publicError,
|
|
14
|
+
reviewChatPreservedMessageCount,
|
|
15
|
+
reviewEvidence,
|
|
16
|
+
snapshotFingerprint,
|
|
17
|
+
stateThread,
|
|
18
|
+
threadKey,
|
|
19
|
+
validateReviewChatAnswer,
|
|
20
|
+
} from './review-chat-context.mjs';
|
|
21
|
+
|
|
22
|
+
const noChange = () => {};
|
|
23
|
+
const snapshotOnly = { mode: 'snapshot-only' };
|
|
24
|
+
function snapshotRead(path) {
|
|
25
|
+
try {
|
|
26
|
+
return { value: JSON.parse(readFileSync(path, 'utf8')) };
|
|
27
|
+
} catch {
|
|
28
|
+
return { error: new Error('The current review snapshot is not available.') };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function completeSnapshot(value) {
|
|
33
|
+
return Array.isArray(value?.files) && Boolean(snapshotFingerprint(value));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function currentSnapshot(path) {
|
|
37
|
+
const result = snapshotRead(path);
|
|
38
|
+
if (result.error) throw result.error;
|
|
39
|
+
if (!completeSnapshot(result.value)) {
|
|
40
|
+
throw new Error('The current review snapshot is incomplete.');
|
|
41
|
+
}
|
|
42
|
+
return result.value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function snapshotState(path, run) {
|
|
46
|
+
try {
|
|
47
|
+
return snapshotFingerprint(currentSnapshot(path)) === run.fingerprint
|
|
48
|
+
? 'current'
|
|
49
|
+
: 'changed';
|
|
50
|
+
} catch {
|
|
51
|
+
return 'unavailable';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function staleCurrentThreads(threads, currentThreads) {
|
|
56
|
+
return [...currentThreads.values()].some((id) => threads.get(id)?.stale);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stateThreads(threads, currentThreads) {
|
|
60
|
+
return [...threads.values()]
|
|
61
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
62
|
+
.map((thread) => stateThread(thread, currentThreads.get(thread.key) === thread.id));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function runState(run) {
|
|
66
|
+
return run.kind === 'compact'
|
|
67
|
+
? { status: 'blocked', needsCompaction: true }
|
|
68
|
+
: { status: 'failed' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cancelledState(run) {
|
|
72
|
+
return run.kind === 'compact'
|
|
73
|
+
? { status: 'blocked', needsCompaction: true }
|
|
74
|
+
: { status: 'cancelled' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function matchingRun(thread, run) {
|
|
78
|
+
return thread.run === run && thread.activeRunId === run.id;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function matchingFingerprint(thread, run, fingerprint) {
|
|
82
|
+
return thread.fingerprint === fingerprint && run.fingerprint === fingerprint;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validatedAnswer(value, paths) {
|
|
86
|
+
try {
|
|
87
|
+
return { answer: validateReviewChatAnswer(value, paths) };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return { error };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function executionResult(provider, input) {
|
|
94
|
+
try {
|
|
95
|
+
return { execution: providerExecution(provider, input) };
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return { error };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function historyWithoutQuestion(messages, question) {
|
|
102
|
+
const last = messages.at(-1);
|
|
103
|
+
if (last?.role !== 'user' || last.text !== question) return messages;
|
|
104
|
+
return messages.slice(0, -1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function appendQuestion(thread, question, addQuestion) {
|
|
108
|
+
if (addQuestion) thread.messages.push({ role: 'user', text: question });
|
|
109
|
+
return historyWithoutQuestion(thread.messages, question);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function compactionParts(messages) {
|
|
113
|
+
return {
|
|
114
|
+
older: messages.slice(0, -reviewChatPreservedMessageCount),
|
|
115
|
+
preserved: messages.slice(-reviewChatPreservedMessageCount),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function canCompact(parts, question, limit) {
|
|
120
|
+
return parts.older.length > 0 && inputBytes(parts.preserved, question) <= limit;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function threadCanRetry(thread) {
|
|
124
|
+
return Boolean(thread.pendingQuestion) && !thread.needsCompaction;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function retryStatus(thread) {
|
|
128
|
+
return ['failed', 'cancelled'].includes(thread.status);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export class ReviewChatController {
|
|
132
|
+
constructor(options = {}) {
|
|
133
|
+
this.snapshotPath = chatSnapshotPath(options);
|
|
134
|
+
this.provider = options.provider;
|
|
135
|
+
this.onChange = typeof options.onChange === 'function' ? options.onChange : noChange;
|
|
136
|
+
this.inputLimitBytes = chatInputLimit(options);
|
|
137
|
+
this.accessMode = options.accessMode || snapshotOnly;
|
|
138
|
+
this.threads = new Map();
|
|
139
|
+
this.currentThreads = new Map();
|
|
140
|
+
this.snapshot = undefined;
|
|
141
|
+
this.fingerprint = undefined;
|
|
142
|
+
this.snapshotError = undefined;
|
|
143
|
+
this.runNumber = 0;
|
|
144
|
+
this.threadNumber = 0;
|
|
145
|
+
this.closed = false;
|
|
146
|
+
this.commandHandlers = {
|
|
147
|
+
new: this.newCommand.bind(this),
|
|
148
|
+
ask: this.askCommand.bind(this),
|
|
149
|
+
cancel: this.cancelCommand.bind(this),
|
|
150
|
+
retry: this.retryCommand.bind(this),
|
|
151
|
+
'retry-compaction': this.retryCompactionCommand.bind(this),
|
|
152
|
+
};
|
|
153
|
+
this.refresh({ notify: false });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
api() {
|
|
157
|
+
return {
|
|
158
|
+
getState: this.getState.bind(this),
|
|
159
|
+
command: this.command.bind(this),
|
|
160
|
+
refresh: this.refresh.bind(this),
|
|
161
|
+
close: this.close.bind(this),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
getState() {
|
|
166
|
+
return clone({
|
|
167
|
+
available: Boolean(this.provider) && !this.closed,
|
|
168
|
+
fingerprint: this.fingerprint || null,
|
|
169
|
+
snapshotReady: Boolean(this.snapshot),
|
|
170
|
+
...(this.snapshotError ? { error: this.snapshotError } : {}),
|
|
171
|
+
inputLimitBytes: this.inputLimitBytes,
|
|
172
|
+
stale: staleCurrentThreads(this.threads, this.currentThreads),
|
|
173
|
+
threads: stateThreads(this.threads, this.currentThreads),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
notify() {
|
|
178
|
+
try {
|
|
179
|
+
this.onChange(this.getState());
|
|
180
|
+
} catch {
|
|
181
|
+
// A client notification cannot stop review work.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
assertProvider() {
|
|
186
|
+
if (this.closed) throw new ReviewChatError('Review chat is closed.', 409);
|
|
187
|
+
if (!this.provider) {
|
|
188
|
+
throw new ReviewChatError('Chat needs a selected coding agent.', 409);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
assertAvailable() {
|
|
193
|
+
this.assertProvider();
|
|
194
|
+
if (this.snapshotError) throw new ReviewChatError(this.snapshotError, 409);
|
|
195
|
+
if (!this.snapshot || !this.fingerprint) {
|
|
196
|
+
throw new ReviewChatError('The current review is not ready.', 409);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
checkFile(command) {
|
|
201
|
+
if (command.scope !== 'file') return;
|
|
202
|
+
if (!currentPaths(this.snapshot).has(command.path)) {
|
|
203
|
+
throw new ReviewChatError('That file is not in the current review.');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
currentThread(command) {
|
|
208
|
+
return this.threads.get(this.currentThreads.get(threadKey(command)));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
discardThread(thread) {
|
|
212
|
+
if (!thread) return;
|
|
213
|
+
this.threads.delete(thread.id);
|
|
214
|
+
if (this.currentThreads.get(thread.key) === thread.id) {
|
|
215
|
+
this.currentThreads.delete(thread.key);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
getThread(command) {
|
|
220
|
+
const thread = this.currentThread(command);
|
|
221
|
+
if (!thread) throw new ReviewChatError('Start a new chat thread first.', 409);
|
|
222
|
+
if (thread.stale || thread.fingerprint !== this.fingerprint) {
|
|
223
|
+
throw new ReviewChatError('This chat thread is stale. Start a new thread.', 409);
|
|
224
|
+
}
|
|
225
|
+
return thread;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
currentRun(thread, run) {
|
|
229
|
+
return !this.closed &&
|
|
230
|
+
!thread.stale &&
|
|
231
|
+
matchingRun(thread, run) &&
|
|
232
|
+
matchingFingerprint(thread, run, this.fingerprint);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
clearRun(thread) {
|
|
236
|
+
thread.run = undefined;
|
|
237
|
+
thread.activeRunId = undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
markFailure(thread, run, error) {
|
|
241
|
+
this.clearRun(thread);
|
|
242
|
+
thread.error = publicError(error);
|
|
243
|
+
Object.assign(thread, runState(run));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
markUnavailableRun(thread, run) {
|
|
247
|
+
this.markFailure(thread, run, new Error('The current review snapshot is not available.'));
|
|
248
|
+
this.refresh({ notify: false });
|
|
249
|
+
this.notify();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fenceSnapshot(thread, run, state) {
|
|
253
|
+
if (state === 'current') return false;
|
|
254
|
+
if (state === 'changed') {
|
|
255
|
+
this.refresh();
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
this.markUnavailableRun(thread, run);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
fenceChangedReview(thread, run) {
|
|
263
|
+
if (!this.currentRun(thread, run)) return true;
|
|
264
|
+
return this.fenceSnapshot(thread, run, snapshotState(this.snapshotPath, run));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
finishFailure(thread, run, error) {
|
|
268
|
+
if (this.fenceChangedReview(thread, run)) return;
|
|
269
|
+
this.markFailure(thread, run, error);
|
|
270
|
+
this.notify();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
completeAnswer(thread, answer) {
|
|
274
|
+
this.clearRun(thread);
|
|
275
|
+
thread.messages.push({ role: 'assistant', answer });
|
|
276
|
+
thread.pendingQuestion = undefined;
|
|
277
|
+
thread.needsCompaction = false;
|
|
278
|
+
thread.status = 'ready';
|
|
279
|
+
thread.error = undefined;
|
|
280
|
+
this.notify();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
completeCompaction(thread, run, answer) {
|
|
284
|
+
const messages = [{ role: 'compacted', answer }, ...run.preserved];
|
|
285
|
+
if (inputBytes(messages, thread.pendingQuestion) > this.inputLimitBytes) {
|
|
286
|
+
this.finishFailure(
|
|
287
|
+
thread,
|
|
288
|
+
run,
|
|
289
|
+
new Error('The compacted chat history still exceeds the input limit.'),
|
|
290
|
+
);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.clearRun(thread);
|
|
294
|
+
thread.messages = messages;
|
|
295
|
+
thread.needsCompaction = false;
|
|
296
|
+
this.startAnswer(thread, thread.pendingQuestion);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
completeRun(thread, run, answer) {
|
|
300
|
+
if (run.kind === 'compact') {
|
|
301
|
+
this.completeCompaction(thread, run, answer);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
this.completeAnswer(thread, answer);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
acceptAnswer(thread, run, result) {
|
|
308
|
+
if (result.error) {
|
|
309
|
+
this.finishFailure(thread, run, result.error);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
this.completeRun(thread, run, result.answer);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
finishSuccess(thread, run, value) {
|
|
316
|
+
if (this.fenceChangedReview(thread, run)) return;
|
|
317
|
+
this.acceptAnswer(thread, run, validatedAnswer(value, run.paths));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
startRun(thread, run, input) {
|
|
321
|
+
thread.run = run;
|
|
322
|
+
thread.activeRunId = run.id;
|
|
323
|
+
thread.status = run.kind === 'compact' ? 'compacting' : 'running';
|
|
324
|
+
thread.error = undefined;
|
|
325
|
+
this.notify();
|
|
326
|
+
const result = executionResult(this.provider, input);
|
|
327
|
+
if (result.error) {
|
|
328
|
+
this.finishFailure(thread, run, result.error);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
run.cancel = result.execution.cancel;
|
|
332
|
+
result.execution.promise.then(
|
|
333
|
+
(value) => this.finishSuccess(thread, run, value),
|
|
334
|
+
(error) => this.finishFailure(thread, run, error),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
answerRun(thread, evidence) {
|
|
339
|
+
return {
|
|
340
|
+
id: ++this.runNumber,
|
|
341
|
+
kind: 'answer',
|
|
342
|
+
fingerprint: this.fingerprint,
|
|
343
|
+
paths: new Set(evidence.paths),
|
|
344
|
+
cancel: noChange,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
startAnswer(thread, question, options = {}) {
|
|
349
|
+
const history = appendQuestion(thread, question, options.addQuestion !== false);
|
|
350
|
+
const evidence = reviewEvidence(this.snapshot, thread);
|
|
351
|
+
const run = this.answerRun(thread, evidence);
|
|
352
|
+
this.startRun(thread, run, makeInput({
|
|
353
|
+
kind: 'answer',
|
|
354
|
+
context: evidence.context,
|
|
355
|
+
messages: history,
|
|
356
|
+
question,
|
|
357
|
+
accessMode: this.accessMode,
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
blockForInputLimit(thread) {
|
|
362
|
+
thread.status = 'blocked';
|
|
363
|
+
thread.needsCompaction = false;
|
|
364
|
+
thread.error = 'The newest chat messages exceed the input limit. Start a new thread.';
|
|
365
|
+
this.notify();
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
compactionRun(evidence, preserved) {
|
|
370
|
+
return {
|
|
371
|
+
id: ++this.runNumber,
|
|
372
|
+
kind: 'compact',
|
|
373
|
+
fingerprint: this.fingerprint,
|
|
374
|
+
paths: new Set(evidence.paths),
|
|
375
|
+
preserved,
|
|
376
|
+
cancel: noChange,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
startCompaction(thread) {
|
|
381
|
+
const parts = compactionParts(thread.messages);
|
|
382
|
+
if (!canCompact(parts, thread.pendingQuestion, this.inputLimitBytes)) {
|
|
383
|
+
return this.blockForInputLimit(thread);
|
|
384
|
+
}
|
|
385
|
+
const evidence = reviewEvidence(this.snapshot, thread);
|
|
386
|
+
const run = this.compactionRun(evidence, parts.preserved);
|
|
387
|
+
this.startRun(thread, run, makeInput({
|
|
388
|
+
kind: 'compact',
|
|
389
|
+
context: evidence.context,
|
|
390
|
+
messages: parts.older,
|
|
391
|
+
accessMode: this.accessMode,
|
|
392
|
+
}));
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
cancelProvider(run) {
|
|
397
|
+
try {
|
|
398
|
+
run.cancel();
|
|
399
|
+
} catch {
|
|
400
|
+
// A provider may already have released its child process.
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
cancelRun(thread, { stale = false } = {}) {
|
|
405
|
+
const run = thread.run;
|
|
406
|
+
if (!run) return false;
|
|
407
|
+
this.clearRun(thread);
|
|
408
|
+
this.cancelProvider(run);
|
|
409
|
+
if (!stale) Object.assign(thread, cancelledState(run));
|
|
410
|
+
if (!stale) thread.error = undefined;
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
staleThread(thread) {
|
|
415
|
+
this.cancelRun(thread, { stale: true });
|
|
416
|
+
thread.stale = true;
|
|
417
|
+
thread.status = 'stale';
|
|
418
|
+
thread.error = undefined;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
staleCurrentThreads() {
|
|
422
|
+
for (const id of this.currentThreads.values()) {
|
|
423
|
+
const thread = this.threads.get(id);
|
|
424
|
+
if (thread && !thread.stale) this.staleThread(thread);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
notifyRefresh(changed, recovered, shouldNotify) {
|
|
429
|
+
if (shouldNotify && (changed || recovered)) this.notify();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
acceptSnapshot(next, shouldNotify) {
|
|
433
|
+
const nextFingerprint = snapshotFingerprint(next);
|
|
434
|
+
const changed = Boolean(this.fingerprint && this.fingerprint !== nextFingerprint);
|
|
435
|
+
const recovered = Boolean(this.snapshotError);
|
|
436
|
+
this.snapshot = next;
|
|
437
|
+
this.snapshotError = undefined;
|
|
438
|
+
this.fingerprint = nextFingerprint;
|
|
439
|
+
if (changed) this.staleCurrentThreads();
|
|
440
|
+
this.notifyRefresh(changed, recovered, shouldNotify);
|
|
441
|
+
return this.getState();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
failedRefresh(error, shouldNotify) {
|
|
445
|
+
this.snapshotError = publicError(error);
|
|
446
|
+
if (shouldNotify) this.notify();
|
|
447
|
+
return this.getState();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
refresh(options = {}) {
|
|
451
|
+
if (this.closed) return this.getState();
|
|
452
|
+
const shouldNotify = options.notify !== false;
|
|
453
|
+
try {
|
|
454
|
+
return this.acceptSnapshot(currentSnapshot(this.snapshotPath), shouldNotify);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
return this.failedRefresh(error, shouldNotify);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
result(accepted) {
|
|
461
|
+
return { accepted, state: this.getState() };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
prepareCommand(requested) {
|
|
465
|
+
this.assertAvailable();
|
|
466
|
+
this.checkFile(requested);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
replaceThread(old, requested) {
|
|
470
|
+
if (old) this.cancelRun(old);
|
|
471
|
+
if (old && !old.stale) this.discardThread(old);
|
|
472
|
+
const fresh = newThread(requested, this.fingerprint, `thread-${++this.threadNumber}`);
|
|
473
|
+
this.threads.set(fresh.id, fresh);
|
|
474
|
+
this.currentThreads.set(threadKey(requested), fresh.id);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
newCommand(requested) {
|
|
478
|
+
this.prepareCommand(requested);
|
|
479
|
+
this.replaceThread(this.currentThread(requested), requested);
|
|
480
|
+
this.notify();
|
|
481
|
+
return this.result(false);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
cancelCommand(requested) {
|
|
485
|
+
this.assertProvider();
|
|
486
|
+
const thread = this.getThread(requested);
|
|
487
|
+
if (!this.cancelRun(thread)) {
|
|
488
|
+
throw new ReviewChatError('This chat thread is not running.', 409);
|
|
489
|
+
}
|
|
490
|
+
this.notify();
|
|
491
|
+
return this.result(false);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
assertIdle(thread) {
|
|
495
|
+
if (thread.run) throw new ReviewChatError('This chat thread is already running.', 409);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
assertAskable(thread) {
|
|
499
|
+
if (thread.needsCompaction) {
|
|
500
|
+
throw new ReviewChatError('Retry compaction or start a new thread before asking again.', 409);
|
|
501
|
+
}
|
|
502
|
+
if (thread.status === 'blocked') {
|
|
503
|
+
throw new ReviewChatError('Start a new thread before asking again.', 409);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
startQuestion(thread, question) {
|
|
508
|
+
thread.pendingQuestion = question;
|
|
509
|
+
if (inputBytes(thread.messages, question) > this.inputLimitBytes) {
|
|
510
|
+
return this.startCompaction(thread);
|
|
511
|
+
}
|
|
512
|
+
this.startAnswer(thread, question);
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
askCommand(requested) {
|
|
517
|
+
this.prepareCommand(requested);
|
|
518
|
+
const thread = this.getThread(requested);
|
|
519
|
+
this.assertIdle(thread);
|
|
520
|
+
this.assertAskable(thread);
|
|
521
|
+
return this.result(this.startQuestion(thread, requested.question));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
assertRetryable(thread) {
|
|
525
|
+
if (!threadCanRetry(thread)) {
|
|
526
|
+
throw new ReviewChatError('This chat thread has no answer to retry.', 409);
|
|
527
|
+
}
|
|
528
|
+
if (!retryStatus(thread)) {
|
|
529
|
+
throw new ReviewChatError('This chat thread cannot be retried now.', 409);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
retryCommand(requested) {
|
|
534
|
+
this.prepareCommand(requested);
|
|
535
|
+
const thread = this.getThread(requested);
|
|
536
|
+
this.assertIdle(thread);
|
|
537
|
+
this.assertRetryable(thread);
|
|
538
|
+
this.startAnswer(thread, thread.pendingQuestion, { addQuestion: false });
|
|
539
|
+
return this.result(true);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
retryCompactionCommand(requested) {
|
|
543
|
+
this.prepareCommand(requested);
|
|
544
|
+
const thread = this.getThread(requested);
|
|
545
|
+
this.assertIdle(thread);
|
|
546
|
+
if (!thread.pendingQuestion || !thread.needsCompaction) {
|
|
547
|
+
throw new ReviewChatError('This chat thread has no compaction to retry.', 409);
|
|
548
|
+
}
|
|
549
|
+
return this.result(this.startCompaction(thread));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
command(rawCommand) {
|
|
553
|
+
const requested = commandInput(rawCommand);
|
|
554
|
+
return this.commandHandlers[requested.type](requested);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
close() {
|
|
558
|
+
if (this.closed) return;
|
|
559
|
+
this.closed = true;
|
|
560
|
+
for (const thread of this.threads.values()) this.cancelRun(thread);
|
|
561
|
+
this.provider?.close?.();
|
|
562
|
+
this.threads.clear();
|
|
563
|
+
this.currentThreads.clear();
|
|
564
|
+
this.snapshot = undefined;
|
|
565
|
+
this.fingerprint = undefined;
|
|
566
|
+
this.snapshotError = undefined;
|
|
567
|
+
}
|
|
568
|
+
}
|