pi-straw-boss 0.56.1 → 0.57.1
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 +9 -0
- package/README.zh-TW.md +9 -0
- package/dist/server/{chunk-SEBJ5DZT.mjs → chunk-LJ3SPNF5.mjs} +4 -1
- package/dist/server/cli.mjs +44 -3
- package/dist/server/main.mjs +1528 -684
- package/package.json +3 -2
- package/src/balance.mjs +3 -2
- package/src/hygiene.ts +282 -0
- package/src/procs/cwd.ts +65 -0
- package/src/procs/guard.ts +198 -0
- package/src/procs/kill.ts +111 -0
- package/src/procs/killguard.ts +94 -0
- package/src/procs/killset.ts +118 -0
- package/src/procs/marker.ts +66 -0
- package/src/procs/table.ts +103 -0
- package/src/procs/worktree.ts +76 -0
- package/src/server/cli.ts +12 -1
- package/src/server/client.ts +2 -2
- package/src/server/intercom.ts +24 -6
- package/src/server/main.ts +6 -0
- package/src/server/procs.ts +485 -0
- package/src/server/procscli.ts +32 -0
- package/src/server/procsops.ts +82 -0
- package/src/server/runs.ts +12 -4
- package/src/server/testguard.ts +1 -1
package/README.md
CHANGED
|
@@ -206,6 +206,15 @@ The plugin starts reaping as soon as it is installed or linked, with no trial ru
|
|
|
206
206
|
Run `bash herdr-reaper/run.sh reaper.mjs dry-run` first to see what it would do, and pin the workspaces you want to keep (the pin action works once the plugin is linked, and a `config.json` with `only_under` can be put in the plugin's config directory before that).
|
|
207
207
|
[How it works](docs/how-it-works.md#worktree-reaper) has the rules and the limits.
|
|
208
208
|
|
|
209
|
+
## Process hygiene
|
|
210
|
+
|
|
211
|
+
Every pi loads a small extension that marks its environment, so what a worker's bash starts carries the worker's id even when `timeout` or `setsid` moved it to a process group of its own.
|
|
212
|
+
A pi-herdr-agents worker registers with the sb server; when the worker quits or its pane closes, the server stops what it left (SIGTERM, then SIGKILL after 10 s, each step checked by pid and start time) and logs one `procs-reaped` line.
|
|
213
|
+
`worktree_remove` and a bash `git worktree remove` are refused while a process works in the checkout, with the pid, program name, CPU and age of each holder and the command that reaps them: `node dist/server/cli.mjs procs reap --checkout <path>`.
|
|
214
|
+
`cli.mjs procs` lists the workers the server knows.
|
|
215
|
+
Heavy-command slots, Linux cgroups and a runaway-process watchdog are planned for a later release.
|
|
216
|
+
[How it works](docs/how-it-works.md#process-hygiene-and-slots) has the rules and the limits.
|
|
217
|
+
|
|
209
218
|
## Learn more
|
|
210
219
|
|
|
211
220
|
[How it works](docs/how-it-works.md) covers roles, the tower, the dashboard, the browser view, what the extension writes, and how Claude Code or Codex sessions join the net.
|
package/README.zh-TW.md
CHANGED
|
@@ -202,6 +202,15 @@ plugin 一安裝或連結就開始清理,沒有試跑,也不會先問:第
|
|
|
202
202
|
請先執行 `bash herdr-reaper/run.sh reaper.mjs dry-run` 看它會做什麼,並釘選想保留的工作區(plugin 連結後 `pin` 動作才能用;也可以在連結之前,先在 plugin 的設定目錄放一個帶 `only_under` 的 `config.json`)。
|
|
203
203
|
[運作方式](docs/how-it-works.md#worktree-reaper)(英文)有規則和限制。
|
|
204
204
|
|
|
205
|
+
## 行程清理
|
|
206
|
+
|
|
207
|
+
每個 pi 都會載入一個小擴充,在環境變數裡留下標記,所以 worker 的 bash 啟動的東西,就算被 `timeout` 或 `setsid` 搬到自己的行程群組,仍帶著 worker 的 id。
|
|
208
|
+
pi-herdr-agents 的 worker 會向 sb 伺服器登記;worker 結束或它的 pane 關掉時,伺服器收掉它留下的行程(先 SIGTERM,10 秒後 SIGKILL,每一步都用 pid 和啟動時間再確認),並記一行 `procs-reaped`。
|
|
209
|
+
`worktree_remove` 和 bash 的 `git worktree remove`,在還有行程以該 checkout 為工作目錄時會被擋下,並列出每個行程的 pid、程式名稱、CPU 和存活時間,以及收掉它們的指令:`node dist/server/cli.mjs procs reap --checkout <path>`。
|
|
210
|
+
`cli.mjs procs` 列出伺服器認得的 worker。
|
|
211
|
+
重型指令的 slot、Linux cgroup 和失控行程的看門狗留待下一版。
|
|
212
|
+
規則與限制見[運作方式](docs/how-it-works.md#process-hygiene-and-slots)。
|
|
213
|
+
|
|
205
214
|
## 延伸閱讀
|
|
206
215
|
|
|
207
216
|
[運作方式](docs/how-it-works.md)(英文)說明角色、塔台、儀表板、瀏覽器畫面、這個擴充套件會寫入什麼,以及 Claude Code 或 Codex 的 session 怎麼加入網。
|
|
@@ -2973,7 +2973,7 @@ var spawnDetached = (spec) => {
|
|
|
2973
2973
|
return { pid: child.pid, exited: new Promise((resolve2) => (child.once("exit", (code) => resolve2(code)), child.once("error", () => resolve2(-1)))) };
|
|
2974
2974
|
};
|
|
2975
2975
|
var isIdentity = (key) => IDENTITY.test(key);
|
|
2976
|
-
var IDENTITY = /^(PI_INTERCOM_SESSION_ID|PI_INTERCOM_STABLE_ID|PI_SUBAGENT_.*|STRAW_BOSS_ROLE|HERDR_PANE_ID|HERDR_TAB_ID|HERDR_WORKSPACE_ID)$/;
|
|
2976
|
+
var IDENTITY = /^(PI_INTERCOM_SESSION_ID|PI_INTERCOM_STABLE_ID|PI_SUBAGENT_.*|STRAW_BOSS_ROLE|STRAW_BOSS_OWNER|HERDR_PANE_ID|HERDR_TAB_ID|HERDR_WORKSPACE_ID)$/;
|
|
2977
2977
|
var serverEnv = (env) => Object.fromEntries(Object.entries(env).filter(([key]) => !isIdentity(key)));
|
|
2978
2978
|
function intercomOf(env, platform = process.platform) {
|
|
2979
2979
|
const scope = env.PI_INTERCOM_SCOPE_ID?.trim(), tcp = env.PI_INTERCOM_TCP?.trim(), transport = env.PI_INTERCOM_TRANSPORT?.trim().toLowerCase();
|
|
@@ -3715,7 +3715,10 @@ export {
|
|
|
3715
3715
|
tailLog,
|
|
3716
3716
|
logSize,
|
|
3717
3717
|
readFrom2 as readFrom,
|
|
3718
|
+
TEST_ROOT,
|
|
3718
3719
|
inTestRun,
|
|
3720
|
+
inside,
|
|
3721
|
+
refuse,
|
|
3719
3722
|
AFTER,
|
|
3720
3723
|
PROTOCOL,
|
|
3721
3724
|
newSecret,
|
package/dist/server/cli.mjs
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
within,
|
|
49
49
|
workspaces,
|
|
50
50
|
writeJson
|
|
51
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-LJ3SPNF5.mjs";
|
|
52
52
|
|
|
53
53
|
// src/server/cli.ts
|
|
54
54
|
import { existsSync, realpathSync as realpathSync2 } from "node:fs";
|
|
@@ -154,6 +154,34 @@ function onFatal(leave, world = {}) {
|
|
|
154
154
|
proc.on("unhandledRejection", fatal);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// src/server/procscli.ts
|
|
158
|
+
import { resolve as resolve2 } from "node:path";
|
|
159
|
+
async function runProcs(ctx, flags, { out, err }) {
|
|
160
|
+
const record = await trusted(ctx);
|
|
161
|
+
if (!record || !ctx.alive(record.pid)) {
|
|
162
|
+
err("no sb server is running, so there is no owner registry to ask\n");
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
const request = flags.sub === "reap" ? { cmd: "procs-reap", ...flags.owner !== void 0 ? { owner: String(flags.owner) } : { checkout: resolve2(String(flags.checkout)) } } : { cmd: "procs-list" };
|
|
166
|
+
let reply;
|
|
167
|
+
try {
|
|
168
|
+
reply = (await callControl(record.socket, record.secret, request, 6e4)).reply;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
err(error instanceof ControlError && error.sent ? `the server took the request but did not answer in time (${error.message}); it may still be running: check with \`procs\` before asking again
|
|
171
|
+
` : `the server did not take the request: ${error.message}
|
|
172
|
+
`);
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
if (!reply?.ok) {
|
|
176
|
+
err(`${String(reply?.error ?? "no answer")}
|
|
177
|
+
`);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
const { ok: _ok, ...rest } = reply;
|
|
181
|
+
out(rest);
|
|
182
|
+
return Array.isArray(rest.left) && rest.left.length > 0 ? 1 : 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
157
185
|
// src/towerprompt/messages.ts
|
|
158
186
|
function localeOf(env) {
|
|
159
187
|
const set = env.LC_ALL || env.LC_MESSAGES || env.LANG || systemLanguage();
|
|
@@ -307,7 +335,7 @@ async function runPrompt(o) {
|
|
|
307
335
|
let chunks = 0, lineChunk = 0, lastDataAt = -Infinity;
|
|
308
336
|
const pasteCapMs = o.pasteCapMs ?? PASTE_CAP_MS;
|
|
309
337
|
let finish;
|
|
310
|
-
const done = new Promise((
|
|
338
|
+
const done = new Promise((resolve3) => finish = resolve3);
|
|
311
339
|
const say = (line, again = true) => {
|
|
312
340
|
if (terminal) {
|
|
313
341
|
cursorTo(o.output, 0);
|
|
@@ -568,6 +596,8 @@ var USAGE = `usage: cli.mjs up [--dir FOLDER --label NAME] [--timeout SECONDS] [
|
|
|
568
596
|
cli.mjs stop | status
|
|
569
597
|
cli.mjs restart [--reason TEXT] [--by TEXT] [--downgrade]
|
|
570
598
|
cli.mjs logs [--lines N] [--follow]
|
|
599
|
+
cli.mjs procs [list]
|
|
600
|
+
cli.mjs procs reap (--owner ID | --checkout PATH) (kill what an ended worker left; refused while a live worker works in the checkout)
|
|
571
601
|
cli.mjs prompt --workspace ID [--state-dir DIR] (the tower prompt of a standing pane: the server types it)`;
|
|
572
602
|
var UsageError = class extends Error {
|
|
573
603
|
};
|
|
@@ -579,6 +609,7 @@ var FLAGS = {
|
|
|
579
609
|
restart: { value: ["reason", "by"], switch: ["downgrade"] },
|
|
580
610
|
logs: { value: ["lines"], switch: ["follow"] },
|
|
581
611
|
prompt: { value: ["workspace", "state-dir"], switch: [] },
|
|
612
|
+
procs: { value: ["owner", "checkout"], switch: [] },
|
|
582
613
|
run: { value: ["by", "detach", "detach-reason", "acl-token"], switch: ["detached"] }
|
|
583
614
|
};
|
|
584
615
|
function parseArgs(argv) {
|
|
@@ -588,6 +619,12 @@ function parseArgs(argv) {
|
|
|
588
619
|
if (!allowed) throw new UsageError(`unknown command ${command}
|
|
589
620
|
${USAGE}`);
|
|
590
621
|
const flags = {};
|
|
622
|
+
if (command === "procs") {
|
|
623
|
+
const sub = rest[0] !== void 0 && !rest[0].startsWith("--") ? rest.shift() : "list";
|
|
624
|
+
if (sub !== "list" && sub !== "reap") throw new UsageError(`unknown procs command ${sub}
|
|
625
|
+
${USAGE}`);
|
|
626
|
+
flags.sub = sub;
|
|
627
|
+
}
|
|
591
628
|
for (let i = 0; i < rest.length; i++) {
|
|
592
629
|
const flag = rest[i].replace(/^--/, "");
|
|
593
630
|
if (!rest[i].startsWith("--")) throw new UsageError(`unexpected argument ${rest[i]}
|
|
@@ -602,6 +639,8 @@ ${USAGE}`);
|
|
|
602
639
|
if (flags.dir === void 0 !== (flags.label === void 0)) throw new UsageError(`--dir and --label go together: the desk steps need both
|
|
603
640
|
${USAGE}`);
|
|
604
641
|
if (flags.dir !== void 0 && (!String(flags.dir).trim() || !String(flags.label).trim())) throw new UsageError("--dir and --label are not empty");
|
|
642
|
+
if (command === "procs" && (flags.sub === "reap" ? flags.owner === void 0 === (flags.checkout === void 0) : flags.owner !== void 0 || flags.checkout !== void 0)) throw new UsageError(`procs reap needs --owner or --checkout, one of them; list takes neither
|
|
643
|
+
${USAGE}`);
|
|
605
644
|
if (flags.timeout !== void 0 && !(Number(flags.timeout) > 0)) throw new UsageError("--timeout is a number of seconds");
|
|
606
645
|
if (flags.lines !== void 0 && !(Number.isInteger(Number(flags.lines)) && Number(flags.lines) > 0)) throw new UsageError("--lines is a positive whole number");
|
|
607
646
|
return { command, flags };
|
|
@@ -676,6 +715,8 @@ ${USAGE}
|
|
|
676
715
|
return await runDetached();
|
|
677
716
|
case "prompt":
|
|
678
717
|
return await promptCommand();
|
|
718
|
+
case "procs":
|
|
719
|
+
return await runProcs(ctx, flags, { out: json, err });
|
|
679
720
|
}
|
|
680
721
|
} catch (error) {
|
|
681
722
|
if (error instanceof ServerError) return fail(error);
|
|
@@ -966,7 +1007,7 @@ if (process.argv[1] && realpath(selfPath()) === realpath(process.argv[1])) {
|
|
|
966
1007
|
setInterval(() => {
|
|
967
1008
|
}, 2 ** 30);
|
|
968
1009
|
const code = await run(process.argv.slice(2));
|
|
969
|
-
await new Promise((
|
|
1010
|
+
await new Promise((resolve3) => process.stdout.write("", () => resolve3()));
|
|
970
1011
|
process.exit(code);
|
|
971
1012
|
}
|
|
972
1013
|
export {
|