papilot 0.8.4 → 0.8.6
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/dist/assets/index-Dm7AYM9e.js +267 -0
- package/dist/assets/index-PFQDdu53.css +1 -0
- package/dist/assets/{math-UelUOI-R.js → math-C7ENJr-a.js} +1 -1
- package/dist/assets/pdf-view-B2xQuHmI.js +598 -0
- package/dist/index.html +2 -2
- package/engine/dist/web/assets/{FullPaper-DbT25OhQ.js → FullPaper-DlgZ1MOz.js} +1 -1
- package/engine/dist/web/assets/index-BCgVChtV.js +19 -0
- package/engine/dist/web/index.html +1 -1
- package/engine/src/client.ts +11 -4
- package/engine/src/core/build.ts +24 -13
- package/engine/src/core/publish.ts +17 -3
- package/engine/src/core/store.ts +1 -0
- package/engine/src/server.ts +3 -2
- package/engine/src/web/review.tsx +37 -7
- package/package.json +2 -2
- package/src/server/agents.ts +10 -1
- package/src/server/app.ts +6 -1
- package/src/server/cursor.ts +20 -1
- package/src/server/git-overview.ts +9 -5
- package/src/server/manuscript.ts +21 -2
- package/src/server/review.ts +8 -3
- package/src/server/start.ts +10 -3
- package/src/server/update-install.ts +61 -0
- package/src/server/updates.ts +77 -34
- package/src/server/workspace.ts +76 -15
- package/src/shared.ts +8 -3
- package/dist/assets/index-CwFvCo6r.css +0 -1
- package/dist/assets/index-dByEATYE.js +0 -267
- package/dist/assets/pdf-view-ChJvfsD8.js +0 -598
- package/engine/dist/web/assets/index-C_KwLmXQ.js +0 -19
package/src/server/updates.ts
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { devRevision, newerVersion, readJson, stableVersion, writeJson } from '../../bin/installation.mjs';
|
|
6
|
-
import { installRelease, loaderFor, npmCli } from './update-install.ts';
|
|
6
|
+
import { download, installLayer, installRelease, loaderFor, npmCli, type DesktopLayer } from './update-install.ts';
|
|
7
7
|
import type { UpdateChannel, UpdateStatus } from '../shared.ts';
|
|
8
8
|
|
|
9
9
|
const activePhases = new Set(['installing', 'waiting', 'restarting']);
|
|
@@ -31,28 +31,41 @@ export async function latestManifest(channel: UpdateChannel = 'stable', fetcher
|
|
|
31
31
|
}
|
|
32
32
|
export const latestRelease = async (channel: UpdateChannel = 'stable', fetcher = fetch) => (await latestManifest(channel, fetcher)).version as string;
|
|
33
33
|
export const DESKTOP_RELEASES = 'https://github.com/PapilotAI/papilot/releases';
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
34
|
+
// Nightly builds of main replace the assets of this one prerelease.
|
|
35
|
+
export const DESKTOP_DEV_TAG = 'desktop-dev';
|
|
36
|
+
// Release downloads, not the rate-limited REST API. The smoke test serves
|
|
37
|
+
// them locally.
|
|
38
|
+
const desktopDownloads = () => (process.env.PAPILOT_DESKTOP_UPDATE_SOURCE ? process.env.PAPILOT_DESKTOP_UPDATE_SOURCE.replace(/\/+$/, '') + '/PapilotAI/papilot/releases' : DESKTOP_RELEASES);
|
|
39
|
+
export interface DesktopRelease { version: string; downloadUrl: string; layer?: DesktopLayer }
|
|
40
|
+
// desktop.json (written by scripts/desktop-layer.mjs, uploaded after every
|
|
41
|
+
// other asset of its release) names the version, its update layer and the
|
|
42
|
+
// installer shell that layer runs on. Stable: the latest release. Dev: the
|
|
43
|
+
// latest nightly build of main. The layer is offered only to that shell.
|
|
44
|
+
export async function latestDesktopRelease(fetcher = fetch, channel: UpdateChannel = 'stable', fingerprint = ''): Promise<DesktopRelease | null> {
|
|
45
|
+
const folder = channel === 'dev' ? `download/${DESKTOP_DEV_TAG}` : 'latest/download';
|
|
46
|
+
let manifest: any;
|
|
47
|
+
try {
|
|
48
|
+
const bytes = await download(`${desktopDownloads()}/${folder}/desktop.json`, fetcher, 64 * 1024, true);
|
|
49
|
+
if (!bytes) return null;
|
|
50
|
+
manifest = JSON.parse(bytes.toString('utf8'));
|
|
51
|
+
} catch { throw new Error('Could not check for updates.'); }
|
|
52
|
+
const version = manifest?.version;
|
|
53
|
+
if (manifest?.format !== 1 || (channel === 'dev' ? !devRevision(version) : !stableVersion(version))) throw new Error('Invalid release information.');
|
|
54
|
+
const tag = channel === 'dev' ? DESKTOP_DEV_TAG : `v${version}`, layer = manifest.layer;
|
|
55
|
+
const usable = fingerprint && manifest.fingerprint === fingerprint && typeof layer?.name === 'string' && /^papilot-app-[\w.-]+\.tgz$/.test(layer.name)
|
|
56
|
+
&& Number.isSafeInteger(layer.size) && layer.size > 0 && /^[a-f0-9]{64}$/.test(layer.sha256);
|
|
57
|
+
return { version, downloadUrl: `${DESKTOP_RELEASES}/tag/${tag}`,
|
|
58
|
+
...(usable ? { layer: { url: `${desktopDownloads()}/download/${tag}/${layer.name}`, size: layer.size, sha256: layer.sha256 } } : {}) };
|
|
49
59
|
}
|
|
50
60
|
const validTarget = (version: unknown, channel: UpdateChannel): version is string => channel === 'dev' ? Boolean(devRevision(version)) : stableVersion(version);
|
|
51
61
|
const available = (next: string, current: string, channel: UpdateChannel) => validTarget(next, channel)
|
|
52
62
|
&& (channel === 'dev' || devRevision(current) ? next !== current : newerVersion(next, current));
|
|
53
63
|
interface Options {
|
|
54
64
|
desktop?: boolean;
|
|
55
|
-
|
|
65
|
+
// A packaged desktop app: its bundled server and shell fingerprint.
|
|
66
|
+
bundle?: { root: string; version: string; fingerprint: string };
|
|
67
|
+
latestDesktop?: (channel: UpdateChannel) => Promise<DesktopRelease | null>;
|
|
68
|
+
installLayer?: (data: string, version: string, layer: DesktopLayer, bundle: { root: string; fingerprint: string }) => Promise<string>;
|
|
56
69
|
root: string; data: string; version: string; port: number;
|
|
57
70
|
busy: () => boolean; shutdown: () => void;
|
|
58
71
|
latest?: (channel: UpdateChannel) => Promise<string | Record<string, any>>; install?: (data: string, version: string) => Promise<string>;
|
|
@@ -70,15 +83,23 @@ export class Updates {
|
|
|
70
83
|
private file: string;
|
|
71
84
|
private lock: string;
|
|
72
85
|
private channel: UpdateChannel;
|
|
73
|
-
private
|
|
86
|
+
private channelFile: string;
|
|
87
|
+
// Desktop: the release page when the update needs a new installer.
|
|
88
|
+
private downloadUrl?: string;
|
|
89
|
+
private target: DesktopRelease | null = null;
|
|
90
|
+
// Only packaged desktop apps install layers; the development app links to releases.
|
|
91
|
+
private layers: boolean;
|
|
74
92
|
constructor(private options: Options) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
|
|
93
|
+
const folder = path.join(options.data, options.desktop ? 'updates/desktop' : 'updates');
|
|
94
|
+
this.file = path.join(folder, 'status.json'); this.lock = path.join(folder, 'install.lock'); this.channelFile = path.join(folder, 'channel.json');
|
|
95
|
+
this.layers = Boolean(options.desktop && options.bundle?.fingerprint);
|
|
96
|
+
if (options.desktop && !this.layers) this.downloadUrl = DESKTOP_RELEASES;
|
|
97
|
+
const preference = readJson(this.channelFile)?.channel;
|
|
98
|
+
this.channel = options.desktop && !this.layers ? 'stable' : preference === 'dev' || preference === 'stable' ? preference : devRevision(options.version) ? 'dev' : 'stable';
|
|
99
|
+
let supported = options.desktop ? this.layers : options.supported ?? true;
|
|
79
100
|
if (!options.desktop && options.supported === undefined) try { npmCli(); } catch { supported = false; }
|
|
80
101
|
this.state = { currentVersion: options.version, latestVersion: null, phase: 'idle', checkedAt: null, error: null, canUpdate: supported };
|
|
81
|
-
const saved = options.desktop ? null : readJson(this.file);
|
|
102
|
+
const saved = options.desktop && !this.layers ? null : readJson(this.file);
|
|
82
103
|
if (saved && ['failed', 'updated', 'installing', 'waiting', 'restarting'].includes(saved.phase)) this.state = { ...this.state, ...saved, currentVersion: options.version, canUpdate: supported };
|
|
83
104
|
if (saved && activePhases.has(saved.phase) && !alive(saved.pid)) this.change({ phase: 'failed', checkedAt: Date.now(), error: 'The update was interrupted. Your current installation is still available; retry the update.' });
|
|
84
105
|
}
|
|
@@ -86,8 +107,8 @@ export class Updates {
|
|
|
86
107
|
const saved = readJson(this.file);
|
|
87
108
|
if (saved && activePhases.has(this.state.phase) && ['updated', 'failed'].includes(saved.phase)) this.state = { ...this.state, ...saved, currentVersion: this.options.version };
|
|
88
109
|
const { currentVersion, latestVersion, phase, checkedAt, error, canUpdate } = this.state, desktop = this.options.desktop;
|
|
89
|
-
if (desktop) return { currentVersion, latestVersion, phase, checkedAt, error,
|
|
90
|
-
return { currentVersion, latestVersion, phase, checkedAt, error, canUpdate, channel: this.channel };
|
|
110
|
+
if (desktop && !this.layers) return { currentVersion, latestVersion, phase, checkedAt, error, canUpdate: false, downloadUrl: this.downloadUrl };
|
|
111
|
+
return { currentVersion, latestVersion, phase, checkedAt, error, canUpdate, channel: this.channel, ...(this.downloadUrl ? { downloadUrl: this.downloadUrl } : {}) };
|
|
91
112
|
}
|
|
92
113
|
get restarting() { return this.snapshot().phase === 'restarting'; }
|
|
93
114
|
enter() { this.inFlight++; let released = false; return () => { if (!released) { released = true; this.inFlight--; } }; }
|
|
@@ -104,13 +125,14 @@ export class Updates {
|
|
|
104
125
|
writeJson(this.file, { ...this.state, pid: process.pid });
|
|
105
126
|
}
|
|
106
127
|
setChannel(channel: unknown) {
|
|
107
|
-
if (this.options.desktop) throw new Error('Download desktop updates from GitHub Releases.');
|
|
128
|
+
if (this.options.desktop && !this.layers) throw new Error('Download desktop updates from GitHub Releases.');
|
|
108
129
|
if (channel !== 'stable' && channel !== 'dev') throw new Error('Choose a valid update channel.');
|
|
109
130
|
if (activePhases.has(this.state.phase)) throw new Error('Wait for the current update to finish before changing channels.');
|
|
110
131
|
if (channel === this.channel) return this.check();
|
|
111
132
|
this.channel = channel;
|
|
112
|
-
writeJson(
|
|
113
|
-
this.checking = null;
|
|
133
|
+
writeJson(this.channelFile, { channel });
|
|
134
|
+
this.checking = null; this.target = null;
|
|
135
|
+
if (this.layers) this.downloadUrl = undefined;
|
|
114
136
|
this.change({ latestVersion: null, checkedAt: null, phase: 'idle', error: null });
|
|
115
137
|
return this.check();
|
|
116
138
|
}
|
|
@@ -119,12 +141,12 @@ export class Updates {
|
|
|
119
141
|
if (activePhases.has(this.state.phase)) return Promise.resolve(this.snapshot());
|
|
120
142
|
this.change({ phase: 'checking' });
|
|
121
143
|
const channel = this.channel;
|
|
122
|
-
const discovery = this.options.desktop ?
|
|
144
|
+
const discovery = this.options.desktop ? this.desktopRelease(channel) : (this.options.latest || latestManifest)(channel);
|
|
123
145
|
const checking = discovery.then(found => {
|
|
124
146
|
const manifest = typeof found === 'string' ? { version: found } : found;
|
|
125
147
|
if (!manifest && this.options.desktop) {
|
|
126
|
-
if (!this.closed && this.checking === checking) {
|
|
127
|
-
this.downloadUrl = DESKTOP_RELEASES;
|
|
148
|
+
if (!this.closed && this.checking === checking && !activePhases.has(this.state.phase)) {
|
|
149
|
+
this.target = null; this.downloadUrl = this.layers ? undefined : DESKTOP_RELEASES;
|
|
128
150
|
this.change({ latestVersion: null, checkedAt: Date.now(), error: null, phase: 'idle' });
|
|
129
151
|
}
|
|
130
152
|
return this.snapshot();
|
|
@@ -133,7 +155,12 @@ export class Updates {
|
|
|
133
155
|
if (!validTarget(version, channel)) throw new Error('Invalid release version.');
|
|
134
156
|
if (!this.closed && this.checking === checking && !activePhases.has(this.state.phase)) {
|
|
135
157
|
const newer = available(version, this.options.version, channel);
|
|
136
|
-
if (this.options.desktop)
|
|
158
|
+
if (this.options.desktop) {
|
|
159
|
+
const release = manifest as DesktopRelease, bundled = version === this.options.bundle?.version;
|
|
160
|
+
// A layer for this shell, or the installer's own server, installs in place.
|
|
161
|
+
this.target = this.layers && newer && (release.layer || bundled) ? release : null;
|
|
162
|
+
this.downloadUrl = this.layers ? newer && !this.target ? release.downloadUrl : undefined : release.downloadUrl;
|
|
163
|
+
}
|
|
137
164
|
this.change({ latestVersion: version, checkedAt: Date.now(), error: null, phase: newer ? 'available' : 'idle' });
|
|
138
165
|
}
|
|
139
166
|
return this.snapshot();
|
|
@@ -144,9 +171,23 @@ export class Updates {
|
|
|
144
171
|
this.checking = checking;
|
|
145
172
|
return this.checking;
|
|
146
173
|
}
|
|
174
|
+
private desktopRelease(channel: UpdateChannel) {
|
|
175
|
+
return (this.options.latestDesktop || (channel => latestDesktopRelease(fetch, channel, this.options.bundle?.fingerprint)))(channel);
|
|
176
|
+
}
|
|
177
|
+
// A retry after a restart finds the release again; the check that offered it
|
|
178
|
+
// happened in the previous process.
|
|
179
|
+
private async desktopLayer(version: string) {
|
|
180
|
+
const bundle = this.options.bundle!;
|
|
181
|
+
if (version === bundle.version) return bundle.root;
|
|
182
|
+
let release = this.target;
|
|
183
|
+
if (release?.version !== version) release = await this.desktopRelease(this.channel);
|
|
184
|
+
if (release?.version !== version || !release.layer) throw new Error('Check for a newer release before updating.');
|
|
185
|
+
return (this.options.installLayer || installLayer)(this.options.data, version, release.layer, bundle);
|
|
186
|
+
}
|
|
147
187
|
install(version: unknown) {
|
|
148
|
-
if (this.options.desktop) throw new Error('Download desktop updates from GitHub Releases.');
|
|
188
|
+
if (this.options.desktop && !this.layers) throw new Error('Download desktop updates from GitHub Releases.');
|
|
149
189
|
if (activePhases.has(this.state.phase)) return this.snapshot();
|
|
190
|
+
if (this.options.desktop && this.downloadUrl) throw new Error('Download desktop updates from GitHub Releases.');
|
|
150
191
|
if (!this.state.canUpdate) throw new Error('npm is unavailable to Papilot.');
|
|
151
192
|
if (!validTarget(version, this.channel) || version !== this.state.latestVersion || !available(version, this.options.version, this.channel)) throw new Error('Check for a newer release before updating.');
|
|
152
193
|
fs.mkdirSync(path.dirname(this.lock), { recursive: true, mode: 0o700 });
|
|
@@ -155,7 +196,9 @@ export class Updates {
|
|
|
155
196
|
if (previous) fs.rmSync(this.lock);
|
|
156
197
|
const fd = fs.openSync(this.lock, 'wx', 0o600); fs.writeFileSync(fd, JSON.stringify({ pid: process.pid })); fs.closeSync(fd);
|
|
157
198
|
this.change({ phase: 'installing', error: null });
|
|
158
|
-
|
|
199
|
+
// Returning to the installer's own version needs no download.
|
|
200
|
+
const installing = this.options.desktop ? this.desktopLayer(version) : (this.options.install || installRelease)(this.options.data, version);
|
|
201
|
+
void installing.then(root => {
|
|
159
202
|
if (this.closed) return;
|
|
160
203
|
this.prepared = root; this.change({ phase: 'waiting' });
|
|
161
204
|
return this.activate();
|
package/src/server/workspace.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { Codex, RpcResponseError } from './codex.ts';
|
|
|
12
12
|
import { AGENT_NAMES, AntigravityAgent, ClaudeAgent, GlmAgent, type AgentRuntime } from './agents.ts';
|
|
13
13
|
import { ACP_PROFILES, AcpAgent } from './acp.ts';
|
|
14
14
|
import { CursorAgent } from './cursor.ts';
|
|
15
|
-
import { reviewClient, taskRoute, confirmedReviewResponse, type ReviewClient } from './review.ts';
|
|
15
|
+
import { reviewClient, reviewConnectionError, taskRoute, confirmedReviewResponse, type ReviewClient } from './review.ts';
|
|
16
16
|
import { VERSION } from './paths.ts';
|
|
17
17
|
import { prepareRepository } from './imports.ts';
|
|
18
18
|
import { findSkill, type SkillFile } from './skills.ts';
|
|
@@ -668,6 +668,7 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
668
668
|
async reconcile(taskId: string) {
|
|
669
669
|
const task = this.store.task(taskId);
|
|
670
670
|
if (task.status === 'deleting') throw new Error('Retry deletion to confirm this task was removed.');
|
|
671
|
+
if (task.pendingBuild && !task.pendingReview) { void this.resumeBuild(taskId); return this.store.task(taskId); }
|
|
671
672
|
if (task.status !== 'uncertain') return task;
|
|
672
673
|
if (task.pendingReview) return this.reconcileReview(task);
|
|
673
674
|
const run = this.active.get(taskId) || this.store.runs().find(r => r.id === task.pendingRunId && r.taskId === taskId);
|
|
@@ -752,40 +753,100 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
752
753
|
if (this.reviewWrites.has(taskId)) throw new Error('Wait for the current review action before building a review.');
|
|
753
754
|
if (this.publishing.has(taskId) || (!automatic && (BUSY.has(task.status) || task.status === 'queued'))) throw new Error('This task is busy. Wait for it to finish or pause it first.');
|
|
754
755
|
this.publishing.add(taskId);
|
|
755
|
-
this.store.
|
|
756
|
-
|
|
756
|
+
const runs = this.store.runs().filter(run => run.taskId === taskId && run.status === 'completed' && (run.reviewRound === 0 || (run.reviewRound === undefined && run.id === after?.id))).map(run => run.id);
|
|
757
|
+
this.store.updateTask(taskId, { status: 'building', error: null, pendingBuild: { requestId: randomUUID(), runs, sendBack: Boolean(after && !after.requestKey.startsWith(BUILD_FIX)), restarts: 0 } });
|
|
758
|
+
return this.buildReview(task).finally(() => { this.publishing.delete(taskId); this.schedule(); });
|
|
757
759
|
}
|
|
758
|
-
|
|
760
|
+
// Resume a persisted job, including jobs saved by versions before pendingBuild.
|
|
761
|
+
async resumeBuild(taskId: string) {
|
|
762
|
+
const task = this.store.task(taskId);
|
|
763
|
+
if (this.closing || this.publishing.has(taskId) || task.mode === 'review' || task.pendingReview || this.active.has(taskId)
|
|
764
|
+
|| !['building', 'failed', 'interrupted', 'uncertain'].includes(task.status)) return false;
|
|
765
|
+
let pending = task.pendingBuild;
|
|
766
|
+
if (!pending) {
|
|
767
|
+
const job = task.review?.job;
|
|
768
|
+
if (!job?.id || !['running', 'queued'].includes(job.status)
|
|
769
|
+
|| (task.status !== 'building' && !/fetch failed|server restarted during the PDF build/i.test(task.error || ''))) return false;
|
|
770
|
+
pending = { requestId: randomUUID(), job: job.id, runs: this.store.runs().filter(run => run.taskId === taskId && run.status === 'completed' && run.reviewRound === 0).map(run => run.id), sendBack: false, restarts: 0 };
|
|
771
|
+
}
|
|
772
|
+
this.publishing.add(taskId);
|
|
773
|
+
this.store.updateTask(taskId, { status: 'building', error: null, pendingBuild: pending });
|
|
774
|
+
try { await this.buildReview(task, true); }
|
|
775
|
+
finally { this.publishing.delete(taskId); this.schedule(); }
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
private async buildReview(task: Task, resume = false) {
|
|
759
779
|
const taskId = task.id;
|
|
760
|
-
|
|
780
|
+
let pending = this.store.task(taskId).pendingBuild!;
|
|
781
|
+
let client: ReviewClient | undefined;
|
|
782
|
+
let reuse = resume;
|
|
783
|
+
// Only connection failures are retried. Every submission has a persisted key,
|
|
784
|
+
// so losing its response cannot submit a second build.
|
|
785
|
+
const request = async <T>(work: (client: ReviewClient) => Promise<T>, submitting = false): Promise<T> => {
|
|
786
|
+
for (let attempt = 0; ; attempt++) {
|
|
787
|
+
if (this.closing) throw new Error();
|
|
788
|
+
try {
|
|
789
|
+
client ??= await this.connectReview(this.store.project(task.projectId).path, reuse);
|
|
790
|
+
const value = await work(client);
|
|
791
|
+
if (this.closing) throw new Error();
|
|
792
|
+
return value;
|
|
793
|
+
} catch (error) {
|
|
794
|
+
if (this.closing || !reviewConnectionError(error) || attempt >= 3) throw error;
|
|
795
|
+
console.warn('PDF comparison connection interrupted:', task.name, pending.job || pending.requestId, error);
|
|
796
|
+
client = undefined; reuse = !submitting;
|
|
797
|
+
await delay(1500 * 2 ** attempt);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
};
|
|
761
801
|
try {
|
|
762
|
-
const client = await this.connectReview(this.store.project(task.projectId).path);
|
|
763
|
-
const job = await client.post(`${taskRoute(task.name)}/publish`, {});
|
|
764
802
|
for (;;) {
|
|
765
803
|
if (this.closing) return;
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
804
|
+
if (!pending.job) {
|
|
805
|
+
// New writes require our engine version, which understands request IDs.
|
|
806
|
+
// Read-only recovery may have connected to an older installed engine.
|
|
807
|
+
client = undefined; reuse = false;
|
|
808
|
+
const job = await request(client => client.post<{ job: string }>(`${taskRoute(task.name)}/publish`, { requestId: pending.requestId }), true);
|
|
809
|
+
pending = { ...pending, job: job.job };
|
|
810
|
+
this.store.updateTask(taskId, { pendingBuild: pending });
|
|
811
|
+
}
|
|
812
|
+
const current = await request(client => client.get(`/api/jobs/${pending.job}`));
|
|
813
|
+
const summary = await request(client => client.get(taskRoute(task.name)));
|
|
814
|
+
this.store.updateTask(taskId, { review: { ...this.store.task(taskId).review, state: summary.state, round: summary.round, counts: summary.counts, job: { ...current, id: pending.job } } });
|
|
769
815
|
if (current.status === 'done' || current.status === 'completed' || current.status === 'succeeded') {
|
|
770
816
|
// Freeze the result round for these instructions. Later follow-ups
|
|
771
817
|
// have their own results and cannot silently replace these links.
|
|
772
|
-
|
|
818
|
+
this.store.transaction(() => {
|
|
819
|
+
for (const run of this.store.runs().filter(run => pending.runs.includes(run.id))) this.store.saveRun({ ...run, reviewRound: current.outcome === 'empty' || current.outcome === 'unchanged' ? null : current.round ?? summary.round ?? null });
|
|
820
|
+
this.store.updateTask(taskId, { status: this.store.runs().some(r => r.taskId === taskId && r.status === 'queued') ? 'queued' : 'reviewing', error: null, pendingBuild: null });
|
|
821
|
+
});
|
|
773
822
|
break;
|
|
774
823
|
}
|
|
775
824
|
if (current.status === 'failed') {
|
|
825
|
+
if (current.error === 'The review server restarted during the build.' && pending.restarts < 1) {
|
|
826
|
+
pending = { ...pending, job: undefined, requestId: randomUUID(), restarts: pending.restarts + 1 };
|
|
827
|
+
this.store.updateTask(taskId, { pendingBuild: pending });
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
776
830
|
const failure: BuildFailure = { error: current.error || 'PDF build failed.', details: current.details || [], ...(current.log ? { log: current.log } : {}) };
|
|
777
831
|
// Instructions the author queued meanwhile go first; the build after them sends any error that remains.
|
|
778
|
-
if (sendBack && FIXABLE.test(failure.error) && await this.enqueue(taskId, '', BUILD_FIX +
|
|
779
|
-
this.store.updateTask(taskId, { status: 'queued', error: null });
|
|
832
|
+
if (pending.sendBack && FIXABLE.test(failure.error) && await this.enqueue(taskId, '', BUILD_FIX + pending.job, false, { build: failure }).then(() => true, () => false)) {
|
|
833
|
+
this.store.updateTask(taskId, { status: 'queued', error: null, pendingBuild: null });
|
|
780
834
|
return;
|
|
781
835
|
}
|
|
782
836
|
throw new Error([failure.error, ...failure.details].join('\n'));
|
|
783
837
|
}
|
|
784
838
|
await delay(1500);
|
|
785
839
|
}
|
|
786
|
-
this.store.updateTask(taskId, { status: this.store.runs().some(r => r.taskId === taskId && r.status === 'queued') ? 'queued' : 'reviewing', error: null });
|
|
787
840
|
await this.refresh(taskId);
|
|
788
|
-
} catch (e) {
|
|
841
|
+
} catch (e) {
|
|
842
|
+
if (this.closing) return;
|
|
843
|
+
if (reviewConnectionError(e)) {
|
|
844
|
+
console.warn('PDF comparison recovery pending:', task.name, pending.job || pending.requestId, e);
|
|
845
|
+
this.store.updateTask(taskId, { status: 'uncertain', pendingBuild: pending,
|
|
846
|
+
error: 'The PDF comparison service could not be reached. Your edits are saved. Select Check status to recover the existing build.' });
|
|
847
|
+
}
|
|
848
|
+
else this.store.updateTask(taskId, { status: 'failed', pendingBuild: null, error: errorText(e) });
|
|
849
|
+
}
|
|
789
850
|
}
|
|
790
851
|
private finishAiReview(task: Task, run: Run, turnId: string) {
|
|
791
852
|
try {
|
package/src/shared.ts
CHANGED
|
@@ -19,7 +19,8 @@ export interface UpdateStatus {
|
|
|
19
19
|
// unsent counts review updates the agent has not received; comments are the
|
|
20
20
|
// ones that ask for a revision. files are the manuscript files this task's
|
|
21
21
|
// accepted changes touched since its last commit, relative to the repository root.
|
|
22
|
-
export interface ReviewSummary { state: string; round: number | null; counts: Record<string, number>; unsent?: number; comments?: number; files?: string[]; next?: string; job?: { phase: string; status: string; error?: string; details?: string[] } | null }
|
|
22
|
+
export interface ReviewSummary { state: string; round: number | null; counts: Record<string, number>; unsent?: number; comments?: number; files?: string[]; next?: string; job?: { id?: string; phase: string; status: string; error?: string; details?: string[] } | null }
|
|
23
|
+
export interface PendingBuild { requestId: string; job?: string; runs: string[]; sendBack: boolean; restarts: number }
|
|
23
24
|
export interface PendingReview { route: string; body: Record<string, unknown>; previousStatus: TaskStatus }
|
|
24
25
|
export interface TaskCommitReview { token: string; files: string[]; diff: string }
|
|
25
26
|
export interface TaskReconciliation {
|
|
@@ -44,6 +45,8 @@ export interface Task {
|
|
|
44
45
|
error: string | null; review: ReviewSummary | null;
|
|
45
46
|
pauseVersion?: number; pendingRunId?: string | null;
|
|
46
47
|
pendingReview?: PendingReview | null;
|
|
48
|
+
// Persist before submission so reconnects resume the same build and run links.
|
|
49
|
+
pendingBuild?: PendingBuild | null;
|
|
47
50
|
// The task's last commit from its review, and the accepted text it included.
|
|
48
51
|
committed?: { hash: string; accepted: string; at: number; pushed?: boolean } | null;
|
|
49
52
|
}
|
|
@@ -113,7 +116,8 @@ export interface Person { id: string; name: string; role: 'owner' | 'member'; jo
|
|
|
113
116
|
export interface Device { id: string; kind: 'passkey' | 'session'; label: string; createdAt: number; lastUsedAt: number | null }
|
|
114
117
|
export interface People { origin: string | null; people: Person[] }
|
|
115
118
|
// staged: the index has changes to this file; unstaged: the working tree has more.
|
|
116
|
-
|
|
119
|
+
// index is the staged status where it differs from status, which is the working tree's when it has changes.
|
|
120
|
+
export interface GitFile { path: string; status: string; staged: boolean; unstaged: boolean; lines?: [number, number]; from?: string; index?: string }
|
|
117
121
|
// merging is a merge in progress, with the files whose conflict markers remain.
|
|
118
122
|
export interface GitStatus { branch: string | null; upstream: string | null; ahead: number; behind: number; files: GitFile[]; head: { hash: string; subject: string; date: number } | null; remote?: boolean; merging?: { message: string; conflicts: string[] } | null }
|
|
119
123
|
export interface GitDiff { path: string; diff: string; truncated: boolean }
|
|
@@ -122,7 +126,8 @@ export interface GitWorktree {
|
|
|
122
126
|
locked?: string; prunable?: string; status: GitStatus | null; error?: string;
|
|
123
127
|
taskId?: string;
|
|
124
128
|
}
|
|
125
|
-
|
|
129
|
+
// refs are local branches; remote branches and tags are listed apart, as an editor's graph shows them.
|
|
130
|
+
export interface GitGraphCommit { hash: string; parents: string[]; subject: string; author: string; date: number; refs: string[]; remotes?: string[]; tags?: string[] }
|
|
126
131
|
export interface GitOverview { worktrees: GitWorktree[]; commits: GitGraphCommit[]; truncated: boolean }
|
|
127
132
|
// A running compilation, from latexmk's output: its step, the TeX pass, and
|
|
128
133
|
// the pages typeset in that pass against the last known page count.
|