prreviewbuddy 0.43.0 → 0.45.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/CHANGELOG.md +70 -0
- package/dist/{delete_review-DPRL5IPt.js → delete_review-BWMmO81Q.js} +257 -145
- package/dist/main.js +95 -14
- package/dist/server.js +221 -82
- package/package.json +1 -1
- package/static/shell.css +210 -1
- package/static/sidebar.css +58 -14
- package/static/workspace.js +35 -35
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.45.0
|
|
4
|
+
|
|
5
|
+
**Expand a pane to fill the review page.** Buddy AI, Issues, Questions, Files and the review path
|
|
6
|
+
each have an expand button at the right of their title (on the review path, beside the theme count).
|
|
7
|
+
The pane you expand takes the whole review page below the header, which still shows the pull
|
|
8
|
+
request, its branch and its freshness. The collapse button, Escape or your browser's Back button puts
|
|
9
|
+
the page back as it was, and Forward expands it again. Nothing in the pane is reloaded on the way in
|
|
10
|
+
or out, so a conversation, an open file and your scroll position are all still there. An expanded
|
|
11
|
+
pane is part of the page's address, so a reload keeps it. With the review path expanded, its themes
|
|
12
|
+
are listed above the current one, and moving between them keeps it expanded.
|
|
13
|
+
|
|
14
|
+
**Ask Buddy about a whole issue or question.** Every issue and question card now has a sparkle icon
|
|
15
|
+
next to Copy. It opens the same questions you get by selecting text: **Explain**, **Explain simply**,
|
|
16
|
+
**Is this actually a problem?**, **Why does this matter?**, **Suggest a better implementation** and
|
|
17
|
+
**Generate a question for the author**, plus your own prompts. Buddy AI answers about the entire
|
|
18
|
+
finding straight away, in the pane beside the review. **Ask a question** and your own prompts put the
|
|
19
|
+
finding in the question box for you to finish instead. Selecting text works as before: whatever you
|
|
20
|
+
pick goes into the question box, and nothing is sent until you send it.
|
|
21
|
+
|
|
22
|
+
**Explain and Explain simply are now two different questions.** In the review page's menus,
|
|
23
|
+
**Explain** gives a full explanation with the technical detail kept, and **Explain simply** asks for
|
|
24
|
+
the plain version that **Explain** used to give.
|
|
25
|
+
|
|
26
|
+
**Starter prompts.** If you have never saved a prompt, **Your prompts** now starts with five:
|
|
27
|
+
**Teach me this**, **What could go wrong?**, **Walk me through it**, **What should I verify?** and
|
|
28
|
+
**What does it assume?**. They are ordinary prompts that you can edit or delete, and a prompt you delete
|
|
29
|
+
does not come back. If you have ever saved a prompt, your list is left exactly as it is.
|
|
30
|
+
|
|
31
|
+
**Your prompts can be folded away.** Click **Your prompts** in Buddy AI to hide the prompts and show
|
|
32
|
+
only how many you have, and click it again to show them. **Add** stays visible either way, and every
|
|
33
|
+
review remembers your choice. The suggested questions Buddy AI makes for each review are not affected.
|
|
34
|
+
|
|
35
|
+
**The address always names the theme you are reading.** Opening the review path now shows its
|
|
36
|
+
address as the theme it opens on, and a theme tag on a card changes the address too. Back and Forward
|
|
37
|
+
step through the themes you actually read, instead of going back to the review path while still
|
|
38
|
+
showing the theme you left.
|
|
39
|
+
|
|
40
|
+
**Review summaries on the Reviews page show code as code.** Names the review wrote in backticks now
|
|
41
|
+
appear as inline code instead of showing the backticks.
|
|
42
|
+
|
|
43
|
+
## 0.44.0
|
|
44
|
+
|
|
45
|
+
**`prreviewbuddy review --queue` puts the reviews it starts on your review queue.** Add `--queue`
|
|
46
|
+
to any review command, such as `prreviewbuddy review --pr <url> <url> --queue`, and each pull request's
|
|
47
|
+
review goes on your review queue as soon as the review exists, without waiting for the analysis to
|
|
48
|
+
finish. The cards are already waiting under **Queue** on the Reviews page while they are still being
|
|
49
|
+
analysed, and a Reviews page you have open picks them up by itself. It works for one pull request or
|
|
50
|
+
several, for `--branch` and `--commit`, and beside `--fresh`, `--update` and `--reanalyse`. A review
|
|
51
|
+
already in the queue keeps its place. A review you have marked as reviewed is left out and keeps its
|
|
52
|
+
status: the command says so, and you can move it back to In progress on the page to queue it. When
|
|
53
|
+
you review several pull requests at once, the summary at the end lists what is in your queue, and
|
|
54
|
+
they are queued in the order their reviews were created, which isn't always the order you typed
|
|
55
|
+
them. Without `--queue`, nothing changes.
|
|
56
|
+
|
|
57
|
+
**The review queue no longer loses changes made at the same time.** With `--queue` there are two
|
|
58
|
+
things writing your review queue, the terminal and the Reviews page, and a change made in one while
|
|
59
|
+
the other was saving could be lost, or in the worst case empty the queue. Queue changes now happen
|
|
60
|
+
one at a time across every process on your machine, and the queue file is never left half-written.
|
|
61
|
+
|
|
62
|
+
**Copy has moved to the top of issue and question cards.** The button that copies a finding as
|
|
63
|
+
Markdown, ready to paste into a GitHub comment, used to sit at the bottom of the card beside
|
|
64
|
+
**Dismiss** and **Mark answered**. It is now a copy icon at the right of the card's title, because it
|
|
65
|
+
copies the whole finding, and hovering it says **Copy finding** or **Copy question**. After you
|
|
66
|
+
click it, it says **Copied** for a moment. The bottom of a card now holds only the actions that
|
|
67
|
+
change your review: **Investigate**, **Dismiss** and **Mark answered**.
|
|
68
|
+
|
|
69
|
+
**A clearer error when a pull request comes after another flag.** `prreviewbuddy review --pr <url>
|
|
70
|
+
--queue <url>` used to answer with a message about the old command syntax. It now says which flag
|
|
71
|
+
the second URL came after, and to put all the URLs together after `--pr`.
|
|
72
|
+
|
|
3
73
|
## 0.43.0
|
|
4
74
|
|
|
5
75
|
**A review page shows when its pull request has merged or closed.** The **Merged** and **PR closed**
|
|
@@ -3802,6 +3802,133 @@ function doneVerb(counts) {
|
|
|
3802
3802
|
return counts.reviewerClosed > 0 ? "dealt with" : "resolved";
|
|
3803
3803
|
}
|
|
3804
3804
|
//#endregion
|
|
3805
|
+
//#region ../../packages/review-harness/src/workspace/write_lock.ts
|
|
3806
|
+
/**
|
|
3807
|
+
* One lock file per name, taken exclusively, so a read-change-write in one process cannot land in
|
|
3808
|
+
* the middle of another's.
|
|
3809
|
+
*
|
|
3810
|
+
* Lifted out of `store.ts` unchanged, so the review queue can use it too. `queue.ts` cannot import
|
|
3811
|
+
* the store (the store's prune imports the queue, and the reverse import would close the cycle), and
|
|
3812
|
+
* a second lock written for the queue alone would be a second set of rules about stale holders to
|
|
3813
|
+
* get right. This module depends on `store_root.ts` and on Node, nothing else, for the same reason.
|
|
3814
|
+
*
|
|
3815
|
+
* Names share one directory. A review is locked under its id, a UUID, so a fixed word such as
|
|
3816
|
+
* `queue` can never collide with one.
|
|
3817
|
+
*/
|
|
3818
|
+
/** Raised when another process has held a review's write lock for longer than any write takes. */
|
|
3819
|
+
var WorkspaceWriteBusyError = class extends Error {
|
|
3820
|
+
constructor() {
|
|
3821
|
+
super("This review is being written by another process. Try again in a moment.");
|
|
3822
|
+
this.name = "WorkspaceWriteBusyError";
|
|
3823
|
+
}
|
|
3824
|
+
};
|
|
3825
|
+
var WRITE_LOCKS_DIR = join(STORE_ROOT, "locks");
|
|
3826
|
+
/** Every write under the lock is one read and one rename, milliseconds. This is a hang, not a queue. */
|
|
3827
|
+
var WRITE_LOCK_WAIT_MS = 5e3;
|
|
3828
|
+
/**
|
|
3829
|
+
* A lock this old is broken whoever holds it.
|
|
3830
|
+
*
|
|
3831
|
+
* Every hold is synchronous and lasts one read and one rename, so no live writer is anywhere near
|
|
3832
|
+
* this. It is the fallback for what a pid cannot say: a holder on another machine, a lock not yet
|
|
3833
|
+
* written out, and a pid that died and was reused by some unrelated long-lived process, which a
|
|
3834
|
+
* liveness check alone would read as the holder still working, for ever.
|
|
3835
|
+
*/
|
|
3836
|
+
var WRITE_LOCK_STALE_MS = 3e4;
|
|
3837
|
+
var heldWriteLocks = /* @__PURE__ */ new Map();
|
|
3838
|
+
var sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
3839
|
+
/**
|
|
3840
|
+
* One review's writes, one at a time across every process on the machine.
|
|
3841
|
+
*
|
|
3842
|
+
* The CLI and the workspace server are separate processes over one store. Without this, each could
|
|
3843
|
+
* load the same review, change its own fields and rename its result into place, and whichever
|
|
3844
|
+
* renamed last would discard the other: an update finishing in the terminal would put back a
|
|
3845
|
+
* question the reviewer had just ticked on the page. A file created exclusively is the lock, so it
|
|
3846
|
+
* holds between processes and needs nothing running to arbitrate.
|
|
3847
|
+
*
|
|
3848
|
+
* Synchronous, including the wait, because every write it guards is. It is held for a read and a
|
|
3849
|
+
* rename, so a wait is milliseconds; one that runs to `WRITE_LOCK_WAIT_MS` means a holder has hung,
|
|
3850
|
+
* and that is raised rather than written through. Re-entrant within a process, so a change made
|
|
3851
|
+
* under it can save without deadlocking itself. A lock left by a process that died is broken: at
|
|
3852
|
+
* once when its holder is on this machine and gone, and after `WRITE_LOCK_STALE_MS` in every case,
|
|
3853
|
+
* because a pid can be reused and a live one proves nothing about who took the lock.
|
|
3854
|
+
*/
|
|
3855
|
+
function withWriteLock(id, fn) {
|
|
3856
|
+
const depth = heldWriteLocks.get(id) ?? 0;
|
|
3857
|
+
if (depth > 0) {
|
|
3858
|
+
heldWriteLocks.set(id, depth + 1);
|
|
3859
|
+
try {
|
|
3860
|
+
return fn();
|
|
3861
|
+
} finally {
|
|
3862
|
+
heldWriteLocks.set(id, depth);
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
mkdirSync(WRITE_LOCKS_DIR, { recursive: true });
|
|
3866
|
+
const path = join(WRITE_LOCKS_DIR, `${id}.lock`);
|
|
3867
|
+
const mine = {
|
|
3868
|
+
token: randomUUID(),
|
|
3869
|
+
pid: process.pid,
|
|
3870
|
+
host: hostname(),
|
|
3871
|
+
at: Date.now()
|
|
3872
|
+
};
|
|
3873
|
+
const deadline = Date.now() + WRITE_LOCK_WAIT_MS;
|
|
3874
|
+
for (;;) {
|
|
3875
|
+
try {
|
|
3876
|
+
const fd = openSync(path, "wx", 384);
|
|
3877
|
+
try {
|
|
3878
|
+
writeSync(fd, JSON.stringify(mine));
|
|
3879
|
+
} finally {
|
|
3880
|
+
closeSync(fd);
|
|
3881
|
+
}
|
|
3882
|
+
break;
|
|
3883
|
+
} catch (error) {
|
|
3884
|
+
if (error.code !== "EEXIST") throw error;
|
|
3885
|
+
}
|
|
3886
|
+
if (breakIfAbandoned(path)) continue;
|
|
3887
|
+
if (Date.now() >= deadline) throw new WorkspaceWriteBusyError();
|
|
3888
|
+
Atomics.wait(sleeper, 0, 0, 2);
|
|
3889
|
+
}
|
|
3890
|
+
heldWriteLocks.set(id, 1);
|
|
3891
|
+
try {
|
|
3892
|
+
return fn();
|
|
3893
|
+
} finally {
|
|
3894
|
+
heldWriteLocks.delete(id);
|
|
3895
|
+
if (readHolder(path)?.token === mine.token) try {
|
|
3896
|
+
unlinkSync(path);
|
|
3897
|
+
} catch {}
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
function readHolder(path) {
|
|
3901
|
+
try {
|
|
3902
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
3903
|
+
} catch {
|
|
3904
|
+
return null;
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
/** Removes a lock whose holder is gone, and says whether it did. */
|
|
3908
|
+
function breakIfAbandoned(path) {
|
|
3909
|
+
const holder = readHolder(path);
|
|
3910
|
+
let age = 0;
|
|
3911
|
+
try {
|
|
3912
|
+
age = Date.now() - Math.min(statSync(path).mtimeMs, holder?.at ?? Infinity);
|
|
3913
|
+
} catch {
|
|
3914
|
+
return true;
|
|
3915
|
+
}
|
|
3916
|
+
if (!(age > WRITE_LOCK_STALE_MS || holder !== null && holder.host === hostname() && !processAlive(holder.pid))) return false;
|
|
3917
|
+
if (readHolder(path)?.token !== holder?.token) return true;
|
|
3918
|
+
try {
|
|
3919
|
+
unlinkSync(path);
|
|
3920
|
+
} catch {}
|
|
3921
|
+
return true;
|
|
3922
|
+
}
|
|
3923
|
+
function processAlive(pid) {
|
|
3924
|
+
try {
|
|
3925
|
+
process.kill(pid, 0);
|
|
3926
|
+
return true;
|
|
3927
|
+
} catch (error) {
|
|
3928
|
+
return error.code === "EPERM";
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
//#endregion
|
|
3805
3932
|
//#region ../../packages/review-harness/src/workspace/queue.ts
|
|
3806
3933
|
/**
|
|
3807
3934
|
* The review queue: the set the reviewer is working through now, in the order they chose.
|
|
@@ -3823,6 +3950,11 @@ function doneVerb(counts) {
|
|
|
3823
3950
|
*/
|
|
3824
3951
|
var QUEUE_PATH = join(STORE_ROOT, "queue.json");
|
|
3825
3952
|
/**
|
|
3953
|
+
* The name the queue is locked under. Reviews are locked under their UUIDs in the same directory,
|
|
3954
|
+
* so a plain word cannot collide with one.
|
|
3955
|
+
*/
|
|
3956
|
+
var QUEUE_LOCK = "queue";
|
|
3957
|
+
/**
|
|
3826
3958
|
* An object rather than a bare array, because an array is the one JSON shape that cannot grow a
|
|
3827
3959
|
* sibling field, and "when was this queued" is a plausible second column.
|
|
3828
3960
|
*
|
|
@@ -3849,31 +3981,65 @@ function readQueue() {
|
|
|
3849
3981
|
}
|
|
3850
3982
|
}
|
|
3851
3983
|
/**
|
|
3852
|
-
*
|
|
3984
|
+
* Change the queue as it is on disk now: read, change, write, with nothing able to land in between.
|
|
3985
|
+
*
|
|
3986
|
+
* **The only way the queue is written.** The workspace server and the CLI are separate processes
|
|
3987
|
+
* over this one file, and a `--queue` from the terminal can arrive while somebody is adding or
|
|
3988
|
+
* removing on the page. Each of them reading the queue, changing its copy and writing it back would
|
|
3989
|
+
* keep whichever wrote last and drop the other's change -- while both reported success. So the
|
|
3990
|
+
* whole read-change-write runs under the queue's cross-process lock, the same lock a review's own
|
|
3991
|
+
* writes take (see `write_lock.ts`).
|
|
3853
3992
|
*
|
|
3854
|
-
*
|
|
3855
|
-
*
|
|
3856
|
-
*
|
|
3993
|
+
* One lock, taken once, here. Every mutation below is one call to this and nothing inside `change`
|
|
3994
|
+
* writes, so the transaction is visibly this function's body and nothing depends on the lock being
|
|
3995
|
+
* re-entrant. `change` may read other files -- `queue_live.ts` asks which reviews still exist --
|
|
3996
|
+
* but must not write the queue or take another lock.
|
|
3857
3997
|
*
|
|
3858
|
-
* `
|
|
3859
|
-
*
|
|
3860
|
-
*
|
|
3861
|
-
* so the caller has to be able to tell the reviewer it did not happen.
|
|
3998
|
+
* `change` returns the queue to store, or null to store nothing, in which case the queue as it was
|
|
3999
|
+
* read is the answer. Null from this function means the write failed, including a lock held past
|
|
4000
|
+
* any real write, which is the same thing to the caller: it did not happen.
|
|
3862
4001
|
*/
|
|
3863
|
-
function
|
|
3864
|
-
const next = normalise(ids);
|
|
4002
|
+
function updateQueue(change) {
|
|
3865
4003
|
try {
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
}
|
|
3871
|
-
return next;
|
|
4004
|
+
return withWriteLock(QUEUE_LOCK, () => {
|
|
4005
|
+
const current = readQueue();
|
|
4006
|
+
const next = change(current);
|
|
4007
|
+
return next === null ? current : writeQueueUnlocked(next);
|
|
4008
|
+
});
|
|
3872
4009
|
} catch {
|
|
3873
4010
|
return null;
|
|
3874
4011
|
}
|
|
3875
4012
|
}
|
|
3876
4013
|
/**
|
|
4014
|
+
* The write itself, for `updateQueue` only: the caller holds the lock.
|
|
4015
|
+
*
|
|
4016
|
+
* To a temporary file and renamed into place, because a rename is atomic and a write is not.
|
|
4017
|
+
* `writeFileSync` empties the file before it fills it, and `readQueue` reads a file it cannot parse
|
|
4018
|
+
* as "nothing queued" -- so a reader in another process that caught it half-written would take the
|
|
4019
|
+
* queue to be empty and, on its next write, make it so. Found doing exactly that: four processes
|
|
4020
|
+
* adding twelve reviews each left one in the queue. The temporary name carries the pid so two
|
|
4021
|
+
* processes never share one, although under the lock they never write at once anyway.
|
|
4022
|
+
*/
|
|
4023
|
+
function writeQueueUnlocked(ids) {
|
|
4024
|
+
const next = normalise(ids);
|
|
4025
|
+
mkdirSync(STORE_ROOT, { recursive: true });
|
|
4026
|
+
const stored = {
|
|
4027
|
+
version: VERSION,
|
|
4028
|
+
ids: next
|
|
4029
|
+
};
|
|
4030
|
+
const temporary = `${QUEUE_PATH}.${process.pid}.tmp`;
|
|
4031
|
+
try {
|
|
4032
|
+
writeFileSync(temporary, `${JSON.stringify(stored, null, 2)}\n`);
|
|
4033
|
+
renameSync(temporary, QUEUE_PATH);
|
|
4034
|
+
} catch (error) {
|
|
4035
|
+
try {
|
|
4036
|
+
unlinkSync(temporary);
|
|
4037
|
+
} catch {}
|
|
4038
|
+
throw error;
|
|
4039
|
+
}
|
|
4040
|
+
return next;
|
|
4041
|
+
}
|
|
4042
|
+
/**
|
|
3877
4043
|
* Append, and leave a review already queued exactly where it is.
|
|
3878
4044
|
*
|
|
3879
4045
|
* Adding something twice is a no-op rather than a move. Nothing in this UI reorders a queue, so a
|
|
@@ -3881,9 +4047,7 @@ function writeQueue(ids) {
|
|
|
3881
4047
|
* and it would do it without ever saying so.
|
|
3882
4048
|
*/
|
|
3883
4049
|
function queueAdd(id) {
|
|
3884
|
-
|
|
3885
|
-
if (current.includes(id)) return current;
|
|
3886
|
-
return writeQueue([...current, id]);
|
|
4050
|
+
return updateQueue((current) => current.includes(id) ? null : [...current, id]);
|
|
3887
4051
|
}
|
|
3888
4052
|
/**
|
|
3889
4053
|
* Append several, in the order given, for the reviews index's bulk bar.
|
|
@@ -3894,24 +4058,20 @@ function queueAdd(id) {
|
|
|
3894
4058
|
* named in `full`, and the caller has something true to say about it.
|
|
3895
4059
|
*/
|
|
3896
4060
|
function queueAddMany(ids) {
|
|
3897
|
-
|
|
3898
|
-
const
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
};
|
|
3905
|
-
const queue = writeQueue([...current, ...fresh.slice(0, room)]);
|
|
4061
|
+
let full = [];
|
|
4062
|
+
const queue = updateQueue((current) => {
|
|
4063
|
+
const fresh = ids.filter((id, at) => !current.includes(id) && ids.indexOf(id) === at);
|
|
4064
|
+
const room = Math.max(0, 50 - current.length);
|
|
4065
|
+
full = fresh.slice(room);
|
|
4066
|
+
return fresh.length <= full.length ? null : [...current, ...fresh.slice(0, room)];
|
|
4067
|
+
});
|
|
3906
4068
|
return queue === null ? null : {
|
|
3907
4069
|
queue,
|
|
3908
4070
|
full
|
|
3909
4071
|
};
|
|
3910
4072
|
}
|
|
3911
4073
|
function queueRemove(id) {
|
|
3912
|
-
|
|
3913
|
-
if (!current.includes(id)) return current;
|
|
3914
|
-
return writeQueue(current.filter((queued) => queued !== id));
|
|
4074
|
+
return updateQueue((current) => current.includes(id) ? current.filter((queued) => queued !== id) : null);
|
|
3915
4075
|
}
|
|
3916
4076
|
/**
|
|
3917
4077
|
* The invariant, as one named thing: the queue holds unfinished work only.
|
|
@@ -4042,119 +4202,6 @@ function saveWorkspace(workspace) {
|
|
|
4042
4202
|
writeWorkspace(workspace);
|
|
4043
4203
|
});
|
|
4044
4204
|
}
|
|
4045
|
-
/** Raised when another process has held a review's write lock for longer than any write takes. */
|
|
4046
|
-
var WorkspaceWriteBusyError = class extends Error {
|
|
4047
|
-
constructor() {
|
|
4048
|
-
super("This review is being written by another process. Try again in a moment.");
|
|
4049
|
-
this.name = "WorkspaceWriteBusyError";
|
|
4050
|
-
}
|
|
4051
|
-
};
|
|
4052
|
-
var WRITE_LOCKS_DIR = join(STORE_ROOT, "locks");
|
|
4053
|
-
/** Every write under the lock is one read and one rename, milliseconds. This is a hang, not a queue. */
|
|
4054
|
-
var WRITE_LOCK_WAIT_MS = 5e3;
|
|
4055
|
-
/**
|
|
4056
|
-
* A lock this old is broken whoever holds it.
|
|
4057
|
-
*
|
|
4058
|
-
* Every hold is synchronous and lasts one read and one rename, so no live writer is anywhere near
|
|
4059
|
-
* this. It is the fallback for what a pid cannot say: a holder on another machine, a lock not yet
|
|
4060
|
-
* written out, and a pid that died and was reused by some unrelated long-lived process, which a
|
|
4061
|
-
* liveness check alone would read as the holder still working, for ever.
|
|
4062
|
-
*/
|
|
4063
|
-
var WRITE_LOCK_STALE_MS = 3e4;
|
|
4064
|
-
var heldWriteLocks = /* @__PURE__ */ new Map();
|
|
4065
|
-
var sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
4066
|
-
/**
|
|
4067
|
-
* One review's writes, one at a time across every process on the machine.
|
|
4068
|
-
*
|
|
4069
|
-
* The CLI and the workspace server are separate processes over one store. Without this, each could
|
|
4070
|
-
* load the same review, change its own fields and rename its result into place, and whichever
|
|
4071
|
-
* renamed last would discard the other: an update finishing in the terminal would put back a
|
|
4072
|
-
* question the reviewer had just ticked on the page. A file created exclusively is the lock, so it
|
|
4073
|
-
* holds between processes and needs nothing running to arbitrate.
|
|
4074
|
-
*
|
|
4075
|
-
* Synchronous, including the wait, because every write it guards is. It is held for a read and a
|
|
4076
|
-
* rename, so a wait is milliseconds; one that runs to `WRITE_LOCK_WAIT_MS` means a holder has hung,
|
|
4077
|
-
* and that is raised rather than written through. Re-entrant within a process, so a change made
|
|
4078
|
-
* under it can save without deadlocking itself. A lock left by a process that died is broken: at
|
|
4079
|
-
* once when its holder is on this machine and gone, and after `WRITE_LOCK_STALE_MS` in every case,
|
|
4080
|
-
* because a pid can be reused and a live one proves nothing about who took the lock.
|
|
4081
|
-
*/
|
|
4082
|
-
function withWriteLock(id, fn) {
|
|
4083
|
-
const depth = heldWriteLocks.get(id) ?? 0;
|
|
4084
|
-
if (depth > 0) {
|
|
4085
|
-
heldWriteLocks.set(id, depth + 1);
|
|
4086
|
-
try {
|
|
4087
|
-
return fn();
|
|
4088
|
-
} finally {
|
|
4089
|
-
heldWriteLocks.set(id, depth);
|
|
4090
|
-
}
|
|
4091
|
-
}
|
|
4092
|
-
mkdirSync(WRITE_LOCKS_DIR, { recursive: true });
|
|
4093
|
-
const path = join(WRITE_LOCKS_DIR, `${id}.lock`);
|
|
4094
|
-
const mine = {
|
|
4095
|
-
token: randomUUID(),
|
|
4096
|
-
pid: process.pid,
|
|
4097
|
-
host: hostname(),
|
|
4098
|
-
at: Date.now()
|
|
4099
|
-
};
|
|
4100
|
-
const deadline = Date.now() + WRITE_LOCK_WAIT_MS;
|
|
4101
|
-
for (;;) {
|
|
4102
|
-
try {
|
|
4103
|
-
const fd = openSync(path, "wx", 384);
|
|
4104
|
-
try {
|
|
4105
|
-
writeSync(fd, JSON.stringify(mine));
|
|
4106
|
-
} finally {
|
|
4107
|
-
closeSync(fd);
|
|
4108
|
-
}
|
|
4109
|
-
break;
|
|
4110
|
-
} catch (error) {
|
|
4111
|
-
if (error.code !== "EEXIST") throw error;
|
|
4112
|
-
}
|
|
4113
|
-
if (breakIfAbandoned(path)) continue;
|
|
4114
|
-
if (Date.now() >= deadline) throw new WorkspaceWriteBusyError();
|
|
4115
|
-
Atomics.wait(sleeper, 0, 0, 2);
|
|
4116
|
-
}
|
|
4117
|
-
heldWriteLocks.set(id, 1);
|
|
4118
|
-
try {
|
|
4119
|
-
return fn();
|
|
4120
|
-
} finally {
|
|
4121
|
-
heldWriteLocks.delete(id);
|
|
4122
|
-
if (readHolder(path)?.token === mine.token) try {
|
|
4123
|
-
unlinkSync(path);
|
|
4124
|
-
} catch {}
|
|
4125
|
-
}
|
|
4126
|
-
}
|
|
4127
|
-
function readHolder(path) {
|
|
4128
|
-
try {
|
|
4129
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
4130
|
-
} catch {
|
|
4131
|
-
return null;
|
|
4132
|
-
}
|
|
4133
|
-
}
|
|
4134
|
-
/** Removes a lock whose holder is gone, and says whether it did. */
|
|
4135
|
-
function breakIfAbandoned(path) {
|
|
4136
|
-
const holder = readHolder(path);
|
|
4137
|
-
let age = 0;
|
|
4138
|
-
try {
|
|
4139
|
-
age = Date.now() - Math.min(statSync(path).mtimeMs, holder?.at ?? Infinity);
|
|
4140
|
-
} catch {
|
|
4141
|
-
return true;
|
|
4142
|
-
}
|
|
4143
|
-
if (!(age > WRITE_LOCK_STALE_MS || holder !== null && holder.host === hostname() && !processAlive(holder.pid))) return false;
|
|
4144
|
-
if (readHolder(path)?.token !== holder?.token) return true;
|
|
4145
|
-
try {
|
|
4146
|
-
unlinkSync(path);
|
|
4147
|
-
} catch {}
|
|
4148
|
-
return true;
|
|
4149
|
-
}
|
|
4150
|
-
function processAlive(pid) {
|
|
4151
|
-
try {
|
|
4152
|
-
process.kill(pid, 0);
|
|
4153
|
-
return true;
|
|
4154
|
-
} catch (error) {
|
|
4155
|
-
return error.code === "EPERM";
|
|
4156
|
-
}
|
|
4157
|
-
}
|
|
4158
4205
|
/**
|
|
4159
4206
|
* Change a review as it is on disk now: read, change, write, with nothing able to run in between.
|
|
4160
4207
|
*
|
|
@@ -7429,6 +7476,71 @@ function readIndexToken() {
|
|
|
7429
7476
|
return replacement;
|
|
7430
7477
|
}
|
|
7431
7478
|
//#endregion
|
|
7479
|
+
//#region ../../packages/review-harness/src/workspace/queue_live.ts
|
|
7480
|
+
/**
|
|
7481
|
+
* The review queue as it relates to reviews that actually exist.
|
|
7482
|
+
*
|
|
7483
|
+
* `queue.ts` stores ids and must not learn to read the review store: `store.ts` imports it for the
|
|
7484
|
+
* prune, and the reverse import would close the cycle. Anything that has to ask both questions --
|
|
7485
|
+
* is this id queued, and is it a review -- lives here instead, which imports both and is imported by
|
|
7486
|
+
* neither. That used to be one function private to `http.ts`; the CLI's `--queue` needs the same
|
|
7487
|
+
* answers, and two copies of "which queued ids are still live" would drift.
|
|
7488
|
+
*/
|
|
7489
|
+
/**
|
|
7490
|
+
* Drop queued ids whose review has been deleted or pruned, and store the result if that changed it.
|
|
7491
|
+
*
|
|
7492
|
+
* `stored` is what the caller last saw, and is only a shortcut: when every id in it is live there is
|
|
7493
|
+
* nothing to do. Otherwise the compaction is made against the queue as it is under the lock, not
|
|
7494
|
+
* against `stored`, so an id somebody queued since `stored` was read is not dropped by writing an
|
|
7495
|
+
* older list back.
|
|
7496
|
+
*
|
|
7497
|
+
* Falls back to what was handed in on a write failure: a stale id in the answer is a card the page
|
|
7498
|
+
* cannot draw, which it already tolerates, and is better than refusing an Add over it.
|
|
7499
|
+
*/
|
|
7500
|
+
function compactQueue(stored) {
|
|
7501
|
+
if (stored.every(isLive)) return stored;
|
|
7502
|
+
return updateQueue((current) => livePart(current)) ?? stored;
|
|
7503
|
+
}
|
|
7504
|
+
/**
|
|
7505
|
+
* Put one review on the queue, the way the index's Add does, and say how that went.
|
|
7506
|
+
*
|
|
7507
|
+
* **A completed review is left out**, not queued and not reopened. The queue holds unfinished work
|
|
7508
|
+
* only -- `dequeueOnComplete` is the other half of that rule -- and quietly moving somebody's review
|
|
7509
|
+
* back to In progress because a command mentioned it is a status change nobody asked for.
|
|
7510
|
+
*
|
|
7511
|
+
* Already queued is `already`, not a move: the same rule `queueAdd` keeps, for the same reason.
|
|
7512
|
+
*
|
|
7513
|
+
* Compacted before adding, for the reason `addMany` in `http.ts` gives: a review deleted since it
|
|
7514
|
+
* was queued would otherwise hold a place, and a live review could be refused as "full" by it.
|
|
7515
|
+
*/
|
|
7516
|
+
function queueForReview(id) {
|
|
7517
|
+
const workspace = loadWorkspace(id);
|
|
7518
|
+
if (!workspace) return "gone";
|
|
7519
|
+
if (workspace.completedAt !== void 0) return "complete";
|
|
7520
|
+
let result = "added";
|
|
7521
|
+
return updateQueue((current) => {
|
|
7522
|
+
const live = current.filter(isLive);
|
|
7523
|
+
const unchanged = live.length === current.length ? null : live;
|
|
7524
|
+
if (live.includes(id)) {
|
|
7525
|
+
result = "already";
|
|
7526
|
+
return unchanged;
|
|
7527
|
+
}
|
|
7528
|
+
if (live.length >= 50) {
|
|
7529
|
+
result = "full";
|
|
7530
|
+
return unchanged;
|
|
7531
|
+
}
|
|
7532
|
+
return [...live, id];
|
|
7533
|
+
}) === null ? "unsaved" : result;
|
|
7534
|
+
}
|
|
7535
|
+
function isLive(id) {
|
|
7536
|
+
return loadWorkspace(id) !== null;
|
|
7537
|
+
}
|
|
7538
|
+
/** The queue without its deleted reviews, or null when there are none to drop. */
|
|
7539
|
+
function livePart(queue) {
|
|
7540
|
+
const live = queue.filter(isLive);
|
|
7541
|
+
return live.length === queue.length ? null : live;
|
|
7542
|
+
}
|
|
7543
|
+
//#endregion
|
|
7432
7544
|
//#region ../../packages/review-harness/src/workspace/relative_time.ts
|
|
7433
7545
|
/**
|
|
7434
7546
|
* Relative time, in the one wording this product uses.
|
|
@@ -12515,4 +12627,4 @@ function lineageIds(id) {
|
|
|
12515
12627
|
return [.../* @__PURE__ */ new Set([id, ...lineage.map((review) => review.id)])];
|
|
12516
12628
|
}
|
|
12517
12629
|
//#endregion
|
|
12518
|
-
export {
|
|
12630
|
+
export { bootstrapUrl as $, recentWorkspaces as $t, createPrState as A, STORE_ROOT as An, fail as At, toneOf as B, driverGone as Bt, readEvents as C, git as Cn, fillFileUrlTemplate as Ct, announceJob as D, readAgentPreference as Dn, reviewedShaOf as Dt, telemetryUploadConsent as E, clearAgentPreference as En, followedBranch as Et, markAllRead as F, detectAgents as Fn, isLiveJob as Ft, MANAGED_ROOT as G, latestKissRun as Gt, withUsageRecorded as H, followedRefName as Ht, markRead as I, AgentCancelledError as In, phasesFor as It, modelHelpLines as J, loadWorkspace as Jt, readMarker as K, lineageKeyFor as Kt, pruneNotifications as L, AgentUnavailableError as Ln, progressSteps as Lt, eventName as M, DEFAULT_AGENT_ID as Mn, loadJob as Mt, headline as N, agentById as Nn, saveJob as Nt, branchOf as O, writeAgentPreference as On, allJobs as Ot, listNotifications as P, agentFor as Pn, PHASES as Pt, readIndexToken as Q, previousKissResult as Qt, readNotification as R, purposeOf as Rt, startJob as S, isCommitReview as Sn, remoteHost as St, recordTelemetryUploadConsent as T, resolveModel as Tn, checkReview as Tt, EXPLAIN_SIMPLY_PROMPT as U, groupByLineage as Ut, unreadCount as V, isClaimed as Vt, ReviewBeingDeletedError as W, indexReviewGroups as Wt, compactQueue as X, mutateWorkspace as Xt, relativeTime as Y, matchingWorkspaceIds as Yt, queueForReview as Z, positionInLineage as Zt, agentEnvOf as _, reviewerDispositions as _n, statedWorkspacePort as _t, wasBlocked as a, workspaceStamps as an, spawnServer as at, reanalyseReview as b, resolveTarget as bn, forgeResolver as bt, RefreshUnavailableError as c, queueAddMany as cn, writeServerRecord as ct, refreshPrContext as d, readQueue as dn, feedbackUrl as dt, reviewedCommit as en, clearServerRecord as et, isUnchanged as f, doneVerb as fn, DEFAULT_WORKSPACE_PORT as ft, runningJobs as g, questionsOutstanding as gn, resolveWorkspacePort as gt, liveUpdateFor as h, issuesOutstanding as hn, parseWorkspacePort as ht, liveJobsFor as i, workspaceRevision as in, serverEntryExists as it, createPrUpdate as j, AGENT_IDS as jn, isTerminal as jt, subjectOf as k, CONFIG_PATH as kn, endedAt as kt, mergeUpdateInto as l, queueRemove as ln, BUILD_VERSION as lt, UpdateAlreadyRunningError as m, isQuestionOutstanding as mn, MIN_WORKSPACE_PORT as mt, deleteReview as n, summarise as nn, readServerRecord as nt, discardJob as o, dequeueOnComplete as on, stopServer as ot, askCheckout as p, isIssueOutstanding as pn, MAX_WORKSPACE_PORT as pt, removeWorktree as q, lineagePosition as qt, lineageIds as r, touchWorkspace as rn, reviewsUrl as rt, FETCH_TIMEOUT_MS as s, queueAdd as sn, workspaceUrl as st, deleteLineage as t, reviewedRepositories as tn, ensureServer as tt, updateReview as u, queueStamp as un, PACKAGE_NAME as ut, recordKissRun as v, processDiscussion as vn, writePortPreference as vt, record as w, resolveAgent as wn, checkCodeFreshness as wt, runJob as x, displayRef as xn, forgeFor as xt, startKissJob as y, describeAuthorship as yn, claimRequestUrl as yt, removeNotification as z, clearClaim as zt };
|