@brutalsystems/muster 3.3.0 → 3.3.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/dist/guard.js +50 -5
- package/dist/run.js +10 -1
- package/package.json +1 -1
package/dist/guard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, realpathSync, } from "node:fs";
|
|
4
4
|
import { realpath } from "node:fs/promises";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
@@ -425,16 +425,24 @@ export function resolveResume(req) {
|
|
|
425
425
|
* tincan and the ledger could not say which one a message reaches. A fork
|
|
426
426
|
* only reads the original, so it may resume a live one.
|
|
427
427
|
*/
|
|
428
|
-
export function assertResumable(resume, env, alive = pidAlive) {
|
|
428
|
+
export function assertResumable(resume, env, cwd, alive = pidAlive) {
|
|
429
429
|
const configDir = claudeConfigDir(env);
|
|
430
430
|
const projects = join(configDir, "projects");
|
|
431
|
-
let
|
|
431
|
+
let transcript;
|
|
432
432
|
try {
|
|
433
|
-
|
|
433
|
+
transcript = readdirSync(projects)
|
|
434
|
+
.map((dir) => join(projects, dir, resume.id + ".jsonl"))
|
|
435
|
+
.find((file) => existsSync(file));
|
|
434
436
|
}
|
|
435
437
|
catch { }
|
|
436
|
-
if (!
|
|
438
|
+
if (!transcript)
|
|
437
439
|
throw new Error(`No Claude conversation ${resume.id} in ${configDir}; --resume reopens a session recorded in the profile this launch runs in`);
|
|
440
|
+
// Claude 2.1.302 resumes a conversation from any directory, so this is
|
|
441
|
+
// muster's to refuse (#10): the history would describe one tree while the
|
|
442
|
+
// agent works in another. Unknown when the transcript names no cwd.
|
|
443
|
+
const recorded = transcriptCwd(transcript);
|
|
444
|
+
if (recorded !== undefined && realPath(recorded) !== realPath(cwd))
|
|
445
|
+
throw new Error(`Claude session ${resume.id} was recorded in ${recorded}, not ${cwd}; resume it with --cwd ${recorded}`);
|
|
438
446
|
if (resume.fork)
|
|
439
447
|
return;
|
|
440
448
|
const sessions = claudeSessionsDir(env);
|
|
@@ -458,6 +466,43 @@ export function assertResumable(resume, env, alive = pidAlive) {
|
|
|
458
466
|
throw new Error(`Claude session ${resume.id} is running (pid ${pid}); stop it before resuming it, or pass --fork-session to continue it under a new id`);
|
|
459
467
|
}
|
|
460
468
|
}
|
|
469
|
+
/** The first `cwd` a transcript records, read from its head: a transcript can
|
|
470
|
+
* run to many megabytes, and the cwd is on its first message. */
|
|
471
|
+
function transcriptCwd(file) {
|
|
472
|
+
let head = "";
|
|
473
|
+
try {
|
|
474
|
+
const fd = openSync(file, "r");
|
|
475
|
+
try {
|
|
476
|
+
const buf = Buffer.alloc(1 << 20);
|
|
477
|
+
head = buf.toString("utf8", 0, readSync(fd, buf, 0, buf.length, 0));
|
|
478
|
+
}
|
|
479
|
+
finally {
|
|
480
|
+
closeSync(fd);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
for (const line of head.split("\n")) {
|
|
487
|
+
if (!line.includes('"cwd"'))
|
|
488
|
+
continue;
|
|
489
|
+
try {
|
|
490
|
+
const { cwd } = JSON.parse(line);
|
|
491
|
+
if (typeof cwd === "string" && cwd)
|
|
492
|
+
return cwd;
|
|
493
|
+
}
|
|
494
|
+
catch { }
|
|
495
|
+
}
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
498
|
+
function realPath(path) {
|
|
499
|
+
try {
|
|
500
|
+
return realpathSync(path);
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return resolve(path);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
461
506
|
function pidAlive(pid) {
|
|
462
507
|
try {
|
|
463
508
|
process.kill(pid, 0);
|
package/dist/run.js
CHANGED
|
@@ -802,6 +802,15 @@ export class Muster {
|
|
|
802
802
|
? await resolveOpenCode(root.pid, entry.server_url, req.cwd, startedAt, deadline, req.prompt)
|
|
803
803
|
: await resolveClaude(root.pid, resolveEnv, deadline);
|
|
804
804
|
if (identity) {
|
|
805
|
+
// The answer #10 asks for: a resume that registered some other
|
|
806
|
+
// session is a launch that failed, never a new session reported as
|
|
807
|
+
// the old one. Not fatal at once, like every other diagnosis here,
|
|
808
|
+
// in case the registry is read mid-write.
|
|
809
|
+
const resume = req.runtime === "claude" ? resolveResume(req) : null;
|
|
810
|
+
if (resume && (identity.id === resume.id) === resume.fork)
|
|
811
|
+
throw new Error(resume.fork
|
|
812
|
+
? `--fork-session registered the original session ${resume.id} instead of a new one`
|
|
813
|
+
: `Claude registered session ${identity.id} instead of resuming ${resume.id}`);
|
|
805
814
|
diagnostic =
|
|
806
815
|
req.runtime === "codex"
|
|
807
816
|
? "thread id reserved but no rollout"
|
|
@@ -923,7 +932,7 @@ export class Muster {
|
|
|
923
932
|
throw new Error("--resume is available to a local requester only");
|
|
924
933
|
if (identityName)
|
|
925
934
|
throw new Error("--resume cannot be combined with an identity: the identity's config dir holds none of this profile's conversations");
|
|
926
|
-
assertResumable(resume, agentBase);
|
|
935
|
+
assertResumable(resume, agentBase, effective.cwd);
|
|
927
936
|
}
|
|
928
937
|
let argv = plannedArgv;
|
|
929
938
|
let codexPolicy = [];
|