sisyphi 0.1.4 → 0.1.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/dist/daemon.js
CHANGED
|
@@ -20,12 +20,13 @@ import {
|
|
|
20
20
|
} from "./chunk-N2BPQOO2.js";
|
|
21
21
|
|
|
22
22
|
// src/daemon/index.ts
|
|
23
|
-
import { mkdirSync as mkdirSync5, readFileSync as
|
|
23
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2, existsSync as existsSync6 } from "fs";
|
|
24
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
24
25
|
|
|
25
26
|
// src/shared/config.ts
|
|
26
27
|
import { readFileSync } from "fs";
|
|
27
28
|
var DEFAULT_CONFIG = {
|
|
28
|
-
pollIntervalMs:
|
|
29
|
+
pollIntervalMs: 5e3
|
|
29
30
|
};
|
|
30
31
|
function readJsonFile(filePath) {
|
|
31
32
|
try {
|
|
@@ -43,23 +44,20 @@ function loadConfig(cwd) {
|
|
|
43
44
|
|
|
44
45
|
// src/daemon/server.ts
|
|
45
46
|
import { createServer } from "net";
|
|
46
|
-
import { unlinkSync, existsSync as existsSync5, writeFileSync as writeFileSync4, readFileSync as
|
|
47
|
+
import { unlinkSync, existsSync as existsSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync8, mkdirSync as mkdirSync4 } from "fs";
|
|
47
48
|
import { join as join4 } from "path";
|
|
48
49
|
|
|
49
50
|
// src/daemon/session-manager.ts
|
|
50
51
|
import { v4 as uuidv4 } from "uuid";
|
|
51
|
-
import { existsSync as existsSync4, readdirSync as readdirSync4 } from "fs";
|
|
52
|
+
import { existsSync as existsSync4, readdirSync as readdirSync4, rmSync as rmSync2 } from "fs";
|
|
52
53
|
|
|
53
54
|
// src/daemon/state.ts
|
|
54
|
-
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, renameSync } from "fs";
|
|
55
|
-
import { dirname, join } from "path";
|
|
56
55
|
import { randomUUID } from "crypto";
|
|
56
|
+
import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
|
|
57
|
+
import { dirname, join } from "path";
|
|
57
58
|
var PLAN_SEED = `---
|
|
58
59
|
description: >
|
|
59
|
-
Living document of what still needs to happen. Write
|
|
60
|
-
here: phases, next steps, file references, open questions. Remove or collapse
|
|
61
|
-
items as they're completed so this file only reflects outstanding work. The
|
|
62
|
-
orchestrator sees this every cycle \u2014 keep it focused and current.
|
|
60
|
+
Living document of what still needs to happen. Write out ne
|
|
63
61
|
---
|
|
64
62
|
`;
|
|
65
63
|
var LOGS_SEED = `---
|
|
@@ -67,22 +65,22 @@ description: >
|
|
|
67
65
|
Session memory. Record important observations, decisions, and findings here.
|
|
68
66
|
This is your persistent memory across cycles: things you tried, what
|
|
69
67
|
worked/failed, design decisions and their rationale, gotchas discovered during
|
|
70
|
-
implementation.
|
|
68
|
+
implementation.
|
|
71
69
|
---
|
|
72
70
|
`;
|
|
73
71
|
var sessionLocks = /* @__PURE__ */ new Map();
|
|
74
72
|
async function withSessionLock(sessionId, fn) {
|
|
75
73
|
const prev = sessionLocks.get(sessionId) ?? Promise.resolve();
|
|
76
|
-
let
|
|
74
|
+
let resolve4;
|
|
77
75
|
const next = new Promise((r) => {
|
|
78
|
-
|
|
76
|
+
resolve4 = r;
|
|
79
77
|
});
|
|
80
78
|
sessionLocks.set(sessionId, next);
|
|
81
79
|
await prev;
|
|
82
80
|
try {
|
|
83
81
|
return fn();
|
|
84
82
|
} finally {
|
|
85
|
-
|
|
83
|
+
resolve4();
|
|
86
84
|
}
|
|
87
85
|
}
|
|
88
86
|
function atomicWrite(filePath, data) {
|
|
@@ -331,6 +329,33 @@ function shellQuote(s) {
|
|
|
331
329
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
332
330
|
}
|
|
333
331
|
|
|
332
|
+
// src/daemon/pane-registry.ts
|
|
333
|
+
var paneMap = /* @__PURE__ */ new Map();
|
|
334
|
+
function registerPane(paneId, sessionId, role, agentId) {
|
|
335
|
+
paneMap.set(paneId, { sessionId, role, agentId });
|
|
336
|
+
}
|
|
337
|
+
function unregisterPane(paneId) {
|
|
338
|
+
paneMap.delete(paneId);
|
|
339
|
+
}
|
|
340
|
+
function unregisterAgentPane(sessionId, agentId) {
|
|
341
|
+
for (const [paneId, entry] of paneMap) {
|
|
342
|
+
if (entry.sessionId === sessionId && entry.agentId === agentId) {
|
|
343
|
+
paneMap.delete(paneId);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function unregisterSessionPanes(sessionId) {
|
|
349
|
+
for (const [paneId, entry] of paneMap) {
|
|
350
|
+
if (entry.sessionId === sessionId) {
|
|
351
|
+
paneMap.delete(paneId);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function lookupPane(paneId) {
|
|
356
|
+
return paneMap.get(paneId);
|
|
357
|
+
}
|
|
358
|
+
|
|
334
359
|
// src/daemon/orchestrator.ts
|
|
335
360
|
var sessionWindowMap = /* @__PURE__ */ new Map();
|
|
336
361
|
var sessionOrchestratorPane = /* @__PURE__ */ new Map();
|
|
@@ -388,10 +413,13 @@ function formatStateForOrchestrator(session) {
|
|
|
388
413
|
if (worktreeAgents.length > 0) {
|
|
389
414
|
const wtLines = worktreeAgents.map((a) => {
|
|
390
415
|
if (a.mergeStatus === "conflict") {
|
|
391
|
-
return `- ${a.id}:
|
|
416
|
+
return `- ${a.id}: CONFLICT \u2014 ${a.mergeDetails ?? "unknown"}
|
|
392
417
|
Branch: ${a.branchName}
|
|
393
418
|
Worktree: ${a.worktreePath}`;
|
|
394
419
|
}
|
|
420
|
+
if (a.mergeStatus === "no-changes") {
|
|
421
|
+
return `- ${a.id}: NO CHANGES \u2014 agent did not commit any work to branch ${a.branchName}`;
|
|
422
|
+
}
|
|
395
423
|
const status = a.mergeStatus ?? "pending";
|
|
396
424
|
return `- ${a.id}: ${status} (branch ${a.branchName})`;
|
|
397
425
|
}).join("\n");
|
|
@@ -462,11 +490,13 @@ Review the current session and delegate the next cycle of work.`;
|
|
|
462
490
|
const claudeCmd = `claude --dangerously-skip-permissions --settings "${settingsPath}" --plugin-dir "${pluginPath}" --append-system-prompt "$(cat '${promptFilePath}')" "$(cat '${userPromptFilePath}')"`;
|
|
463
491
|
const paneId = createPane(windowId, cwd);
|
|
464
492
|
sessionOrchestratorPane.set(sessionId, paneId);
|
|
493
|
+
registerPane(paneId, sessionId, "orchestrator");
|
|
465
494
|
setPaneTitle(paneId, `orchestrator (${sessionId.slice(0, 8)})`);
|
|
466
495
|
setPaneStyle(paneId, ORCHESTRATOR_COLOR);
|
|
467
496
|
const bannerPath = resolve(import.meta.dirname, "../templates/banner.txt");
|
|
468
497
|
const bannerCmd = existsSync(bannerPath) ? `cat '${bannerPath}' &&` : "";
|
|
469
|
-
|
|
498
|
+
const notifyCmd = `sisyphus notify pane-exited --pane-id ${paneId}`;
|
|
499
|
+
sendKeys(paneId, `${bannerCmd} ${envExports} && ${claudeCmd}; ${notifyCmd}`);
|
|
470
500
|
await addOrchestratorCycle(cwd, sessionId, {
|
|
471
501
|
cycle: cycleNum,
|
|
472
502
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -485,6 +515,7 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
|
|
|
485
515
|
const paneId = resolveOrchestratorPane(sessionId, cwd);
|
|
486
516
|
if (paneId) {
|
|
487
517
|
killPane(paneId);
|
|
518
|
+
unregisterPane(paneId);
|
|
488
519
|
sessionOrchestratorPane.delete(sessionId);
|
|
489
520
|
}
|
|
490
521
|
const windowId = sessionWindowMap.get(sessionId);
|
|
@@ -493,7 +524,7 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
|
|
|
493
524
|
const session = getSession(cwd, sessionId);
|
|
494
525
|
const runningAgents = session.agents.filter((a) => a.status === "running");
|
|
495
526
|
if (runningAgents.length === 0) {
|
|
496
|
-
console.
|
|
527
|
+
console.log(`[sisyphus] Orchestrator yielded with no running agents for session ${sessionId}`);
|
|
497
528
|
}
|
|
498
529
|
}
|
|
499
530
|
async function handleOrchestratorComplete(sessionId, cwd, report) {
|
|
@@ -504,6 +535,7 @@ async function handleOrchestratorComplete(sessionId, cwd, report) {
|
|
|
504
535
|
function cleanupSessionMaps(sessionId) {
|
|
505
536
|
sessionOrchestratorPane.delete(sessionId);
|
|
506
537
|
sessionWindowMap.delete(sessionId);
|
|
538
|
+
unregisterSessionPanes(sessionId);
|
|
507
539
|
}
|
|
508
540
|
|
|
509
541
|
// src/daemon/agent.ts
|
|
@@ -539,23 +571,17 @@ function loadWorktreeConfig(cwd) {
|
|
|
539
571
|
return null;
|
|
540
572
|
}
|
|
541
573
|
}
|
|
542
|
-
function
|
|
574
|
+
function createWorktreeShell(cwd, sessionId, agentId) {
|
|
543
575
|
const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
|
|
544
|
-
const worktreePath = join3(worktreeBaseDir(cwd), agentId);
|
|
576
|
+
const worktreePath = join3(worktreeBaseDir(cwd), sessionId.slice(0, 8), agentId);
|
|
545
577
|
mkdirSync2(dirname2(worktreePath), { recursive: true });
|
|
578
|
+
execSafe2(`git -C ${shellQuote2(cwd)} worktree prune`);
|
|
579
|
+
if (existsSync2(worktreePath)) {
|
|
580
|
+
execSafe2(`git -C ${shellQuote2(cwd)} worktree remove --force ${shellQuote2(worktreePath)}`);
|
|
581
|
+
}
|
|
582
|
+
execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
|
|
546
583
|
exec2(`git -C ${shellQuote2(cwd)} branch ${shellQuote2(branchName)} HEAD`);
|
|
547
584
|
exec2(`git -C ${shellQuote2(cwd)} worktree add ${shellQuote2(worktreePath)} ${shellQuote2(branchName)}`);
|
|
548
|
-
const symlinks = [".sisyphus", ".claude"];
|
|
549
|
-
for (const entry of symlinks) {
|
|
550
|
-
const src = join3(cwd, entry);
|
|
551
|
-
if (existsSync2(src)) {
|
|
552
|
-
execSafe2(`ln -s ${shellQuote2(src)} ${shellQuote2(join3(worktreePath, entry))}`);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
const config = loadWorktreeConfig(cwd);
|
|
556
|
-
if (config) {
|
|
557
|
-
bootstrapWorktree(cwd, worktreePath, config);
|
|
558
|
-
}
|
|
559
585
|
return { worktreePath, branchName };
|
|
560
586
|
}
|
|
561
587
|
function bootstrapWorktree(cwd, worktreePath, config) {
|
|
@@ -615,6 +641,8 @@ function mergeWorktrees(cwd, agents) {
|
|
|
615
641
|
(a) => a.worktreePath && a.mergeStatus === "pending"
|
|
616
642
|
);
|
|
617
643
|
const results = [];
|
|
644
|
+
execSafe2(`git -C ${shellQuote2(cwd)} add .sisyphus`);
|
|
645
|
+
execSafe2(`git -C ${shellQuote2(cwd)} commit -m 'sisyphus: snapshot session state before merge'`);
|
|
618
646
|
for (const agent of pending) {
|
|
619
647
|
const branch = resolveWorktreeBranch(cwd, agent.worktreePath);
|
|
620
648
|
if (!branch) {
|
|
@@ -637,8 +665,10 @@ function mergeWorktrees(cwd, agents) {
|
|
|
637
665
|
results.push({ agentId: agent.id, name: agent.name, status: "merged" });
|
|
638
666
|
} catch (err) {
|
|
639
667
|
execSafe2(`git -C ${shellQuote2(cwd)} merge --abort`);
|
|
640
|
-
const
|
|
641
|
-
const
|
|
668
|
+
const errObj = err;
|
|
669
|
+
const stdout = errObj.stdout ? (typeof errObj.stdout === "string" ? errObj.stdout : errObj.stdout.toString("utf-8")).trim() : "";
|
|
670
|
+
const stderr = errObj.stderr ? (typeof errObj.stderr === "string" ? errObj.stderr : errObj.stderr.toString("utf-8")).trim() : "";
|
|
671
|
+
const conflictDetails = stdout || stderr || (err instanceof Error ? err.message : String(err));
|
|
642
672
|
results.push({ agentId: agent.id, name: agent.name, status: "conflict", conflictDetails });
|
|
643
673
|
}
|
|
644
674
|
}
|
|
@@ -687,7 +717,7 @@ Task: {{INSTRUCTION}}`;
|
|
|
687
717
|
if (worktreeContext) {
|
|
688
718
|
worktreeBlock = [
|
|
689
719
|
"## Worktree Context",
|
|
690
|
-
`You are working in
|
|
720
|
+
`You are working in an isolated git worktree on branch \`${worktreeContext.branchName}\`.`,
|
|
691
721
|
`If you start any services that require ports, add ${worktreeContext.offset} to the default port.`
|
|
692
722
|
].join("\n");
|
|
693
723
|
}
|
|
@@ -705,7 +735,7 @@ async function spawnAgent(opts) {
|
|
|
705
735
|
let branchName;
|
|
706
736
|
let worktreeContext;
|
|
707
737
|
if (opts.worktree) {
|
|
708
|
-
const wt =
|
|
738
|
+
const wt = createWorktreeShell(cwd, sessionId, agentId);
|
|
709
739
|
worktreePath = wt.worktreePath;
|
|
710
740
|
branchName = wt.branchName;
|
|
711
741
|
paneCwd = worktreePath;
|
|
@@ -714,6 +744,7 @@ async function spawnAgent(opts) {
|
|
|
714
744
|
worktreeContext = { offset: portOffset, total: portOffset, branchName };
|
|
715
745
|
}
|
|
716
746
|
const paneId = createPane(windowId, paneCwd);
|
|
747
|
+
registerPane(paneId, sessionId, "agent", agentId);
|
|
717
748
|
setPaneTitle(paneId, `${name} (${agentId})`);
|
|
718
749
|
setPaneStyle(paneId, color);
|
|
719
750
|
const suffix = renderAgentSuffix(sessionId, instruction, worktreeContext);
|
|
@@ -728,7 +759,8 @@ async function spawnAgent(opts) {
|
|
|
728
759
|
].join(" && ");
|
|
729
760
|
const agentFlag = agentType ? ` --agent ${shellQuote3(agentType)}` : "";
|
|
730
761
|
const claudeCmd = `claude --dangerously-skip-permissions --plugin-dir "${pluginPath}"${agentFlag} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote3(instruction)}`;
|
|
731
|
-
|
|
762
|
+
const notifyCmd = `sisyphus notify pane-exited --pane-id ${paneId}`;
|
|
763
|
+
const fullCmd = `${bannerCmd} ${envExports} && ${claudeCmd}; ${notifyCmd}`;
|
|
732
764
|
const agent = {
|
|
733
765
|
id: agentId,
|
|
734
766
|
name,
|
|
@@ -743,6 +775,24 @@ async function spawnAgent(opts) {
|
|
|
743
775
|
...worktreePath ? { worktreePath, branchName, mergeStatus: "pending" } : {}
|
|
744
776
|
};
|
|
745
777
|
await addAgent(cwd, sessionId, agent);
|
|
778
|
+
if (opts.worktree && worktreePath) {
|
|
779
|
+
const config = loadWorktreeConfig(cwd);
|
|
780
|
+
if (config) {
|
|
781
|
+
const wtPath = worktreePath;
|
|
782
|
+
setImmediate(() => {
|
|
783
|
+
try {
|
|
784
|
+
bootstrapWorktree(cwd, wtPath, config);
|
|
785
|
+
} catch (err) {
|
|
786
|
+
console.error(`[sisyphus] worktree bootstrap failed for ${agentId}: ${err instanceof Error ? err.message : err}`);
|
|
787
|
+
}
|
|
788
|
+
sendKeys(paneId, fullCmd);
|
|
789
|
+
});
|
|
790
|
+
} else {
|
|
791
|
+
sendKeys(paneId, fullCmd);
|
|
792
|
+
}
|
|
793
|
+
} else {
|
|
794
|
+
sendKeys(paneId, fullCmd);
|
|
795
|
+
}
|
|
746
796
|
return agent;
|
|
747
797
|
}
|
|
748
798
|
function nextReportNumber(cwd, sessionId, agentId) {
|
|
@@ -788,6 +838,7 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
|
788
838
|
const agentArr = session.agents;
|
|
789
839
|
const agent = agentArr.slice().reverse().find((a) => a.id === agentId);
|
|
790
840
|
if (agent) {
|
|
841
|
+
unregisterPane(agent.paneId);
|
|
791
842
|
killPane(agent.paneId);
|
|
792
843
|
}
|
|
793
844
|
const windowId = getWindowId(sessionId);
|
|
@@ -795,6 +846,7 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
|
795
846
|
return allAgentsDone(session);
|
|
796
847
|
}
|
|
797
848
|
async function handleAgentKilled(cwd, sessionId, agentId, reason) {
|
|
849
|
+
unregisterAgentPane(sessionId, agentId);
|
|
798
850
|
await updateAgent(cwd, sessionId, agentId, {
|
|
799
851
|
status: "killed",
|
|
800
852
|
killedReason: reason,
|
|
@@ -817,7 +869,7 @@ var onAllAgentsDone = null;
|
|
|
817
869
|
function setRespawnCallback(cb) {
|
|
818
870
|
onAllAgentsDone = cb;
|
|
819
871
|
}
|
|
820
|
-
function startMonitor(pollIntervalMs =
|
|
872
|
+
function startMonitor(pollIntervalMs = 5e3) {
|
|
821
873
|
if (monitorInterval) return;
|
|
822
874
|
monitorInterval = setInterval(() => {
|
|
823
875
|
pollAllSessions().catch((err) => {
|
|
@@ -906,16 +958,129 @@ async function pollSession(sessionId, cwd, windowId) {
|
|
|
906
958
|
}
|
|
907
959
|
}
|
|
908
960
|
|
|
961
|
+
// src/daemon/updater.ts
|
|
962
|
+
import { execSync as execSync3 } from "child_process";
|
|
963
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
964
|
+
import { resolve as resolve3 } from "path";
|
|
965
|
+
import { get } from "https";
|
|
966
|
+
function readPackageVersion() {
|
|
967
|
+
for (const rel of ["../package.json", "../../package.json"]) {
|
|
968
|
+
try {
|
|
969
|
+
const raw = readFileSync7(resolve3(import.meta.dirname, rel), "utf-8");
|
|
970
|
+
const pkg = JSON.parse(raw);
|
|
971
|
+
if (pkg.name === "sisyphi" && pkg.version) return pkg.version;
|
|
972
|
+
} catch {
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return "0.0.0";
|
|
976
|
+
}
|
|
977
|
+
var currentVersion = readPackageVersion();
|
|
978
|
+
function checkForUpdate() {
|
|
979
|
+
return new Promise((resolve4) => {
|
|
980
|
+
const timeout = setTimeout(() => {
|
|
981
|
+
resolve4(null);
|
|
982
|
+
}, 5e3);
|
|
983
|
+
const req = get("https://registry.npmjs.org/sisyphi/latest", (res) => {
|
|
984
|
+
let data = "";
|
|
985
|
+
res.on("data", (chunk) => {
|
|
986
|
+
data += chunk.toString();
|
|
987
|
+
});
|
|
988
|
+
res.on("end", () => {
|
|
989
|
+
clearTimeout(timeout);
|
|
990
|
+
try {
|
|
991
|
+
const { version: latest } = JSON.parse(data);
|
|
992
|
+
if (latest && latest !== currentVersion) {
|
|
993
|
+
resolve4({ current: currentVersion, latest });
|
|
994
|
+
} else {
|
|
995
|
+
resolve4(null);
|
|
996
|
+
}
|
|
997
|
+
} catch {
|
|
998
|
+
resolve4(null);
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
});
|
|
1002
|
+
req.on("error", () => {
|
|
1003
|
+
clearTimeout(timeout);
|
|
1004
|
+
resolve4(null);
|
|
1005
|
+
});
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
function applyUpdate() {
|
|
1009
|
+
try {
|
|
1010
|
+
const nodeDir = resolve3(process.execPath, "..");
|
|
1011
|
+
const env = { ...process.env, PATH: `${nodeDir}:${process.env.PATH ?? ""}` };
|
|
1012
|
+
execSync3("npm install -g sisyphi", { timeout: 15e3, stdio: "pipe", env });
|
|
1013
|
+
return true;
|
|
1014
|
+
} catch (err) {
|
|
1015
|
+
console.error("[sisyphus] Auto-update failed:", err);
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async function checkAndApply() {
|
|
1020
|
+
try {
|
|
1021
|
+
const update = await checkForUpdate();
|
|
1022
|
+
if (!update) return;
|
|
1023
|
+
console.log(`[sisyphus] Update available: ${update.current} \u2192 ${update.latest}`);
|
|
1024
|
+
const success = applyUpdate();
|
|
1025
|
+
if (success) {
|
|
1026
|
+
console.log(`[sisyphus] Updated to ${update.latest}, restarting daemon...`);
|
|
1027
|
+
process.exit(0);
|
|
1028
|
+
}
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
console.error("[sisyphus] Auto-update check failed:", err);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
909
1034
|
// src/daemon/session-manager.ts
|
|
910
1035
|
async function startSession(task, cwd, tmuxSession, windowId) {
|
|
1036
|
+
const config = loadConfig(cwd);
|
|
1037
|
+
if (config.autoUpdate !== false) {
|
|
1038
|
+
await checkAndApply();
|
|
1039
|
+
}
|
|
911
1040
|
const sessionId = uuidv4();
|
|
912
1041
|
const session = createSession(sessionId, task, cwd);
|
|
913
1042
|
await updateSessionTmux(cwd, sessionId, tmuxSession, windowId);
|
|
914
1043
|
trackSession(sessionId, cwd, tmuxSession);
|
|
915
1044
|
await spawnOrchestrator(sessionId, cwd, windowId);
|
|
916
1045
|
updateTrackedWindow(sessionId, windowId);
|
|
1046
|
+
pruneOldSessions(cwd);
|
|
917
1047
|
return session;
|
|
918
1048
|
}
|
|
1049
|
+
var PRUNE_KEEP_COUNT = 10;
|
|
1050
|
+
var PRUNE_KEEP_DAYS = 7;
|
|
1051
|
+
function pruneOldSessions(cwd) {
|
|
1052
|
+
try {
|
|
1053
|
+
const dir = sessionsDir(cwd);
|
|
1054
|
+
if (!existsSync4(dir)) return;
|
|
1055
|
+
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
1056
|
+
const candidates = [];
|
|
1057
|
+
for (const entry of entries) {
|
|
1058
|
+
if (!entry.isDirectory()) continue;
|
|
1059
|
+
try {
|
|
1060
|
+
const session = getSession(cwd, entry.name);
|
|
1061
|
+
if (session.status === "active" || session.status === "paused") continue;
|
|
1062
|
+
candidates.push({ id: session.id, createdAt: new Date(session.createdAt).getTime() });
|
|
1063
|
+
} catch {
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (candidates.length <= PRUNE_KEEP_COUNT) return;
|
|
1067
|
+
candidates.sort((a, b) => b.createdAt - a.createdAt);
|
|
1068
|
+
const cutoff = Date.now() - PRUNE_KEEP_DAYS * 24 * 60 * 60 * 1e3;
|
|
1069
|
+
const keep = /* @__PURE__ */ new Set();
|
|
1070
|
+
for (let i = 0; i < Math.min(PRUNE_KEEP_COUNT, candidates.length); i++) {
|
|
1071
|
+
keep.add(candidates[i].id);
|
|
1072
|
+
}
|
|
1073
|
+
for (const c of candidates) {
|
|
1074
|
+
if (c.createdAt >= cutoff) keep.add(c.id);
|
|
1075
|
+
}
|
|
1076
|
+
for (const c of candidates) {
|
|
1077
|
+
if (keep.has(c.id)) continue;
|
|
1078
|
+
rmSync2(sessionDir(cwd, c.id), { recursive: true, force: true });
|
|
1079
|
+
}
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
console.error("[sisyphus] Session pruning failed:", err);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
919
1084
|
async function resumeSession(sessionId, cwd, tmuxSession, windowId, message) {
|
|
920
1085
|
const session = getSession(cwd, sessionId);
|
|
921
1086
|
if (session.status !== "active") {
|
|
@@ -973,23 +1138,27 @@ function listSessions(cwd) {
|
|
|
973
1138
|
}
|
|
974
1139
|
return sessions;
|
|
975
1140
|
}
|
|
1141
|
+
var pendingRespawns = /* @__PURE__ */ new Set();
|
|
976
1142
|
function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
1143
|
+
if (pendingRespawns.has(sessionId)) return;
|
|
977
1144
|
const session = getSession(cwd, sessionId);
|
|
978
1145
|
if (session.status !== "active") return;
|
|
1146
|
+
pendingRespawns.add(sessionId);
|
|
979
1147
|
const worktreeAgents = session.agents.filter((a) => a.worktreePath && a.mergeStatus === "pending");
|
|
980
1148
|
if (worktreeAgents.length > 0) {
|
|
981
1149
|
const results = mergeWorktrees(cwd, worktreeAgents);
|
|
982
1150
|
for (const result of results) {
|
|
983
|
-
const mergeStatus = result.status
|
|
1151
|
+
const mergeStatus = result.status;
|
|
984
1152
|
updateAgent(cwd, sessionId, result.agentId, {
|
|
985
1153
|
mergeStatus,
|
|
986
1154
|
mergeDetails: result.conflictDetails
|
|
987
1155
|
}).catch((err) => console.error(`[sisyphus] Failed to update merge status for ${result.agentId}:`, err));
|
|
988
1156
|
}
|
|
989
1157
|
}
|
|
990
|
-
|
|
1158
|
+
setImmediate(() => {
|
|
1159
|
+
pendingRespawns.delete(sessionId);
|
|
991
1160
|
spawnOrchestrator(sessionId, cwd, windowId).then(() => updateTrackedWindow(sessionId, windowId)).catch((err) => console.error(`[sisyphus] Failed to respawn orchestrator for session ${sessionId}:`, err));
|
|
992
|
-
}
|
|
1161
|
+
});
|
|
993
1162
|
}
|
|
994
1163
|
async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktree) {
|
|
995
1164
|
const windowId = getWindowId(sessionId);
|
|
@@ -1016,7 +1185,19 @@ async function handleReport(cwd, sessionId, agentId, content) {
|
|
|
1016
1185
|
await handleAgentReport(cwd, sessionId, agentId, content);
|
|
1017
1186
|
}
|
|
1018
1187
|
async function handleYield(sessionId, cwd, nextPrompt) {
|
|
1188
|
+
const pre = getSession(cwd, sessionId);
|
|
1189
|
+
if (pre.status === "paused") {
|
|
1190
|
+
await updateSessionStatus(cwd, sessionId, "active");
|
|
1191
|
+
}
|
|
1019
1192
|
await handleOrchestratorYield(sessionId, cwd, nextPrompt);
|
|
1193
|
+
const session = getSession(cwd, sessionId);
|
|
1194
|
+
const hasRunningAgents = session.agents.some((a) => a.status === "running");
|
|
1195
|
+
if (!hasRunningAgents) {
|
|
1196
|
+
const windowId = getWindowId(sessionId);
|
|
1197
|
+
if (windowId) {
|
|
1198
|
+
onAllAgentsDone2(sessionId, cwd, windowId);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1020
1201
|
}
|
|
1021
1202
|
async function handleComplete(sessionId, cwd, report) {
|
|
1022
1203
|
await handleOrchestratorComplete(sessionId, cwd, report);
|
|
@@ -1049,12 +1230,40 @@ async function handleKill(sessionId, cwd) {
|
|
|
1049
1230
|
}
|
|
1050
1231
|
await updateSessionStatus(cwd, sessionId, "completed");
|
|
1051
1232
|
untrackSession(sessionId);
|
|
1233
|
+
unregisterSessionPanes(sessionId);
|
|
1052
1234
|
if (windowId) {
|
|
1053
1235
|
killWindow(windowId);
|
|
1054
1236
|
}
|
|
1055
1237
|
clearAgentCounter(sessionId);
|
|
1056
1238
|
return killedAgents;
|
|
1057
1239
|
}
|
|
1240
|
+
async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
1241
|
+
const session = getSession(cwd, sessionId);
|
|
1242
|
+
if (session.status !== "active") return;
|
|
1243
|
+
if (role === "agent" && agentId) {
|
|
1244
|
+
const agent = session.agents.find((a) => a.id === agentId);
|
|
1245
|
+
if (!agent || agent.status !== "running") return;
|
|
1246
|
+
const allDone = await handleAgentKilled(cwd, sessionId, agentId, "pane exited");
|
|
1247
|
+
if (allDone) {
|
|
1248
|
+
const windowId = getWindowId(sessionId);
|
|
1249
|
+
if (windowId) {
|
|
1250
|
+
onAllAgentsDone2(sessionId, cwd, windowId);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
} else if (role === "orchestrator") {
|
|
1254
|
+
const hasRunningAgents = session.agents.some((a) => a.status === "running");
|
|
1255
|
+
if (!hasRunningAgents && session.agents.length > 0) {
|
|
1256
|
+
const windowId = getWindowId(sessionId);
|
|
1257
|
+
if (windowId) {
|
|
1258
|
+
console.log(`[sisyphus] Orchestrator pane exited for session ${sessionId}, all agents done \u2014 triggering respawn`);
|
|
1259
|
+
onAllAgentsDone2(sessionId, cwd, windowId);
|
|
1260
|
+
}
|
|
1261
|
+
} else if (!hasRunningAgents) {
|
|
1262
|
+
await updateSessionStatus(cwd, sessionId, "paused");
|
|
1263
|
+
console.log(`[sisyphus] Session ${sessionId} paused: orchestrator pane exited with no agents`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1058
1267
|
|
|
1059
1268
|
// src/daemon/server.ts
|
|
1060
1269
|
var server = null;
|
|
@@ -1077,7 +1286,7 @@ function loadSessionRegistry() {
|
|
|
1077
1286
|
const p = registryPath();
|
|
1078
1287
|
if (!existsSync5(p)) return {};
|
|
1079
1288
|
try {
|
|
1080
|
-
return JSON.parse(
|
|
1289
|
+
return JSON.parse(readFileSync8(p, "utf-8"));
|
|
1081
1290
|
} catch {
|
|
1082
1291
|
return {};
|
|
1083
1292
|
}
|
|
@@ -1199,6 +1408,18 @@ async function handleRequest(req) {
|
|
|
1199
1408
|
persistSessionRegistry();
|
|
1200
1409
|
return { ok: true, data: { killedAgents, sessionId: req.sessionId } };
|
|
1201
1410
|
}
|
|
1411
|
+
case "pane-exited": {
|
|
1412
|
+
const entry = lookupPane(req.paneId);
|
|
1413
|
+
if (!entry) return { ok: true };
|
|
1414
|
+
const cwd = sessionCwdMap.get(entry.sessionId);
|
|
1415
|
+
if (!cwd) {
|
|
1416
|
+
unregisterPane(req.paneId);
|
|
1417
|
+
return { ok: true };
|
|
1418
|
+
}
|
|
1419
|
+
unregisterPane(req.paneId);
|
|
1420
|
+
await handlePaneExited(req.paneId, cwd, entry.sessionId, entry.role, entry.agentId);
|
|
1421
|
+
return { ok: true };
|
|
1422
|
+
}
|
|
1202
1423
|
default:
|
|
1203
1424
|
return { ok: false, error: `Unknown request type: ${req.type}` };
|
|
1204
1425
|
}
|
|
@@ -1208,7 +1429,7 @@ async function handleRequest(req) {
|
|
|
1208
1429
|
}
|
|
1209
1430
|
}
|
|
1210
1431
|
function startServer() {
|
|
1211
|
-
return new Promise((
|
|
1432
|
+
return new Promise((resolve4, reject) => {
|
|
1212
1433
|
const sock = socketPath();
|
|
1213
1434
|
if (existsSync5(sock)) {
|
|
1214
1435
|
unlinkSync(sock);
|
|
@@ -1240,14 +1461,14 @@ function startServer() {
|
|
|
1240
1461
|
server.on("error", reject);
|
|
1241
1462
|
server.listen(sock, () => {
|
|
1242
1463
|
console.log(`[sisyphus] Daemon listening on ${sock}`);
|
|
1243
|
-
|
|
1464
|
+
resolve4(server);
|
|
1244
1465
|
});
|
|
1245
1466
|
});
|
|
1246
1467
|
}
|
|
1247
1468
|
function stopServer() {
|
|
1248
|
-
return new Promise((
|
|
1469
|
+
return new Promise((resolve4) => {
|
|
1249
1470
|
if (!server) {
|
|
1250
|
-
|
|
1471
|
+
resolve4();
|
|
1251
1472
|
return;
|
|
1252
1473
|
}
|
|
1253
1474
|
server.close(() => {
|
|
@@ -1256,7 +1477,7 @@ function stopServer() {
|
|
|
1256
1477
|
unlinkSync(sock);
|
|
1257
1478
|
}
|
|
1258
1479
|
server = null;
|
|
1259
|
-
|
|
1480
|
+
resolve4();
|
|
1260
1481
|
});
|
|
1261
1482
|
});
|
|
1262
1483
|
}
|
|
@@ -1276,7 +1497,7 @@ function isProcessAlive(pid) {
|
|
|
1276
1497
|
function readPid() {
|
|
1277
1498
|
const pidFile = daemonPidPath();
|
|
1278
1499
|
try {
|
|
1279
|
-
const pid = parseInt(
|
|
1500
|
+
const pid = parseInt(readFileSync9(pidFile, "utf-8").trim(), 10);
|
|
1280
1501
|
return pid && isProcessAlive(pid) ? pid : null;
|
|
1281
1502
|
} catch {
|
|
1282
1503
|
return null;
|
|
@@ -1343,7 +1564,7 @@ async function recoverSessions() {
|
|
|
1343
1564
|
continue;
|
|
1344
1565
|
}
|
|
1345
1566
|
try {
|
|
1346
|
-
const session = JSON.parse(
|
|
1567
|
+
const session = JSON.parse(readFileSync9(stateFile, "utf-8"));
|
|
1347
1568
|
if (session.status === "active" || session.status === "paused") {
|
|
1348
1569
|
registerSessionCwd(sessionId, cwd);
|
|
1349
1570
|
resetAgentCounterFromState(sessionId, session.agents ?? []);
|
|
@@ -1357,6 +1578,18 @@ async function recoverSessions() {
|
|
|
1357
1578
|
const lastIncompleteCycle = [...session.orchestratorCycles].reverse().find((c) => !c.completedAt && c.paneId);
|
|
1358
1579
|
if (lastIncompleteCycle?.paneId) {
|
|
1359
1580
|
setOrchestratorPaneId(sessionId, lastIncompleteCycle.paneId);
|
|
1581
|
+
const livePaneIds = new Set(livePanes.map((p) => p.paneId));
|
|
1582
|
+
if (livePaneIds.has(lastIncompleteCycle.paneId)) {
|
|
1583
|
+
registerPane(lastIncompleteCycle.paneId, sessionId, "orchestrator");
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
for (const agent of session.agents) {
|
|
1587
|
+
if (agent.status === "running" && agent.paneId) {
|
|
1588
|
+
const livePaneIds = new Set(livePanes.map((p) => p.paneId));
|
|
1589
|
+
if (livePaneIds.has(agent.paneId)) {
|
|
1590
|
+
registerPane(agent.paneId, sessionId, "agent", agent.id);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1360
1593
|
}
|
|
1361
1594
|
console.log(`[sisyphus] Reconnected session ${sessionId} to tmux window ${session.tmuxWindowId}`);
|
|
1362
1595
|
if (session.status === "active" && session.agents.length > 0) {
|
|
@@ -1406,31 +1639,44 @@ async function startDaemon() {
|
|
|
1406
1639
|
process.on("SIGINT", shutdown);
|
|
1407
1640
|
}
|
|
1408
1641
|
var command = process.argv[2];
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1642
|
+
(async () => {
|
|
1643
|
+
switch (command) {
|
|
1644
|
+
case "stop":
|
|
1645
|
+
stopDaemon();
|
|
1646
|
+
break;
|
|
1647
|
+
case "restart": {
|
|
1648
|
+
stopDaemon();
|
|
1649
|
+
await sleep(500);
|
|
1650
|
+
const respawnedPid = readPid();
|
|
1651
|
+
if (respawnedPid) {
|
|
1652
|
+
console.log(`[sisyphus] Daemon restarted (pid ${respawnedPid}) by process manager`);
|
|
1653
|
+
break;
|
|
1654
|
+
}
|
|
1655
|
+
await startDaemon();
|
|
1656
|
+
break;
|
|
1417
1657
|
}
|
|
1418
|
-
|
|
1419
|
-
|
|
1658
|
+
case "start":
|
|
1659
|
+
case void 0:
|
|
1660
|
+
await startDaemon();
|
|
1661
|
+
break;
|
|
1662
|
+
case "help":
|
|
1663
|
+
case "--help":
|
|
1664
|
+
case "-h":
|
|
1665
|
+
console.log("Usage: sisyphusd [command]");
|
|
1666
|
+
console.log("");
|
|
1667
|
+
console.log("Commands:");
|
|
1668
|
+
console.log(" start Start the daemon (default if no command given)");
|
|
1669
|
+
console.log(" stop Stop the running daemon");
|
|
1670
|
+
console.log(" restart Stop and restart the daemon");
|
|
1671
|
+
console.log(" help Show this help message");
|
|
1672
|
+
break;
|
|
1673
|
+
default:
|
|
1674
|
+
console.error(`[sisyphus] Unknown command: ${command}`);
|
|
1675
|
+
console.error("Usage: sisyphusd [start|stop|restart|help]");
|
|
1420
1676
|
process.exit(1);
|
|
1421
|
-
});
|
|
1422
|
-
break;
|
|
1423
1677
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
process.exit(1);
|
|
1429
|
-
});
|
|
1430
|
-
break;
|
|
1431
|
-
default:
|
|
1432
|
-
console.error(`[sisyphus] Unknown command: ${command}`);
|
|
1433
|
-
console.error("Usage: sisyphusd [start|stop|restart]");
|
|
1434
|
-
process.exit(1);
|
|
1435
|
-
}
|
|
1678
|
+
})().catch((err) => {
|
|
1679
|
+
console.error("[sisyphus] Fatal error:", err);
|
|
1680
|
+
process.exit(1);
|
|
1681
|
+
});
|
|
1436
1682
|
//# sourceMappingURL=daemon.js.map
|