@ravshansbox/pi-next-issue 2.19.0 → 2.20.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 +5 -4
- package/index.ts +29 -28
- package/package.json +1 -1
- package/src/worker.ts +25 -4
package/README.md
CHANGED
|
@@ -47,12 +47,13 @@ Pi does not implement the issue in that session. The worker does the work. The b
|
|
|
47
47
|
|
|
48
48
|
## Worker
|
|
49
49
|
|
|
50
|
-
The extension does no work itself. On `/next-issue` or `next_issue_start` it connects to the `next-issue` worker on the socket file
|
|
50
|
+
The extension does no work itself. On `/next-issue` or `next_issue_start` it connects to the `next-issue` worker of the repository on the socket file `.next-issue/worker.sock` in the repository. When no worker answers, it starts one in the repository with `npx -y next-issue@<version> serve`, where `<version>` is the version of the extension, or `latest` when the extension runs from a source checkout with version `0.0.0`, in the background, and writes the output of the worker to `.next-issue/worker.log` in the repository. When the path of the socket file is too long for the system, the extension says so and starts nothing.
|
|
51
51
|
|
|
52
|
-
-
|
|
52
|
+
- Each repository has its own worker. Every pi in a repository shares the worker of that repository and shows its run, whoever started it. A run, restart or upgrade in one repository never affects another.
|
|
53
53
|
- Opening pi does not start a worker. It only shows a run when a worker is going.
|
|
54
54
|
- The run does not belong to pi: it goes on after `/new`, `/resume`, a fork, a reload or quitting pi.
|
|
55
|
-
- The worker
|
|
56
|
-
-
|
|
55
|
+
- The worker exits when no run is going and no pi is connected. Stop it at once with `npx next-issue stop` in the repository.
|
|
56
|
+
- Versions before one worker per repository ran one worker for the machine, which does not stop by itself and can keep claiming issues alongside the new worker. Stop it once after you upgrade, as the [`next-issue` README](../next-issue/README.md#worker) says.
|
|
57
|
+
- When the extension is newer than the worker of a repository, that worker closes once no run is going, and the extension starts the newer worker. Workers of other repositories carry on.
|
|
57
58
|
|
|
58
59
|
Each run asks the worker to merge the pull requests. [ADR 0002](https://github.com/ravshansbox/next-issue/blob/main/docs/adr/0002-merge-setting.md) compares where the merge setting could live. See the [`next-issue` README](../next-issue/README.md) for what a run does, its labels, its state and its config.
|
package/index.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
type ExtensionAPI,
|
|
6
|
-
type ExtensionContext,
|
|
7
|
-
getAgentDir,
|
|
8
|
-
} from '@earendil-works/pi-coding-agent';
|
|
4
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
9
5
|
import { Type } from 'typebox';
|
|
10
6
|
import { must, type RunOptions } from './src/exec.ts';
|
|
11
7
|
import { repoRoot } from './src/git.ts';
|
|
@@ -15,10 +11,11 @@ import {
|
|
|
15
11
|
spawnWorker,
|
|
16
12
|
WorkerClient,
|
|
17
13
|
type WorkerEvent,
|
|
14
|
+
workerPaths,
|
|
18
15
|
} from './src/worker.ts';
|
|
19
16
|
|
|
20
17
|
export type Options = {
|
|
21
|
-
client: () => WorkerClient;
|
|
18
|
+
client: (root: string) => WorkerClient;
|
|
22
19
|
root: (cwd: string) => Promise<string>;
|
|
23
20
|
gh: (args: string[], options: RunOptions) => Promise<string>;
|
|
24
21
|
};
|
|
@@ -39,10 +36,15 @@ function errorText(error: unknown): string {
|
|
|
39
36
|
return error instanceof Error ? error.message : String(error);
|
|
40
37
|
}
|
|
41
38
|
|
|
42
|
-
function shared(create: () => WorkerClient): WorkerClient {
|
|
43
|
-
const store = globalThis as Record<symbol, WorkerClient | undefined>;
|
|
44
|
-
store[KEY] ??=
|
|
45
|
-
|
|
39
|
+
function shared(create: (root: string) => WorkerClient, root: string): WorkerClient {
|
|
40
|
+
const store = globalThis as Record<symbol, Map<string, WorkerClient> | undefined>;
|
|
41
|
+
const clients = (store[KEY] ??= new Map());
|
|
42
|
+
let client = clients.get(root);
|
|
43
|
+
if (client === undefined) {
|
|
44
|
+
client = create(root);
|
|
45
|
+
clients.set(root, client);
|
|
46
|
+
}
|
|
47
|
+
return client;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
function duration(ms: number): string {
|
|
@@ -103,14 +105,12 @@ function packageVersion(): string {
|
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
const fromMachine: Options = {
|
|
106
|
-
client: () =>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
socketPath: join(agentDir, 'next-issue.sock'),
|
|
108
|
+
client: (root) =>
|
|
109
|
+
new WorkerClient({
|
|
110
|
+
socketPath: workerPaths(root).socketPath,
|
|
110
111
|
version: packageVersion(),
|
|
111
|
-
spawnWorker: (version) => spawnWorker(
|
|
112
|
-
})
|
|
113
|
-
},
|
|
112
|
+
spawnWorker: (version) => spawnWorker(root, version),
|
|
113
|
+
}),
|
|
114
114
|
root: repoRoot,
|
|
115
115
|
gh: (args, options) => must('gh', args, options),
|
|
116
116
|
};
|
|
@@ -129,7 +129,7 @@ export function createExtension(options: Options) {
|
|
|
129
129
|
let baseline = false;
|
|
130
130
|
|
|
131
131
|
const current = (): Progress | undefined =>
|
|
132
|
-
root === undefined ? undefined : shared(options.client).runs[root];
|
|
132
|
+
root === undefined ? undefined : shared(options.client, root).runs[root];
|
|
133
133
|
|
|
134
134
|
const render = () => {
|
|
135
135
|
const progress = current();
|
|
@@ -164,7 +164,7 @@ export function createExtension(options: Options) {
|
|
|
164
164
|
|
|
165
165
|
const askQuestions = () => {
|
|
166
166
|
if (root !== undefined) {
|
|
167
|
-
shared(options.client).send({ type: 'questions', root });
|
|
167
|
+
shared(options.client, root).send({ type: 'questions', root });
|
|
168
168
|
}
|
|
169
169
|
};
|
|
170
170
|
|
|
@@ -214,15 +214,16 @@ export function createExtension(options: Options) {
|
|
|
214
214
|
|
|
215
215
|
const follow = () => {
|
|
216
216
|
unsubscribe?.();
|
|
217
|
+
if (root === undefined) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const client = shared(options.client, root);
|
|
217
221
|
const progress = current();
|
|
218
222
|
handled = progress?.handled.length ?? 0;
|
|
219
223
|
status = progress?.status;
|
|
220
|
-
waiting =
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
: shared(options.client).questions[root]?.map((question) => question.issue);
|
|
224
|
-
baseline = !shared(options.client).connected;
|
|
225
|
-
unsubscribe = shared(options.client).subscribe(onEvent);
|
|
224
|
+
waiting = client.questions[root]?.map((question) => question.issue);
|
|
225
|
+
baseline = !client.connected;
|
|
226
|
+
unsubscribe = client.subscribe(onEvent);
|
|
226
227
|
render();
|
|
227
228
|
paintQuestions();
|
|
228
229
|
if (!baseline) {
|
|
@@ -249,7 +250,7 @@ export function createExtension(options: Options) {
|
|
|
249
250
|
return;
|
|
250
251
|
}
|
|
251
252
|
follow();
|
|
252
|
-
await shared(options.client).attach();
|
|
253
|
+
await shared(options.client, root).attach();
|
|
253
254
|
});
|
|
254
255
|
|
|
255
256
|
pi.on('session_shutdown', async () => {
|
|
@@ -272,7 +273,7 @@ export function createExtension(options: Options) {
|
|
|
272
273
|
show(ctx);
|
|
273
274
|
follow();
|
|
274
275
|
}
|
|
275
|
-
const client = shared(options.client);
|
|
276
|
+
const client = shared(options.client, root);
|
|
276
277
|
await client.ensure();
|
|
277
278
|
return { client, root, progress: client.runs[root] };
|
|
278
279
|
};
|
|
@@ -352,7 +353,7 @@ export function createExtension(options: Options) {
|
|
|
352
353
|
parameters: Type.Object({}),
|
|
353
354
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
354
355
|
const root = await options.root(ctx.cwd);
|
|
355
|
-
const client = shared(options.client);
|
|
356
|
+
const client = shared(options.client, root);
|
|
356
357
|
await client.attach();
|
|
357
358
|
if (!client.connected) {
|
|
358
359
|
return report('no worker is running', root);
|
package/package.json
CHANGED
package/src/worker.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { closeSync, openSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
2
|
+
import { closeSync, mkdirSync, openSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
4
|
import WebSocket from 'ws';
|
|
5
5
|
|
|
6
6
|
export type RunStatus = 'running' | 'stopping' | 'finished' | 'stopped' | 'failed';
|
|
@@ -108,10 +108,30 @@ export function workerArgs(version: string): string[] {
|
|
|
108
108
|
return ['-y', `next-issue@${version === '0.0.0' ? 'latest' : version}`, 'serve'];
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
export function
|
|
112
|
-
|
|
111
|
+
export function workerPaths(root: string): { socketPath: string; logPath: string } {
|
|
112
|
+
return {
|
|
113
|
+
socketPath: join(root, '.next-issue', 'worker.sock'),
|
|
114
|
+
logPath: join(root, '.next-issue', 'worker.log'),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function checkSocketPath(path: string, platform = process.platform): void {
|
|
119
|
+
const size = platform === 'linux' ? 108 : 104;
|
|
120
|
+
const length = Buffer.byteLength(path);
|
|
121
|
+
if (length >= size) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`the socket path ${path} is too long for this system: ${length} bytes, at most ${size - 1}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function spawnWorker(root: string, version: string): void {
|
|
129
|
+
const { logPath } = workerPaths(root);
|
|
130
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
131
|
+
const log = openSync(logPath, 'a');
|
|
113
132
|
try {
|
|
114
133
|
const child = spawn('npx', workerArgs(version), {
|
|
134
|
+
cwd: root,
|
|
115
135
|
detached: true,
|
|
116
136
|
stdio: ['ignore', log, log],
|
|
117
137
|
});
|
|
@@ -191,6 +211,7 @@ export class WorkerClient {
|
|
|
191
211
|
}
|
|
192
212
|
|
|
193
213
|
private async start(): Promise<void> {
|
|
214
|
+
checkSocketPath(this.options.socketPath);
|
|
194
215
|
try {
|
|
195
216
|
await this.open();
|
|
196
217
|
return;
|