@loompad/cli 1.2.4 → 1.2.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/CHANGELOG.md +44 -0
- package/dist/adapters/antigravity-cli.d.ts +7 -0
- package/dist/adapters/antigravity-cli.js +10 -2
- package/dist/adapters/antigravity-cli.js.map +1 -1
- package/dist/adapters/base.d.ts +5 -3
- package/dist/adapters/base.js +7 -2
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +25 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/opencode.d.ts +18 -21
- package/dist/adapters/opencode.js +103 -22
- package/dist/adapters/opencode.js.map +1 -1
- package/dist/cli/index.js +352 -29
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/ui.js +11 -1
- package/dist/cli/ui.js.map +1 -1
- package/dist/core/orchestra.js +17 -1
- package/dist/core/orchestra.js.map +1 -1
- package/dist/core/review-triage.js +3 -13
- package/dist/core/review-triage.js.map +1 -1
- package/dist/core/secret-scan.d.ts +14 -0
- package/dist/core/secret-scan.js +42 -0
- package/dist/core/secret-scan.js.map +1 -0
- package/dist/core/shares.d.ts +33 -0
- package/dist/core/shares.js +54 -0
- package/dist/core/shares.js.map +1 -0
- package/dist/core/turn-gate.d.ts +53 -0
- package/dist/core/turn-gate.js +126 -0
- package/dist/core/turn-gate.js.map +1 -0
- package/dist/core/turn-wait.d.ts +5 -1
- package/dist/core/turn-wait.js +3 -3
- package/dist/core/turn-wait.js.map +1 -1
- package/dist/daemon/client.d.ts +26 -0
- package/dist/daemon/client.js +106 -6
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/routes/chats.js +18 -0
- package/dist/daemon/routes/chats.js.map +1 -1
- package/dist/daemon/routes/public.d.ts +1 -1
- package/dist/daemon/routes/public.js +21 -0
- package/dist/daemon/routes/public.js.map +1 -1
- package/dist/daemon/runner.js +3 -1
- package/dist/daemon/runner.js.map +1 -1
- package/docs/release-notes/v1.2.5.md +19 -0
- package/docs/release-notes/v1.2.6.md +18 -0
- package/package.json +1 -1
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Review Triage: what to read first in a turn's diff.
|
|
3
|
-
*
|
|
4
|
-
* An agent hands back twelve changed files and you have five minutes. Most of
|
|
5
|
-
* them are fine to skim; one deleted a test, one touched the auth middleware,
|
|
6
|
-
* one added a dependency. This reads the patch and ranks the files by what a
|
|
7
|
-
* reviewer would want to see first — each with the reasons, said plainly.
|
|
8
|
-
*
|
|
9
|
-
* It flags what the diff shows, never guesses at intent: "a test was removed",
|
|
10
|
-
* not "the agent cheated". Pure: the runtime calls it on every turn_diff.
|
|
11
|
-
*/
|
|
1
|
+
import { SECRET_PATTERNS } from "./secret-scan.js";
|
|
12
2
|
/** Split a unified diff (git's, plus Loom's new-file patches) into files. */
|
|
13
3
|
export function splitPatch(patch) {
|
|
14
4
|
const out = [];
|
|
@@ -51,8 +41,8 @@ const MIGRATION = /(^|\/)(migrations?|migrate|schema)\/|\.sql$|schema\.prisma$|(
|
|
|
51
41
|
const CONFIG = /(^|\/)(\.env[^/]*|tsconfig[^/]*\.json|\.eslintrc[^/]*|eslint\.config\.[a-z]+|vite\.config\.[a-z]+|next\.config\.[a-z]+|webpack\.config\.[a-z]+|jest\.config\.[a-z]+|vitest\.config\.[a-z]+|\.npmrc|\.gitignore)$/;
|
|
52
42
|
/** What an added line can do that deserves eyes, said the way a reviewer would. */
|
|
53
43
|
const RISKY = [
|
|
54
|
-
|
|
55
|
-
|
|
44
|
+
// the same secret patterns `loom orchestrate --apply` and `loom chain` check before applying
|
|
45
|
+
...SECRET_PATTERNS,
|
|
56
46
|
[/\beval\s*\(|new Function\s*\(/, "eval of a string"],
|
|
57
47
|
[/\b(child_process|execSync|spawnSync|os\.system|subprocess\.(call|run|Popen)|shell\s*=\s*True)\b/, "running shell commands"],
|
|
58
48
|
[/\brm\s+-[a-z]*r[a-z]*f|\bgit\s+push\b.*--force|\bgit\s+reset\s+--hard|\bDROP\s+(TABLE|DATABASE)\b|\bTRUNCATE\s+TABLE\b/i, "a destructive command"],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-triage.js","sourceRoot":"","sources":["../../src/core/review-triage.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"review-triage.js","sourceRoot":"","sources":["../../src/core/review-triage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets in a diff, before it's applied or pushed (#340). The same patterns review triage
|
|
3
|
+
* scores a change by: a key or token pasted into code, a hard-coded credential, or a file
|
|
4
|
+
* that is itself a secret (.env, a private key). Pure: reads a unified diff, returns findings.
|
|
5
|
+
*/
|
|
6
|
+
/** Text that looks like a secret, and how to say it. */
|
|
7
|
+
export declare const SECRET_PATTERNS: Array<[RegExp, string]>;
|
|
8
|
+
export interface SecretFinding {
|
|
9
|
+
file: string;
|
|
10
|
+
/** Line in the new file, when it's an added line. */
|
|
11
|
+
line?: number;
|
|
12
|
+
what: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function findSecrets(diff: string): SecretFinding[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets in a diff, before it's applied or pushed (#340). The same patterns review triage
|
|
3
|
+
* scores a change by: a key or token pasted into code, a hard-coded credential, or a file
|
|
4
|
+
* that is itself a secret (.env, a private key). Pure: reads a unified diff, returns findings.
|
|
5
|
+
*/
|
|
6
|
+
/** Text that looks like a secret, and how to say it. */
|
|
7
|
+
export const SECRET_PATTERNS = [
|
|
8
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----|\bAKIA[0-9A-Z]{16}\b|\b(sk|rk|pk)_(live|test)_[0-9a-zA-Z]{16,}|\bsk-[A-Za-z0-9_-]{20,}|\bgh[pousr]_[A-Za-z0-9]{30,}|\bxox[baprs]-[A-Za-z0-9-]{10,}/, "what looks like a secret key"],
|
|
9
|
+
[/\b(password|passwd|secret|api[_-]?key|token)\b\s*[:=]\s*["'][^"'\s]{6,}["']/i, "a hard-coded credential"],
|
|
10
|
+
];
|
|
11
|
+
/** Files that are secrets by name, whatever they hold. */
|
|
12
|
+
const SECRET_FILE = /(^|\/)(\.env(\.[\w-]+)?|id_(rsa|ed25519|ecdsa)|[^/]+\.(pem|p12|pfx|key))$/i;
|
|
13
|
+
/** …except the ones meant to be committed. */
|
|
14
|
+
const SAFE_FILE = /(^|\/)\.env\.(example|sample|template|defaults)$/i;
|
|
15
|
+
export function findSecrets(diff) {
|
|
16
|
+
const out = [];
|
|
17
|
+
let file = "";
|
|
18
|
+
let line = 0;
|
|
19
|
+
for (const raw of diff.split("\n")) {
|
|
20
|
+
if (raw.startsWith("+++ ")) {
|
|
21
|
+
file = raw.slice(4).replace(/^b\//, "").trim();
|
|
22
|
+
if (file !== "/dev/null" && SECRET_FILE.test(file) && !SAFE_FILE.test(file))
|
|
23
|
+
out.push({ file, what: "a file that holds secrets by its name" });
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)/.exec(raw);
|
|
27
|
+
if (hunk) {
|
|
28
|
+
line = Number(hunk[1]);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (raw.startsWith("+") && !raw.startsWith("+++")) {
|
|
32
|
+
for (const [re, what] of SECRET_PATTERNS)
|
|
33
|
+
if (re.test(raw.slice(1)))
|
|
34
|
+
out.push({ file, line, what });
|
|
35
|
+
line++;
|
|
36
|
+
}
|
|
37
|
+
else if (!raw.startsWith("-"))
|
|
38
|
+
line++;
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=secret-scan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-scan.js","sourceRoot":"","sources":["../../src/core/secret-scan.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wDAAwD;AACxD,MAAM,CAAC,MAAM,eAAe,GAA4B;IACtD,CAAC,uLAAuL,EAAE,8BAA8B,CAAC;IACzN,CAAC,8EAA8E,EAAE,yBAAyB,CAAC;CAC5G,CAAC;AAEF,0DAA0D;AAC1D,MAAM,WAAW,GAAG,4EAA4E,CAAC;AACjG,8CAA8C;AAC9C,MAAM,SAAS,GAAG,mDAAmD,CAAC;AAStE,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,GAAG,GAAoB,EAAE,CAAC;IAChC,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YAC/C,IAAI,IAAI,KAAK,WAAW,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,uCAAuC,EAAE,CAAC,CAAC;YAC/I,SAAS;QACX,CAAC;QACD,MAAM,IAAI,GAAG,2BAA2B,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACnD,IAAI,IAAI,EAAE,CAAC;YAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YAAC,SAAS;QAAC,CAAC;QAC/C,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YAClD,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,eAAe;gBAAE,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;oBAAE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACpG,IAAI,EAAE,CAAC;QACT,CAAC;aAAM,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,IAAI,EAAE,CAAC;IAC1C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only thread links (#199): a teammate opens a link and reads a chat — prompts, replies,
|
|
3
|
+
* what ran, what changed — as the replay page (core/replay.ts), without pairing or a token of
|
|
4
|
+
* their own. Each link is a random token that expires and can be revoked; only its SHA-256 is
|
|
5
|
+
* kept on disk, so the shares file alone opens nothing. ~/.loom/shares.json.
|
|
6
|
+
*/
|
|
7
|
+
export interface Share {
|
|
8
|
+
/** Short id to list and revoke by (not the token). */
|
|
9
|
+
id: string;
|
|
10
|
+
/** sha256 of the token. */
|
|
11
|
+
hash: string;
|
|
12
|
+
projectId: string;
|
|
13
|
+
chat: string;
|
|
14
|
+
title: string;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
expiresAt: number;
|
|
17
|
+
}
|
|
18
|
+
export declare const MAX_SHARE_HOURS: number;
|
|
19
|
+
/** A new link for one chat, good for `hours` (1 h to 30 days). The token is returned once. */
|
|
20
|
+
export declare function createShare(input: {
|
|
21
|
+
projectId: string;
|
|
22
|
+
chat: string;
|
|
23
|
+
title: string;
|
|
24
|
+
hours?: number;
|
|
25
|
+
}, now?: number): {
|
|
26
|
+
share: Share;
|
|
27
|
+
token: string;
|
|
28
|
+
};
|
|
29
|
+
/** The share a token opens — null when there's none, or it expired or was revoked. */
|
|
30
|
+
export declare function openShare(token: string, now?: number): Share | null;
|
|
31
|
+
export declare function listShares(now?: number): Share[];
|
|
32
|
+
/** Revoke by id; true when there was one. */
|
|
33
|
+
export declare function revokeShare(id: string): boolean;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only thread links (#199): a teammate opens a link and reads a chat — prompts, replies,
|
|
3
|
+
* what ran, what changed — as the replay page (core/replay.ts), without pairing or a token of
|
|
4
|
+
* their own. Each link is a random token that expires and can be revoked; only its SHA-256 is
|
|
5
|
+
* kept on disk, so the shares file alone opens nothing. ~/.loom/shares.json.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { loomHome } from "./registry.js";
|
|
11
|
+
export const MAX_SHARE_HOURS = 24 * 30;
|
|
12
|
+
const file = () => path.join(loomHome(), "shares.json");
|
|
13
|
+
const sha = (token) => createHash("sha256").update(token).digest("hex");
|
|
14
|
+
function load() {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(fs.readFileSync(file(), "utf8")).filter((s) => s && s.hash);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function save(shares) {
|
|
23
|
+
fs.mkdirSync(path.dirname(file()), { recursive: true });
|
|
24
|
+
fs.writeFileSync(file(), JSON.stringify(shares, null, 2) + "\n", { mode: 0o600 });
|
|
25
|
+
}
|
|
26
|
+
/** A new link for one chat, good for `hours` (1 h to 30 days). The token is returned once. */
|
|
27
|
+
export function createShare(input, now = Date.now()) {
|
|
28
|
+
const hours = Math.min(MAX_SHARE_HOURS, Math.max(1, Math.round(input.hours ?? 24)));
|
|
29
|
+
const token = randomBytes(24).toString("base64url");
|
|
30
|
+
const share = { id: randomBytes(4).toString("hex"), hash: sha(token), projectId: input.projectId, chat: input.chat,
|
|
31
|
+
title: input.title, createdAt: now, expiresAt: now + hours * 3_600_000 };
|
|
32
|
+
save([...load().filter((s) => s.expiresAt > now), share]);
|
|
33
|
+
return { share, token };
|
|
34
|
+
}
|
|
35
|
+
/** The share a token opens — null when there's none, or it expired or was revoked. */
|
|
36
|
+
export function openShare(token, now = Date.now()) {
|
|
37
|
+
if (!/^[A-Za-z0-9_-]{20,64}$/.test(token))
|
|
38
|
+
return null;
|
|
39
|
+
const h = sha(token);
|
|
40
|
+
return load().find((s) => s.hash === h && s.expiresAt > now) ?? null;
|
|
41
|
+
}
|
|
42
|
+
export function listShares(now = Date.now()) {
|
|
43
|
+
return load().filter((s) => s.expiresAt > now).sort((a, b) => b.createdAt - a.createdAt);
|
|
44
|
+
}
|
|
45
|
+
/** Revoke by id; true when there was one. */
|
|
46
|
+
export function revokeShare(id) {
|
|
47
|
+
const all = load();
|
|
48
|
+
const left = all.filter((s) => s.id !== id);
|
|
49
|
+
if (left.length === all.length)
|
|
50
|
+
return false;
|
|
51
|
+
save(left);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=shares.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shares.js","sourceRoot":"","sources":["../../src/core/shares.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAczC,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,GAAG,EAAE,CAAC;AACvC,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,aAAa,CAAC,CAAC;AACxD,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAEhF,SAAS,IAAI;IACX,IAAI,CAAC;QAAC,OAAQ,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,CAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,EAAE,CAAC;IAAC,CAAC;AAC1H,CAAC;AACD,SAAS,IAAI,CAAC,MAAe;IAC3B,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AACpF,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,WAAW,CAAC,KAAyE,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IACrH,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACpF,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IACpD,MAAM,KAAK,GAAU,EAAE,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI;QACvH,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,GAAG,KAAK,GAAG,SAAS,EAAE,CAAC;IAC3E,IAAI,CAAC,CAAC,GAAG,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IAC1D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC1B,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IACvD,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACvD,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC;IACrB,OAAO,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,IAAI,IAAI,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IACzC,OAAO,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC;AAC3F,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,WAAW,CAAC,EAAU;IACpC,MAAM,GAAG,GAAG,IAAI,EAAE,CAAC;IACnB,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC5C,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC7C,IAAI,CAAC,IAAI,CAAC,CAAC;IACX,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The daemon-wide turn gate (#339): how many agent turns may run at once across every
|
|
3
|
+
* project, and how much memory must be free before another starts. Six turns at once —
|
|
4
|
+
* each starting a dev server and headless Chrome — exhausted 16 GB and reset the Mac;
|
|
5
|
+
* past the cap, a turn waits for a slot instead of piling on.
|
|
6
|
+
*
|
|
7
|
+
* Limits live in ~/.loom/limits.json (`loom limits` sets them) and are re-read every few
|
|
8
|
+
* seconds, so a change applies without a restart. A turn never waits forever: some turns
|
|
9
|
+
* wait on others (a parent on its subtask), so after MAX_WAIT_MS it goes ahead and says so.
|
|
10
|
+
*/
|
|
11
|
+
export interface TurnLimits {
|
|
12
|
+
/** Turns at once across all projects; 0 = no cap. */
|
|
13
|
+
maxTurns: number;
|
|
14
|
+
/** Don't start a turn while less than this % of memory is available; 0 = no memory gate. */
|
|
15
|
+
minFreePct: number;
|
|
16
|
+
}
|
|
17
|
+
/** One turn per 4 GB (at least 2), and 15% of memory kept free. */
|
|
18
|
+
export declare function defaultLimits(totalBytes?: number): TurnLimits;
|
|
19
|
+
export declare const limitsFile: () => string;
|
|
20
|
+
export declare function readLimits(): TurnLimits;
|
|
21
|
+
export declare function writeLimits(patch: Partial<TurnLimits>): TurnLimits;
|
|
22
|
+
/**
|
|
23
|
+
* % of memory available to start something new. On macOS os.freemem() counts only pages
|
|
24
|
+
* nobody has touched (a few % on a busy Mac), so read vm_stat: free + inactive + speculative
|
|
25
|
+
* + purgeable pages are what the kernel hands out without swapping.
|
|
26
|
+
*/
|
|
27
|
+
export declare function availableMemoryPct(): number;
|
|
28
|
+
export interface GateDeps {
|
|
29
|
+
limits(): TurnLimits;
|
|
30
|
+
freePct(): number;
|
|
31
|
+
now(): number;
|
|
32
|
+
sleep(ms: number): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/** A counting gate with a memory check; one per daemon process (turnGate below). */
|
|
35
|
+
export declare class TurnGate {
|
|
36
|
+
private deps;
|
|
37
|
+
private running;
|
|
38
|
+
private seq;
|
|
39
|
+
private cache;
|
|
40
|
+
constructor(deps?: GateDeps);
|
|
41
|
+
private limits;
|
|
42
|
+
/** What's running now (labels), for status. */
|
|
43
|
+
active(): string[];
|
|
44
|
+
/** Why a turn can't start right now, or null when it can. */
|
|
45
|
+
private blocker;
|
|
46
|
+
/**
|
|
47
|
+
* Wait for a slot; resolves with release(). `onWait` hears why it's waiting (once, and
|
|
48
|
+
* again if the reason changes), so the thread can say "waiting for a free slot".
|
|
49
|
+
*/
|
|
50
|
+
acquire(label: string, onWait?: (why: string) => void): Promise<() => void>;
|
|
51
|
+
}
|
|
52
|
+
/** The daemon's gate: every adapter turn passes through it (adapters/index.ts). */
|
|
53
|
+
export declare const turnGate: TurnGate;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The daemon-wide turn gate (#339): how many agent turns may run at once across every
|
|
3
|
+
* project, and how much memory must be free before another starts. Six turns at once —
|
|
4
|
+
* each starting a dev server and headless Chrome — exhausted 16 GB and reset the Mac;
|
|
5
|
+
* past the cap, a turn waits for a slot instead of piling on.
|
|
6
|
+
*
|
|
7
|
+
* Limits live in ~/.loom/limits.json (`loom limits` sets them) and are re-read every few
|
|
8
|
+
* seconds, so a change applies without a restart. A turn never waits forever: some turns
|
|
9
|
+
* wait on others (a parent on its subtask), so after MAX_WAIT_MS it goes ahead and says so.
|
|
10
|
+
*/
|
|
11
|
+
import { execFileSync } from "node:child_process";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { loomHome } from "./registry.js";
|
|
16
|
+
/** One turn per 4 GB (at least 2), and 15% of memory kept free. */
|
|
17
|
+
export function defaultLimits(totalBytes = os.totalmem()) {
|
|
18
|
+
return { maxTurns: Math.max(2, Math.floor(totalBytes / 4 / 1024 ** 3)), minFreePct: 15 };
|
|
19
|
+
}
|
|
20
|
+
export const limitsFile = () => path.join(loomHome(), "limits.json");
|
|
21
|
+
export function readLimits() {
|
|
22
|
+
const base = defaultLimits();
|
|
23
|
+
try {
|
|
24
|
+
const raw = JSON.parse(fs.readFileSync(limitsFile(), "utf8"));
|
|
25
|
+
return {
|
|
26
|
+
maxTurns: Number.isFinite(raw.maxTurns) && raw.maxTurns >= 0 ? Math.floor(raw.maxTurns) : base.maxTurns,
|
|
27
|
+
minFreePct: Number.isFinite(raw.minFreePct) && raw.minFreePct >= 0 && raw.minFreePct < 100 ? raw.minFreePct : base.minFreePct,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return base;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function writeLimits(patch) {
|
|
35
|
+
const next = { ...readLimits(), ...patch };
|
|
36
|
+
fs.mkdirSync(path.dirname(limitsFile()), { recursive: true });
|
|
37
|
+
fs.writeFileSync(limitsFile(), JSON.stringify(next, null, 2) + "\n");
|
|
38
|
+
return next;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* % of memory available to start something new. On macOS os.freemem() counts only pages
|
|
42
|
+
* nobody has touched (a few % on a busy Mac), so read vm_stat: free + inactive + speculative
|
|
43
|
+
* + purgeable pages are what the kernel hands out without swapping.
|
|
44
|
+
*/
|
|
45
|
+
export function availableMemoryPct() {
|
|
46
|
+
if (process.platform === "darwin") {
|
|
47
|
+
try {
|
|
48
|
+
const out = execFileSync("vm_stat", { encoding: "utf8", timeout: 2000 });
|
|
49
|
+
const size = Number(/page size of (\d+) bytes/.exec(out)?.[1] ?? 16384);
|
|
50
|
+
const pages = (name) => Number(new RegExp(`Pages ${name}:\\s+(\\d+)`).exec(out)?.[1] ?? 0);
|
|
51
|
+
const free = (pages("free") + pages("inactive") + pages("speculative") + pages("purgeable")) * size;
|
|
52
|
+
return Math.min(100, (free / os.totalmem()) * 100);
|
|
53
|
+
}
|
|
54
|
+
catch { /* fall through */ }
|
|
55
|
+
}
|
|
56
|
+
return (os.freemem() / os.totalmem()) * 100;
|
|
57
|
+
}
|
|
58
|
+
const MAX_WAIT_MS = Number(process.env.LOOM_TURN_GATE_MAX_WAIT_MS) || 10 * 60_000;
|
|
59
|
+
const POLL_MS = Number(process.env.LOOM_TURN_GATE_POLL_MS) || 1000;
|
|
60
|
+
/** A counting gate with a memory check; one per daemon process (turnGate below). */
|
|
61
|
+
export class TurnGate {
|
|
62
|
+
deps;
|
|
63
|
+
running = new Set();
|
|
64
|
+
seq = 0;
|
|
65
|
+
cache = null;
|
|
66
|
+
constructor(deps = {
|
|
67
|
+
limits: readLimits, freePct: availableMemoryPct, now: Date.now, sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
68
|
+
}) {
|
|
69
|
+
this.deps = deps;
|
|
70
|
+
}
|
|
71
|
+
limits() {
|
|
72
|
+
const now = this.deps.now();
|
|
73
|
+
if (!this.cache || now - this.cache.at > 5000)
|
|
74
|
+
this.cache = { at: now, limits: this.deps.limits() };
|
|
75
|
+
return this.cache.limits;
|
|
76
|
+
}
|
|
77
|
+
/** What's running now (labels), for status. */
|
|
78
|
+
active() {
|
|
79
|
+
return [...this.running].map((k) => k.slice(k.indexOf(":") + 1));
|
|
80
|
+
}
|
|
81
|
+
/** Why a turn can't start right now, or null when it can. */
|
|
82
|
+
blocker() {
|
|
83
|
+
const { maxTurns, minFreePct } = this.limits();
|
|
84
|
+
if (maxTurns > 0 && this.running.size >= maxTurns)
|
|
85
|
+
return `${this.running.size} turns are running (the cap is ${maxTurns})`;
|
|
86
|
+
if (minFreePct > 0) {
|
|
87
|
+
const free = this.deps.freePct();
|
|
88
|
+
// the first turn always runs: a gate that blocks everything on a full machine is a hang
|
|
89
|
+
if (free < minFreePct && this.running.size > 0)
|
|
90
|
+
return `only ${Math.round(free)}% of memory is free (Loom keeps ${minFreePct}%)`;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Wait for a slot; resolves with release(). `onWait` hears why it's waiting (once, and
|
|
96
|
+
* again if the reason changes), so the thread can say "waiting for a free slot".
|
|
97
|
+
*/
|
|
98
|
+
async acquire(label, onWait) {
|
|
99
|
+
const started = this.deps.now();
|
|
100
|
+
let said = null;
|
|
101
|
+
for (;;) {
|
|
102
|
+
const why = this.blocker();
|
|
103
|
+
if (!why)
|
|
104
|
+
break;
|
|
105
|
+
if (this.deps.now() - started > MAX_WAIT_MS) {
|
|
106
|
+
onWait?.(`waited ${Math.round(MAX_WAIT_MS / 60_000)} min for a free slot — starting anyway`);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
if (why.replace(/\d+%/, "") !== said?.replace(/\d+%/, "")) {
|
|
110
|
+
said = why;
|
|
111
|
+
onWait?.(why);
|
|
112
|
+
}
|
|
113
|
+
await this.deps.sleep(POLL_MS);
|
|
114
|
+
}
|
|
115
|
+
const key = `${++this.seq}:${label}`;
|
|
116
|
+
this.running.add(key);
|
|
117
|
+
let released = false;
|
|
118
|
+
return () => { if (!released) {
|
|
119
|
+
released = true;
|
|
120
|
+
this.running.delete(key);
|
|
121
|
+
} };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** The daemon's gate: every adapter turn passes through it (adapters/index.ts). */
|
|
125
|
+
export const turnGate = new TurnGate();
|
|
126
|
+
//# sourceMappingURL=turn-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-gate.js","sourceRoot":"","sources":["../../src/core/turn-gate.ts"],"names":[],"mappings":"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"}
|
package/dist/core/turn-wait.d.ts
CHANGED
|
@@ -9,7 +9,11 @@ export interface TurnOutcome {
|
|
|
9
9
|
status: TurnStatus;
|
|
10
10
|
agent: string;
|
|
11
11
|
reply: string;
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* What the turn cost, or null when the agent didn't say. A subscription CLI (codex on a
|
|
14
|
+
* ChatGPT plan, …) reports nothing or $0 — and "$0" read as "free" (field feedback).
|
|
15
|
+
*/
|
|
16
|
+
costUsd: number | null;
|
|
13
17
|
durationMs?: number;
|
|
14
18
|
error?: string;
|
|
15
19
|
question?: string;
|
package/dist/core/turn-wait.js
CHANGED
|
@@ -17,11 +17,11 @@ export function turnOutcome(events, agentId, sinceId, chat) {
|
|
|
17
17
|
else if (e.kind === "status" && p.state === "turn_cost")
|
|
18
18
|
costUsd += Number(p.costUsd ?? 0) || 0;
|
|
19
19
|
else if (e.kind === "needs_input")
|
|
20
|
-
return { status: "needs_input", agent: agentId, reply, costUsd, question: String(p.question ?? "") };
|
|
20
|
+
return { status: "needs_input", agent: agentId, reply, costUsd: costUsd || null, question: String(p.question ?? "") };
|
|
21
21
|
else if (e.kind === "error")
|
|
22
|
-
return { status: "error", agent: agentId, reply, costUsd, error: String(p.message ?? "the turn failed") };
|
|
22
|
+
return { status: "error", agent: agentId, reply, costUsd: costUsd || null, error: String(p.message ?? "the turn failed") };
|
|
23
23
|
else if (e.kind === "run_complete") {
|
|
24
|
-
const out = { status: p.error ? "error" : "ok", agent: agentId, reply, costUsd: costUsd || Number(p.costUsd ?? 0) ||
|
|
24
|
+
const out = { status: p.error ? "error" : "ok", agent: agentId, reply, costUsd: costUsd || Number(p.costUsd ?? 0) || null,
|
|
25
25
|
...(typeof p.durationMs === "number" ? { durationMs: p.durationMs } : {}) };
|
|
26
26
|
if (p.error)
|
|
27
27
|
out.error = String(p.error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn-wait.js","sourceRoot":"","sources":["../../src/core/turn-wait.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"turn-wait.js","sourceRoot":"","sources":["../../src/core/turn-wait.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAoBH,oGAAoG;AACpG,MAAM,UAAU,WAAW,CAAC,MAAmB,EAAE,OAAe,EAAE,OAAe,EAAE,IAAa;IAC9F,IAAI,KAAK,GAAG,EAAE,EAAE,OAAO,GAAG,CAAC,CAAC;IAC5B,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,CAAC,EAAE,IAAI,OAAO,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO;YAAE,SAAS;QACvD,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,MAAM,CAAC,KAAK,IAAI;YAAE,SAAS;QAChE,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC;QAC1B,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,SAAS,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;YAAE,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC;aAClF,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,KAAK,KAAK,WAAW;YAAE,OAAO,IAAI,MAAM,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;aAC3F,IAAI,CAAC,CAAC,IAAI,KAAK,aAAa;YAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,EAAE,CAAC;aACpJ,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO;YAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,IAAI,iBAAiB,CAAC,EAAE,CAAC;aACnJ,IAAI,CAAC,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;YACnC,MAAM,GAAG,GAAgB,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI,MAAM,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,IAAI,IAAI;gBACpI,GAAG,CAAC,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;YAC9E,IAAI,CAAC,CAAC,KAAK;gBAAE,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YACzC,OAAO,GAAG,CAAC;QACb,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,WAAW,CAAC,CAAqB;IAC/C,IAAI,CAAC,CAAC;QAAE,OAAO,GAAG,CAAC;IACnB,OAAO,CAAC,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpE,CAAC"}
|
package/dist/daemon/client.d.ts
CHANGED
|
@@ -730,6 +730,30 @@ export declare class DaemonClient {
|
|
|
730
730
|
finding: import("../core/review.js").Finding;
|
|
731
731
|
decision: string;
|
|
732
732
|
}>;
|
|
733
|
+
shareChat(id: string, chat: string, hours?: number): Promise<{
|
|
734
|
+
id: string;
|
|
735
|
+
url: string;
|
|
736
|
+
expiresAt: number;
|
|
737
|
+
title: string;
|
|
738
|
+
}>;
|
|
739
|
+
shares(): Promise<{
|
|
740
|
+
shares: Array<{
|
|
741
|
+
id: string;
|
|
742
|
+
projectId: string;
|
|
743
|
+
chat: string;
|
|
744
|
+
title: string;
|
|
745
|
+
createdAt: number;
|
|
746
|
+
expiresAt: number;
|
|
747
|
+
}>;
|
|
748
|
+
}>;
|
|
749
|
+
revokeShare(shareId: string): Promise<{
|
|
750
|
+
revoked: boolean;
|
|
751
|
+
}>;
|
|
752
|
+
setAgentPermissions(id: string, agentId: string, permissions: string): Promise<{
|
|
753
|
+
agent: {
|
|
754
|
+
permissions?: string;
|
|
755
|
+
};
|
|
756
|
+
}>;
|
|
733
757
|
repairDecisions(id: string, opts?: {
|
|
734
758
|
dryRun?: boolean;
|
|
735
759
|
}): Promise<{
|
|
@@ -841,6 +865,8 @@ export declare function desktopOwnsDaemon(cfg?: DaemonConfig | null): Promise<bo
|
|
|
841
865
|
* stale daemons are the classic "failed to fetch / missing route" cause.
|
|
842
866
|
*/
|
|
843
867
|
export declare function ensureDaemon(): Promise<DaemonClient>;
|
|
868
|
+
/** "project: agent" for every agent mid-turn on this daemon (none when it can't be asked). */
|
|
869
|
+
export declare function turnsInFlight(client: DaemonClient): Promise<string[]>;
|
|
844
870
|
export declare function daemonRunning(): Promise<DaemonConfig | null>;
|
|
845
871
|
export declare function stopDaemon(): Promise<boolean>;
|
|
846
872
|
export interface TeamBrainView {
|
package/dist/daemon/client.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* Also owns daemon lifecycle: autostart-on-demand, health, shutdown.
|
|
4
4
|
*/
|
|
5
5
|
import { spawn } from "../core/process.js";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
6
8
|
import { fileURLToPath } from "node:url";
|
|
7
9
|
import WebSocket from "ws";
|
|
8
|
-
import { readDaemonConfig, } from "../core/registry.js";
|
|
10
|
+
import { loomHome, readDaemonConfig, } from "../core/registry.js";
|
|
9
11
|
import { BUILD_REV } from "./server.js";
|
|
10
12
|
export class DaemonError extends Error {
|
|
11
13
|
status;
|
|
@@ -492,6 +494,18 @@ export class DaemonClient {
|
|
|
492
494
|
decideFinding(id, findingId, decision, note) {
|
|
493
495
|
return this.request("POST", `/api/projects/${encodeURIComponent(id)}/findings/${encodeURIComponent(findingId)}`, { decision, ...(note ? { note } : {}) });
|
|
494
496
|
}
|
|
497
|
+
shareChat(id, chat, hours) {
|
|
498
|
+
return this.request("POST", `/api/projects/${encodeURIComponent(id)}/chats/${encodeURIComponent(chat)}/share`, hours ? { hours } : {});
|
|
499
|
+
}
|
|
500
|
+
shares() {
|
|
501
|
+
return this.request("GET", "/api/shares");
|
|
502
|
+
}
|
|
503
|
+
revokeShare(shareId) {
|
|
504
|
+
return this.request("DELETE", `/api/shares/${encodeURIComponent(shareId)}`);
|
|
505
|
+
}
|
|
506
|
+
setAgentPermissions(id, agentId, permissions) {
|
|
507
|
+
return this.request("POST", `/api/projects/${encodeURIComponent(id)}/agents/${encodeURIComponent(agentId)}/permissions`, { permissions });
|
|
508
|
+
}
|
|
495
509
|
repairDecisions(id, opts = {}) {
|
|
496
510
|
return this.request("POST", `/api/projects/${encodeURIComponent(id)}/brain/repair-decisions`, opts.dryRun ? { dryRun: true } : {});
|
|
497
511
|
}
|
|
@@ -626,6 +640,9 @@ export async function desktopOwnsDaemon(cfg = readDaemonConfig()) {
|
|
|
626
640
|
* stale daemons are the classic "failed to fetch / missing route" cause.
|
|
627
641
|
*/
|
|
628
642
|
export async function ensureDaemon() {
|
|
643
|
+
// another loom command is swapping the daemon for a newer build: wait for it rather than
|
|
644
|
+
// reach the one going away (or start a second) — a script's next step lands mid-restart
|
|
645
|
+
await waitForRestart();
|
|
629
646
|
let cfg = readDaemonConfig();
|
|
630
647
|
if (cfg) {
|
|
631
648
|
const probe = await healthProbe(cfg);
|
|
@@ -637,14 +654,84 @@ export async function ensureDaemon() {
|
|
|
637
654
|
process.stderr.write(`loom: Loom Desktop runs a different build (${probe.rev ?? "?"}) than this CLI (${BUILD_REV}) — update one so they match; using Desktop's\n`);
|
|
638
655
|
return new DaemonClient(cfg);
|
|
639
656
|
}
|
|
640
|
-
// Stale build — restart it in place
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
657
|
+
// Stale build — restart it in place, unless agents are mid-turn on it: a restart kills
|
|
658
|
+
// their turns (seen in the field: three long `send --wait` runs died on an update).
|
|
659
|
+
// Use the old build as it is and say how to restart once they're done.
|
|
660
|
+
const busy = await turnsInFlight(new DaemonClient(cfg));
|
|
661
|
+
if (busy.length) {
|
|
662
|
+
process.stderr.write(`loom: the daemon runs an older build (${probe.rev ?? "?"}) than this CLI (${BUILD_REV}), but ${busy.length} turn${busy.length === 1 ? " is" : "s are"} running (${busy.slice(0, 3).join(", ")}${busy.length > 3 ? ", …" : ""}) — ` +
|
|
663
|
+
"using it as it is; restart when they finish: loom up --restart\n");
|
|
664
|
+
return new DaemonClient(cfg);
|
|
665
|
+
}
|
|
666
|
+
// hold the restart lock until the new daemon answers; someone else holding it is restarting already
|
|
667
|
+
if (!takeRestartLock()) {
|
|
668
|
+
await waitForRestart();
|
|
669
|
+
const now = readDaemonConfig();
|
|
670
|
+
if (now && (await healthy(now)))
|
|
671
|
+
return new DaemonClient(now);
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
try {
|
|
675
|
+
await stopDaemon();
|
|
676
|
+
const gone = Date.now() + 5000;
|
|
677
|
+
while (Date.now() < gone && (await healthy(cfg))) {
|
|
678
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
679
|
+
}
|
|
680
|
+
return await startDaemon();
|
|
681
|
+
}
|
|
682
|
+
finally {
|
|
683
|
+
releaseRestartLock();
|
|
684
|
+
}
|
|
645
685
|
}
|
|
646
686
|
}
|
|
647
687
|
}
|
|
688
|
+
return startDaemon();
|
|
689
|
+
}
|
|
690
|
+
const RESTART_LOCK_MS = 30_000;
|
|
691
|
+
const restartLockFile = () => path.join(loomHome(), "daemon.restarting");
|
|
692
|
+
/** Mark "this CLI is restarting the daemon"; false when another one already is (a fresh lock). */
|
|
693
|
+
function takeRestartLock() {
|
|
694
|
+
const file = restartLockFile();
|
|
695
|
+
try {
|
|
696
|
+
fs.writeFileSync(file, JSON.stringify({ pid: process.pid, at: Date.now() }), { flag: "wx" });
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
// a lock left by a CLI that died mid-restart is stale after RESTART_LOCK_MS: take it over
|
|
701
|
+
try {
|
|
702
|
+
if (Date.now() - fs.statSync(file).mtimeMs > RESTART_LOCK_MS) {
|
|
703
|
+
fs.writeFileSync(file, JSON.stringify({ pid: process.pid, at: Date.now() }));
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
catch { /* gone in between: someone finished */ }
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function releaseRestartLock() {
|
|
712
|
+
try {
|
|
713
|
+
fs.rmSync(restartLockFile(), { force: true });
|
|
714
|
+
}
|
|
715
|
+
catch { /* already gone */ }
|
|
716
|
+
}
|
|
717
|
+
/** Wait while another loom command holds a fresh restart lock (at most RESTART_LOCK_MS). */
|
|
718
|
+
async function waitForRestart() {
|
|
719
|
+
const file = restartLockFile();
|
|
720
|
+
const until = Date.now() + RESTART_LOCK_MS;
|
|
721
|
+
while (Date.now() < until) {
|
|
722
|
+
try {
|
|
723
|
+
if (Date.now() - fs.statSync(file).mtimeMs > RESTART_LOCK_MS)
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
catch {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/** Start a detached daemon and wait for it to answer. */
|
|
733
|
+
async function startDaemon() {
|
|
734
|
+
let cfg = null;
|
|
648
735
|
const compiled = fileURLToPath(new URL("../cli/index.js", import.meta.url));
|
|
649
736
|
const devSource = fileURLToPath(new URL("../cli/index.ts", import.meta.url));
|
|
650
737
|
const { existsSync } = await import("node:fs");
|
|
@@ -667,6 +754,19 @@ export async function ensureDaemon() {
|
|
|
667
754
|
}
|
|
668
755
|
throw new Error("could not start the loom daemon (try `loom daemon` in the foreground)");
|
|
669
756
|
}
|
|
757
|
+
/** "project: agent" for every agent mid-turn on this daemon (none when it can't be asked). */
|
|
758
|
+
export async function turnsInFlight(client) {
|
|
759
|
+
try {
|
|
760
|
+
const { projects } = await client.listProjects();
|
|
761
|
+
return projects.flatMap((p) => [
|
|
762
|
+
...(p.agents ?? []).filter((a) => a.busy).map((a) => `${p.name}: ${a.id}`),
|
|
763
|
+
...(p.activity ?? []).map((what) => `${p.name}: ${what}`),
|
|
764
|
+
]);
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
return [];
|
|
768
|
+
}
|
|
769
|
+
}
|
|
670
770
|
export async function daemonRunning() {
|
|
671
771
|
const cfg = readDaemonConfig();
|
|
672
772
|
if (cfg && (await healthy(cfg)))
|