arcane-agents 1.2.0 → 1.2.3
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 +64 -48
- package/config.example.yaml +1 -0
- package/dist/client/assets/index-CT0NFttM.css +32 -0
- package/dist/client/assets/index-DHyA1AST.js +83 -0
- package/dist/client/index.html +2 -2
- package/dist/server/server/bootstrap/serverContext.js +8 -3
- package/dist/server/server/cli.js +120 -2
- package/dist/server/server/config/schema.js +5 -1
- package/dist/server/server/http/routes/registerApiRoutes.js +10 -0
- package/dist/server/server/orchestrator/orchestratorService.js +29 -0
- package/dist/server/server/orchestrator/orchestratorService.test.js +59 -0
- package/dist/server/server/orchestrator/spawn/resolveSpawnPlan.test.js +1 -0
- package/dist/server/server/setup/prerequisites.js +74 -0
- package/dist/server/server/setup/prerequisites.test.js +50 -0
- package/dist/server/server/status/activityParser.js +1 -1
- package/dist/server/server/status/engine/signalContext.js +20 -7
- package/dist/server/server/status/engine/stateMachine/constants.js +6 -2
- package/dist/server/server/status/engine/stateMachine/decision.js +17 -0
- package/dist/server/server/status/engine/stateMachine/decision.test.js +64 -0
- package/dist/server/server/status/engine/stateMachine/helpers.js +7 -1
- package/dist/server/server/status/engine/stateMachine/helpers.test.js +10 -1
- package/dist/server/server/status/engine/stateMachine/idleBlockers.js +13 -0
- package/dist/server/server/status/engine/stateMachine/workingEvidence.js +38 -0
- package/dist/server/server/status/runtime/activityTextExtractors.js +40 -0
- package/dist/server/server/status/runtime/claudeSignals.js +114 -33
- package/dist/server/server/status/runtime/claudeSignals.test.js +23 -0
- package/dist/server/server/status/runtime/codexSignals.js +132 -0
- package/dist/server/server/status/runtime/codexSignals.test.js +47 -0
- package/dist/server/server/status/runtime/runtimeProcess.js +86 -0
- package/dist/server/server/status/runtime/sessionDetection.js +15 -0
- package/dist/server/server/status/runtimeSignals.js +8 -1
- package/dist/server/server/status/statusEvaluator.js +4 -2
- package/dist/server/server/status/statusMonitor.js +22 -4
- package/dist/server/server/status/statusMonitor.test.js +9 -2
- package/dist/server/server/status/statusPipeline.js +14 -4
- package/dist/server/server/tmux/tmuxAdapter.js +30 -51
- package/dist/server/server/tmux/tmuxClient.js +35 -0
- package/dist/server/server/tmux/tmuxClient.test.js +58 -0
- package/dist/server/server/ws/terminalBridge.js +16 -2
- package/dist/server/shared/mapConstants.js +4 -0
- package/package.json +4 -3
- package/dist/client/assets/index-CWU29xaz.css +0 -32
- package/dist/client/assets/index-DNXJVqF0.js +0 -83
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findAgentRuntimeProcess = findAgentRuntimeProcess;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const node_util_1 = require("node:util");
|
|
6
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
7
|
+
const maxProcessTreeDepth = 5;
|
|
8
|
+
async function findAgentRuntimeProcess(panePid) {
|
|
9
|
+
return findAgentRuntimeProcessAtDepth(panePid, 0);
|
|
10
|
+
}
|
|
11
|
+
async function findAgentRuntimeProcessAtDepth(parentPid, depth) {
|
|
12
|
+
if (depth >= maxProcessTreeDepth) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const { stdout } = await execFileAsync("pgrep", ["-P", String(parentPid)], {
|
|
17
|
+
maxBuffer: 1024 * 16
|
|
18
|
+
});
|
|
19
|
+
const childPids = stdout
|
|
20
|
+
.trim()
|
|
21
|
+
.split("\n")
|
|
22
|
+
.map((line) => Number.parseInt(line.trim(), 10))
|
|
23
|
+
.filter((pid) => Number.isFinite(pid) && pid > 0);
|
|
24
|
+
for (const childPid of childPids) {
|
|
25
|
+
const details = await describeProcess(childPid);
|
|
26
|
+
if (details) {
|
|
27
|
+
const runtime = classifyAgentRuntime(details.command, details.args);
|
|
28
|
+
if (runtime) {
|
|
29
|
+
return {
|
|
30
|
+
pid: childPid,
|
|
31
|
+
runtime,
|
|
32
|
+
command: details.command,
|
|
33
|
+
args: details.args
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const nestedMatch = await findAgentRuntimeProcessAtDepth(childPid, depth + 1);
|
|
38
|
+
if (nestedMatch) {
|
|
39
|
+
return nestedMatch;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
async function describeProcess(pid) {
|
|
49
|
+
try {
|
|
50
|
+
const { stdout } = await execFileAsync("ps", ["-o", "comm=", "-o", "args=", "-p", String(pid)], {
|
|
51
|
+
maxBuffer: 1024 * 16
|
|
52
|
+
});
|
|
53
|
+
const line = stdout.trim();
|
|
54
|
+
if (!line) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const [command = "", ...argsParts] = line.split(/\s+/);
|
|
58
|
+
return {
|
|
59
|
+
command: command.trim(),
|
|
60
|
+
args: argsParts.join(" ").trim()
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function classifyAgentRuntime(command, args) {
|
|
68
|
+
const commandLower = command.trim().toLowerCase();
|
|
69
|
+
const argsLower = args.trim().toLowerCase();
|
|
70
|
+
const commandAndArgs = `${commandLower} ${argsLower}`.trim();
|
|
71
|
+
if (commandLower === "claude" ||
|
|
72
|
+
commandAndArgs.includes("/claude") ||
|
|
73
|
+
/\bclaude(?:-code)?\b/.test(commandAndArgs)) {
|
|
74
|
+
return "claude";
|
|
75
|
+
}
|
|
76
|
+
if (commandLower === "opencode" || commandAndArgs.includes("opencode")) {
|
|
77
|
+
return "opencode";
|
|
78
|
+
}
|
|
79
|
+
if (commandLower === "codex" ||
|
|
80
|
+
commandAndArgs.includes("@openai/codex") ||
|
|
81
|
+
commandAndArgs.includes("/bin/codex") ||
|
|
82
|
+
/\bcodex\b/.test(commandAndArgs)) {
|
|
83
|
+
return "codex";
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
@@ -3,13 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.capturePaneLineCount = capturePaneLineCount;
|
|
4
4
|
exports.isLikelyClaudeSession = isLikelyClaudeSession;
|
|
5
5
|
exports.isLikelyOpenCodeSession = isLikelyOpenCodeSession;
|
|
6
|
+
exports.isLikelyCodexSession = isLikelyCodexSession;
|
|
6
7
|
const defaultCapturePaneLines = 35;
|
|
7
8
|
const claudeCapturePaneLines = 60;
|
|
8
9
|
const openCodeCapturePaneLines = 420;
|
|
10
|
+
const codexCapturePaneLines = 420;
|
|
9
11
|
function capturePaneLineCount(worker, commandLower) {
|
|
10
12
|
if (isLikelyOpenCodeSession(worker, commandLower)) {
|
|
11
13
|
return openCodeCapturePaneLines;
|
|
12
14
|
}
|
|
15
|
+
if (isLikelyCodexSession(worker, commandLower)) {
|
|
16
|
+
return codexCapturePaneLines;
|
|
17
|
+
}
|
|
13
18
|
if (isLikelyClaudeSession(worker, commandLower)) {
|
|
14
19
|
return claudeCapturePaneLines;
|
|
15
20
|
}
|
|
@@ -35,3 +40,13 @@ function isLikelyOpenCodeSession(worker, commandLower) {
|
|
|
35
40
|
}
|
|
36
41
|
return commandLower.includes("opencode");
|
|
37
42
|
}
|
|
43
|
+
function isLikelyCodexSession(worker, commandLower) {
|
|
44
|
+
if (worker.runtimeId.toLowerCase().includes("codex")) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const runtimeBinary = worker.command[0]?.toLowerCase() ?? "";
|
|
48
|
+
if (runtimeBinary.includes("codex")) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return commandLower.includes("codex");
|
|
52
|
+
}
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.extractRuntimeActivityText = exports.hasWaitingActivityText = exports.hasActiveWorkActivityText = exports.preferOpenCodeSpecificActivityText = exports.hasOpenCodeActiveSignal = exports.hasOpenCodePromptSignal = exports.detectOpenCodeSignals = exports.isGenericClaudeProgressLabel = exports.hasClaudeLiveProgressSignal = exports.extractClaudeActiveTask = exports.isLikelyOpenCodeSession = exports.isLikelyClaudeSession = exports.capturePaneLineCount = void 0;
|
|
3
|
+
exports.extractRuntimeActivityText = exports.hasWaitingActivityText = exports.hasActiveWorkActivityText = exports.hasCodexActiveSignal = exports.hasCodexPromptSignal = exports.detectCodexSignals = exports.preferOpenCodeSpecificActivityText = exports.hasOpenCodeActiveSignal = exports.hasOpenCodePromptSignal = exports.detectOpenCodeSignals = exports.isGenericClaudeProgressLabel = exports.hasClaudePromptSignal = exports.hasClaudeLiveProgressSignal = exports.extractClaudeActiveTask = exports.detectClaudeSignals = exports.isLikelyCodexSession = exports.isLikelyOpenCodeSession = exports.isLikelyClaudeSession = exports.capturePaneLineCount = void 0;
|
|
4
4
|
var sessionDetection_1 = require("./runtime/sessionDetection");
|
|
5
5
|
Object.defineProperty(exports, "capturePaneLineCount", { enumerable: true, get: function () { return sessionDetection_1.capturePaneLineCount; } });
|
|
6
6
|
Object.defineProperty(exports, "isLikelyClaudeSession", { enumerable: true, get: function () { return sessionDetection_1.isLikelyClaudeSession; } });
|
|
7
7
|
Object.defineProperty(exports, "isLikelyOpenCodeSession", { enumerable: true, get: function () { return sessionDetection_1.isLikelyOpenCodeSession; } });
|
|
8
|
+
Object.defineProperty(exports, "isLikelyCodexSession", { enumerable: true, get: function () { return sessionDetection_1.isLikelyCodexSession; } });
|
|
8
9
|
var claudeSignals_1 = require("./runtime/claudeSignals");
|
|
10
|
+
Object.defineProperty(exports, "detectClaudeSignals", { enumerable: true, get: function () { return claudeSignals_1.detectClaudeSignals; } });
|
|
9
11
|
Object.defineProperty(exports, "extractClaudeActiveTask", { enumerable: true, get: function () { return claudeSignals_1.extractClaudeActiveTask; } });
|
|
10
12
|
Object.defineProperty(exports, "hasClaudeLiveProgressSignal", { enumerable: true, get: function () { return claudeSignals_1.hasClaudeLiveProgressSignal; } });
|
|
13
|
+
Object.defineProperty(exports, "hasClaudePromptSignal", { enumerable: true, get: function () { return claudeSignals_1.hasClaudePromptSignal; } });
|
|
11
14
|
Object.defineProperty(exports, "isGenericClaudeProgressLabel", { enumerable: true, get: function () { return claudeSignals_1.isGenericClaudeProgressLabel; } });
|
|
12
15
|
var openCodeSignals_1 = require("./runtime/openCodeSignals");
|
|
13
16
|
Object.defineProperty(exports, "detectOpenCodeSignals", { enumerable: true, get: function () { return openCodeSignals_1.detectOpenCodeSignals; } });
|
|
14
17
|
Object.defineProperty(exports, "hasOpenCodePromptSignal", { enumerable: true, get: function () { return openCodeSignals_1.hasOpenCodePromptSignal; } });
|
|
15
18
|
Object.defineProperty(exports, "hasOpenCodeActiveSignal", { enumerable: true, get: function () { return openCodeSignals_1.hasOpenCodeActiveSignal; } });
|
|
16
19
|
Object.defineProperty(exports, "preferOpenCodeSpecificActivityText", { enumerable: true, get: function () { return openCodeSignals_1.preferOpenCodeSpecificActivityText; } });
|
|
20
|
+
var codexSignals_1 = require("./runtime/codexSignals");
|
|
21
|
+
Object.defineProperty(exports, "detectCodexSignals", { enumerable: true, get: function () { return codexSignals_1.detectCodexSignals; } });
|
|
22
|
+
Object.defineProperty(exports, "hasCodexPromptSignal", { enumerable: true, get: function () { return codexSignals_1.hasCodexPromptSignal; } });
|
|
23
|
+
Object.defineProperty(exports, "hasCodexActiveSignal", { enumerable: true, get: function () { return codexSignals_1.hasCodexActiveSignal; } });
|
|
17
24
|
var textSignals_1 = require("./runtime/textSignals");
|
|
18
25
|
Object.defineProperty(exports, "hasActiveWorkActivityText", { enumerable: true, get: function () { return textSignals_1.hasActiveWorkActivityText; } });
|
|
19
26
|
Object.defineProperty(exports, "hasWaitingActivityText", { enumerable: true, get: function () { return textSignals_1.hasWaitingActivityText; } });
|
|
@@ -3,15 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.evaluateWorkerStatus = evaluateWorkerStatus;
|
|
4
4
|
const signalContext_1 = require("./engine/signalContext");
|
|
5
5
|
const stateMachine_1 = require("./engine/stateMachine");
|
|
6
|
-
function evaluateWorkerStatus({ worker, currentCommand, output, observation, transcriptSnapshot, interactiveCommands }) {
|
|
6
|
+
function evaluateWorkerStatus({ worker, currentCommand, output, observation, transcriptSnapshot, runtimeProcess, interactiveCommands, runtimeFreshnessWindowMs }) {
|
|
7
7
|
const context = (0, signalContext_1.buildWorkerStatusSignalContext)({
|
|
8
8
|
worker,
|
|
9
9
|
currentCommand,
|
|
10
10
|
output,
|
|
11
11
|
observation,
|
|
12
12
|
transcriptSnapshot,
|
|
13
|
+
runtimeProcess,
|
|
13
14
|
nowMs: Date.now(),
|
|
14
|
-
interactiveCommands
|
|
15
|
+
interactiveCommands,
|
|
16
|
+
runtimeFreshnessWindowMs
|
|
15
17
|
});
|
|
16
18
|
return (0, stateMachine_1.deriveWorkerStatusDecision)(context);
|
|
17
19
|
}
|
|
@@ -10,10 +10,15 @@ const defaultDecisionFacts = {
|
|
|
10
10
|
workerAgeMs: 0,
|
|
11
11
|
isClaudeSession: false,
|
|
12
12
|
isOpenCodeSession: false,
|
|
13
|
+
isCodexSession: false,
|
|
14
|
+
hasClaudePromptSignal: false,
|
|
13
15
|
hasOpenCodePromptSignal: false,
|
|
14
16
|
hasOpenCodeActiveSignal: false,
|
|
17
|
+
hasCodexPromptSignal: false,
|
|
18
|
+
hasCodexActiveSignal: false,
|
|
15
19
|
hasClaudeProgressSignal: false,
|
|
16
20
|
hasActiveClaudeTask: false,
|
|
21
|
+
hasActiveRuntimeProcess: false,
|
|
17
22
|
hasRuntimeActivityText: false,
|
|
18
23
|
hasParsedStrongSignal: false,
|
|
19
24
|
hasParsedNeedsInput: false,
|
|
@@ -41,6 +46,7 @@ class StatusMonitor {
|
|
|
41
46
|
traceMode = resolveStatusTraceMode();
|
|
42
47
|
workerPollConcurrency = resolveStatusPollConcurrency();
|
|
43
48
|
interactiveCommands;
|
|
49
|
+
runtimeFreshnessOverrides;
|
|
44
50
|
constructor(workers, tmux, pollIntervalMs, onWorkerUpdated, onWorkerRemoved, config) {
|
|
45
51
|
this.workers = workers;
|
|
46
52
|
this.tmux = tmux;
|
|
@@ -48,6 +54,13 @@ class StatusMonitor {
|
|
|
48
54
|
this.onWorkerUpdated = onWorkerUpdated;
|
|
49
55
|
this.onWorkerRemoved = onWorkerRemoved;
|
|
50
56
|
this.interactiveCommands = new Set(config.status.interactiveCommands.map((cmd) => cmd.toLowerCase()));
|
|
57
|
+
const freshnessOverrides = new Map();
|
|
58
|
+
for (const [id, runtime] of Object.entries(config.runtimes)) {
|
|
59
|
+
if (runtime.freshnessWindowMs !== undefined) {
|
|
60
|
+
freshnessOverrides.set(id, runtime.freshnessWindowMs);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
this.runtimeFreshnessOverrides = freshnessOverrides;
|
|
51
64
|
}
|
|
52
65
|
start() {
|
|
53
66
|
if (this.intervalId) {
|
|
@@ -170,7 +183,8 @@ class StatusMonitor {
|
|
|
170
183
|
tmux: this.tmux,
|
|
171
184
|
paneObservation: this.paneObservation,
|
|
172
185
|
claudeTranscript: this.claudeTranscript,
|
|
173
|
-
interactiveCommands: this.interactiveCommands
|
|
186
|
+
interactiveCommands: this.interactiveCommands,
|
|
187
|
+
runtimeFreshnessWindowMs: this.runtimeFreshnessOverrides.get(worker.runtimeId)
|
|
174
188
|
});
|
|
175
189
|
if (!signals) {
|
|
176
190
|
this.removeWorker(worker.id);
|
|
@@ -276,7 +290,8 @@ class StatusMonitor {
|
|
|
276
290
|
if (this.traceMode !== "verbose") {
|
|
277
291
|
return;
|
|
278
292
|
}
|
|
279
|
-
|
|
293
|
+
const timestamp = new Date().toLocaleTimeString("en-AU", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
294
|
+
console.log(`[arcane-agents][status] ${timestamp} poll workers=${timing.workerCount} duration=${Math.round(timing.durationMs)}ms ` +
|
|
280
295
|
`avgWorker=${Math.round(timing.averageWorkerDurationMs)}ms maxWorker=${Math.round(timing.maxWorkerDurationMs)}ms ` +
|
|
281
296
|
`outcomes={updated:${timing.outcomeCounts.updated},unchanged:${timing.outcomeCounts.unchanged},removed:${timing.outcomeCounts.removed},failed:${timing.outcomeCounts.failed}}`);
|
|
282
297
|
}
|
|
@@ -305,8 +320,11 @@ class StatusMonitor {
|
|
|
305
320
|
`outQuiet=${Math.round(evaluation.facts.outputQuietForMs)}ms ` +
|
|
306
321
|
`cmdQuiet=${Math.round(evaluation.facts.commandQuietForMs)}ms ` +
|
|
307
322
|
`claude=${evaluation.facts.isClaudeSession ? 1 : 0} ` +
|
|
308
|
-
`opencode=${evaluation.facts.isOpenCodeSession ? 1 : 0}
|
|
309
|
-
|
|
323
|
+
`opencode=${evaluation.facts.isOpenCodeSession ? 1 : 0} ` +
|
|
324
|
+
`codex=${evaluation.facts.isCodexSession ? 1 : 0} ` +
|
|
325
|
+
`runtimeProc=${evaluation.facts.hasActiveRuntimeProcess ? 1 : 0}`;
|
|
326
|
+
const timestamp = new Date().toLocaleTimeString("en-AU", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
327
|
+
console.log(`[arcane-agents][status] ${timestamp} ${worker.displayName ?? worker.name} ${fromTo} (${Math.round(evaluation.confidence * 100)}%)${activityText} reasons=[${reasonText}] ${traceFacts}`);
|
|
310
328
|
}
|
|
311
329
|
recordStatusTransition(worker, evaluation) {
|
|
312
330
|
if (evaluation.status === worker.status) {
|
|
@@ -8,7 +8,7 @@ vitest_1.vi.mock("./statusPipeline", () => ({
|
|
|
8
8
|
evaluateWorkerStatusSignals: vitest_1.vi.fn(),
|
|
9
9
|
normalizeWorkerStatusEvaluation: vitest_1.vi.fn((evaluation) => evaluation)
|
|
10
10
|
}));
|
|
11
|
-
const testConfig = { status: { interactiveCommands: [] } };
|
|
11
|
+
const testConfig = { status: { interactiveCommands: [] }, runtimes: {} };
|
|
12
12
|
const defaultFacts = {
|
|
13
13
|
command: "claude",
|
|
14
14
|
commandQuietForMs: 0,
|
|
@@ -16,10 +16,15 @@ const defaultFacts = {
|
|
|
16
16
|
workerAgeMs: 0,
|
|
17
17
|
isClaudeSession: true,
|
|
18
18
|
isOpenCodeSession: false,
|
|
19
|
+
isCodexSession: false,
|
|
20
|
+
hasClaudePromptSignal: false,
|
|
19
21
|
hasOpenCodePromptSignal: false,
|
|
20
22
|
hasOpenCodeActiveSignal: false,
|
|
23
|
+
hasCodexPromptSignal: false,
|
|
24
|
+
hasCodexActiveSignal: false,
|
|
21
25
|
hasClaudeProgressSignal: false,
|
|
22
26
|
hasActiveClaudeTask: false,
|
|
27
|
+
hasActiveRuntimeProcess: false,
|
|
23
28
|
hasRuntimeActivityText: false,
|
|
24
29
|
hasParsedStrongSignal: false,
|
|
25
30
|
hasParsedNeedsInput: false,
|
|
@@ -90,7 +95,9 @@ function createSignals() {
|
|
|
90
95
|
lastOutputChangeAtMs: Date.now()
|
|
91
96
|
},
|
|
92
97
|
transcriptSnapshot: undefined,
|
|
93
|
-
|
|
98
|
+
runtimeProcess: undefined,
|
|
99
|
+
interactiveCommands: new Set(),
|
|
100
|
+
runtimeFreshnessWindowMs: undefined
|
|
94
101
|
};
|
|
95
102
|
}
|
|
96
103
|
function createEvaluation(status) {
|
|
@@ -4,15 +4,21 @@ exports.collectWorkerStatusSignals = collectWorkerStatusSignals;
|
|
|
4
4
|
exports.evaluateWorkerStatusSignals = evaluateWorkerStatusSignals;
|
|
5
5
|
exports.normalizeWorkerStatusEvaluation = normalizeWorkerStatusEvaluation;
|
|
6
6
|
const runtimeSignals_1 = require("./runtimeSignals");
|
|
7
|
+
const runtimeProcess_1 = require("./runtime/runtimeProcess");
|
|
7
8
|
const statusEvaluator_1 = require("./statusEvaluator");
|
|
8
9
|
const paneObservation_1 = require("./paneObservation");
|
|
9
|
-
async function collectWorkerStatusSignals({ worker, tmux, paneObservation, claudeTranscript, interactiveCommands }) {
|
|
10
|
+
async function collectWorkerStatusSignals({ worker, tmux, paneObservation, claudeTranscript, interactiveCommands, runtimeFreshnessWindowMs }) {
|
|
10
11
|
const paneState = await tmux.getPaneState(worker.tmuxRef);
|
|
11
12
|
if (paneState.isDead) {
|
|
12
13
|
return undefined;
|
|
13
14
|
}
|
|
15
|
+
const currentCommandLower = paneState.currentCommand.toLowerCase();
|
|
16
|
+
const runtimeProcess = paneState.panePid && (currentCommandLower === "bash" || currentCommandLower === "zsh" || currentCommandLower === "sh")
|
|
17
|
+
? await (0, runtimeProcess_1.findAgentRuntimeProcess)(paneState.panePid)
|
|
18
|
+
: undefined;
|
|
19
|
+
const captureCommand = runtimeProcess?.runtime ?? currentCommandLower;
|
|
14
20
|
const [output, transcriptSnapshot] = await Promise.all([
|
|
15
|
-
tmux.capturePane(worker.tmuxRef, (0, runtimeSignals_1.capturePaneLineCount)(worker,
|
|
21
|
+
tmux.capturePane(worker.tmuxRef, (0, runtimeSignals_1.capturePaneLineCount)(worker, captureCommand)),
|
|
16
22
|
claudeTranscript.poll(worker, paneState.currentCommand, paneState.currentPath, paneState.panePid)
|
|
17
23
|
]);
|
|
18
24
|
const observation = (0, paneObservation_1.observePane)(paneObservation, worker.id, paneState.currentCommand, output);
|
|
@@ -21,7 +27,9 @@ async function collectWorkerStatusSignals({ worker, tmux, paneObservation, claud
|
|
|
21
27
|
output,
|
|
22
28
|
observation,
|
|
23
29
|
transcriptSnapshot,
|
|
24
|
-
|
|
30
|
+
runtimeProcess,
|
|
31
|
+
interactiveCommands,
|
|
32
|
+
runtimeFreshnessWindowMs
|
|
25
33
|
};
|
|
26
34
|
}
|
|
27
35
|
function evaluateWorkerStatusSignals(worker, signals) {
|
|
@@ -31,7 +39,9 @@ function evaluateWorkerStatusSignals(worker, signals) {
|
|
|
31
39
|
output: signals.output,
|
|
32
40
|
observation: signals.observation,
|
|
33
41
|
transcriptSnapshot: signals.transcriptSnapshot,
|
|
34
|
-
|
|
42
|
+
runtimeProcess: signals.runtimeProcess,
|
|
43
|
+
interactiveCommands: signals.interactiveCommands,
|
|
44
|
+
runtimeFreshnessWindowMs: signals.runtimeFreshnessWindowMs
|
|
35
45
|
});
|
|
36
46
|
}
|
|
37
47
|
function normalizeWorkerStatusEvaluation(evaluation) {
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TmuxAdapter = void 0;
|
|
4
|
+
exports.clipboardCandidatesForEnvironment = clipboardCandidatesForEnvironment;
|
|
4
5
|
const node_child_process_1 = require("node:child_process");
|
|
5
6
|
const node_util_1 = require("node:util");
|
|
7
|
+
const tmuxClient_1 = require("./tmuxClient");
|
|
6
8
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
7
9
|
class TmuxAdapter {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
constructor(
|
|
11
|
-
this.
|
|
10
|
+
config;
|
|
11
|
+
managedDefaultsConfigured = false;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = config;
|
|
12
14
|
}
|
|
13
15
|
async spawnWorker(input) {
|
|
14
|
-
const target = `${this.sessionName}:${input.windowName}`;
|
|
16
|
+
const target = `${this.config.sessionName}:${input.windowName}`;
|
|
15
17
|
const commandLine = input.command.map(shellQuote).join(" ");
|
|
16
18
|
const env = `ARCANE_AGENTS_WORKER_ID=${input.workerId}`;
|
|
17
19
|
if (await this.hasSession()) {
|
|
@@ -19,7 +21,7 @@ class TmuxAdapter {
|
|
|
19
21
|
"new-window",
|
|
20
22
|
"-d",
|
|
21
23
|
"-t",
|
|
22
|
-
this.sessionName,
|
|
24
|
+
this.config.sessionName,
|
|
23
25
|
"-n",
|
|
24
26
|
input.windowName,
|
|
25
27
|
"-c",
|
|
@@ -30,11 +32,12 @@ class TmuxAdapter {
|
|
|
30
32
|
]);
|
|
31
33
|
}
|
|
32
34
|
else {
|
|
35
|
+
this.managedDefaultsConfigured = false;
|
|
33
36
|
await this.runTmux([
|
|
34
37
|
"new-session",
|
|
35
38
|
"-d",
|
|
36
39
|
"-s",
|
|
37
|
-
this.sessionName,
|
|
40
|
+
this.config.sessionName,
|
|
38
41
|
"-n",
|
|
39
42
|
input.windowName,
|
|
40
43
|
"-c",
|
|
@@ -44,7 +47,7 @@ class TmuxAdapter {
|
|
|
44
47
|
commandLine
|
|
45
48
|
]);
|
|
46
49
|
}
|
|
47
|
-
await this.
|
|
50
|
+
await this.ensureManagedDefaults();
|
|
48
51
|
await this.setWindowMetadata(target, {
|
|
49
52
|
"@arcane_agents_managed": "1",
|
|
50
53
|
"@arcane_agents_worker_id": input.workerId,
|
|
@@ -63,7 +66,7 @@ class TmuxAdapter {
|
|
|
63
66
|
throw new Error(`Unable to resolve tmux pane for ${target}.`);
|
|
64
67
|
}
|
|
65
68
|
return {
|
|
66
|
-
session: this.sessionName,
|
|
69
|
+
session: this.config.sessionName,
|
|
67
70
|
window: input.windowName,
|
|
68
71
|
pane
|
|
69
72
|
};
|
|
@@ -75,23 +78,18 @@ class TmuxAdapter {
|
|
|
75
78
|
}
|
|
76
79
|
await this.stopGracefully(ref);
|
|
77
80
|
}
|
|
78
|
-
async
|
|
79
|
-
if (this.
|
|
81
|
+
async ensureManagedDefaults() {
|
|
82
|
+
if (this.managedDefaultsConfigured) {
|
|
80
83
|
return;
|
|
81
84
|
}
|
|
82
85
|
if (!(await this.hasSession())) {
|
|
83
86
|
return;
|
|
84
87
|
}
|
|
85
88
|
const copyCommand = await detectClipboardCopyCommand();
|
|
86
|
-
|
|
87
|
-
this.
|
|
88
|
-
return;
|
|
89
|
+
for (const command of (0, tmuxClient_1.buildFriendlyTmuxDefaults)({ copyCommand })) {
|
|
90
|
+
await this.runTmux(command).catch(() => undefined);
|
|
89
91
|
}
|
|
90
|
-
|
|
91
|
-
this.runTmux(["set-option", "-t", this.sessionName, "set-clipboard", "external"]),
|
|
92
|
-
this.runTmux(["set-option", "-t", this.sessionName, "copy-command", copyCommand])
|
|
93
|
-
]).catch(() => undefined);
|
|
94
|
-
this.sessionClipboardConfigured = true;
|
|
92
|
+
this.managedDefaultsConfigured = true;
|
|
95
93
|
}
|
|
96
94
|
async windowExists(ref) {
|
|
97
95
|
try {
|
|
@@ -160,7 +158,8 @@ class TmuxAdapter {
|
|
|
160
158
|
throw error;
|
|
161
159
|
}
|
|
162
160
|
await new Promise((resolve, reject) => {
|
|
163
|
-
const
|
|
161
|
+
const tmuxCommand = (0, tmuxClient_1.buildTmuxCommandPrefix)(this.config);
|
|
162
|
+
const guardCommand = `${tmuxCommand} has-session -t ${shellQuote(externalSession)} >/dev/null 2>&1 || exit 0; exec ${tmuxCommand} attach-session -t ${shellQuote(externalSession)}`;
|
|
164
163
|
const child = (0, node_child_process_1.spawn)("xdg-terminal-exec", ["sh", "-lc", guardCommand], {
|
|
165
164
|
detached: true,
|
|
166
165
|
stdio: "ignore"
|
|
@@ -219,7 +218,7 @@ class TmuxAdapter {
|
|
|
219
218
|
}
|
|
220
219
|
async hasSession() {
|
|
221
220
|
try {
|
|
222
|
-
await this.runTmux(["has-session", "-t", this.sessionName]);
|
|
221
|
+
await this.runTmux(["has-session", "-t", this.config.sessionName]);
|
|
223
222
|
return true;
|
|
224
223
|
}
|
|
225
224
|
catch {
|
|
@@ -230,7 +229,7 @@ class TmuxAdapter {
|
|
|
230
229
|
return `${ref.session}:${ref.window}`;
|
|
231
230
|
}
|
|
232
231
|
async runTmux(args) {
|
|
233
|
-
const { stdout } = await execFileAsync("tmux", args, {
|
|
232
|
+
const { stdout } = await execFileAsync("tmux", (0, tmuxClient_1.buildTmuxArgs)(args, this.config), {
|
|
234
233
|
maxBuffer: 1024 * 1024
|
|
235
234
|
});
|
|
236
235
|
return stdout.trimEnd();
|
|
@@ -246,21 +245,6 @@ class TmuxAdapter {
|
|
|
246
245
|
}
|
|
247
246
|
await this.runTmux(["send-keys", "-t", target, "C-c"]).catch(() => undefined);
|
|
248
247
|
await delay(220);
|
|
249
|
-
const paneInfo = await this.runTmux([
|
|
250
|
-
"list-panes",
|
|
251
|
-
"-t",
|
|
252
|
-
target,
|
|
253
|
-
"-F",
|
|
254
|
-
"#{pane_pid}\t#{pane_current_command}\t#{pane_dead}"
|
|
255
|
-
]).catch(() => "");
|
|
256
|
-
const [panePidText = "", paneCommand = "", paneDeadFlag = "1"] = firstLine(paneInfo).split("\t");
|
|
257
|
-
const panePid = Number.parseInt(panePidText, 10);
|
|
258
|
-
const currentCommand = paneCommand.trim().toLowerCase();
|
|
259
|
-
const paneDead = paneDeadFlag === "1";
|
|
260
|
-
if (!paneDead && Number.isFinite(panePid) && panePid > 1 && currentCommand !== "bash" && currentCommand !== "zsh") {
|
|
261
|
-
await terminateProcessGroup(panePid).catch(() => undefined);
|
|
262
|
-
await delay(90);
|
|
263
|
-
}
|
|
264
248
|
await this.runTmux(["kill-window", "-t", target]).catch(() => undefined);
|
|
265
249
|
}
|
|
266
250
|
}
|
|
@@ -292,7 +276,7 @@ function normalizeOption(value) {
|
|
|
292
276
|
return value;
|
|
293
277
|
}
|
|
294
278
|
async function detectClipboardCopyCommand() {
|
|
295
|
-
const candidates =
|
|
279
|
+
const candidates = clipboardCandidatesForEnvironment(process.platform, process.env);
|
|
296
280
|
for (const candidate of candidates) {
|
|
297
281
|
if (await commandExists(candidate.binary)) {
|
|
298
282
|
return candidate.command;
|
|
@@ -300,18 +284,22 @@ async function detectClipboardCopyCommand() {
|
|
|
300
284
|
}
|
|
301
285
|
return undefined;
|
|
302
286
|
}
|
|
303
|
-
function
|
|
287
|
+
function clipboardCandidatesForEnvironment(platform, env = process.env) {
|
|
304
288
|
if (platform === "darwin") {
|
|
305
289
|
return [{ binary: "pbcopy", command: "pbcopy" }];
|
|
306
290
|
}
|
|
307
291
|
if (platform === "win32") {
|
|
308
292
|
return [{ binary: "clip.exe", command: "clip.exe" }];
|
|
309
293
|
}
|
|
310
|
-
|
|
294
|
+
const linuxCandidates = [
|
|
311
295
|
{ binary: "wl-copy", command: "wl-copy" },
|
|
312
296
|
{ binary: "xclip", command: "xclip -selection clipboard -in" },
|
|
313
297
|
{ binary: "xsel", command: "xsel --clipboard --input" }
|
|
314
298
|
];
|
|
299
|
+
if (platform === "linux" && isWslEnvironment(env)) {
|
|
300
|
+
return [{ binary: "clip.exe", command: "clip.exe" }, ...linuxCandidates];
|
|
301
|
+
}
|
|
302
|
+
return linuxCandidates;
|
|
315
303
|
}
|
|
316
304
|
async function commandExists(binary) {
|
|
317
305
|
const locator = process.platform === "win32" ? "where" : "which";
|
|
@@ -325,15 +313,6 @@ async function commandExists(binary) {
|
|
|
325
313
|
return false;
|
|
326
314
|
}
|
|
327
315
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
maxBuffer: 1024 * 64
|
|
331
|
-
});
|
|
332
|
-
const pgid = Number.parseInt(stdout.trim(), 10);
|
|
333
|
-
if (!Number.isFinite(pgid) || pgid <= 1) {
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
await execFileAsync("kill", ["-TERM", `-${pgid}`], { maxBuffer: 1024 * 64 }).catch(() => undefined);
|
|
337
|
-
await delay(120);
|
|
338
|
-
await execFileAsync("kill", ["-KILL", `-${pgid}`], { maxBuffer: 1024 * 64 }).catch(() => undefined);
|
|
316
|
+
function isWslEnvironment(env) {
|
|
317
|
+
return Boolean(env.WSL_DISTRO_NAME || env.WSL_INTEROP || env.WSLENV);
|
|
339
318
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildTmuxArgs = buildTmuxArgs;
|
|
4
|
+
exports.buildTmuxAttachArgs = buildTmuxAttachArgs;
|
|
5
|
+
exports.buildTmuxCommandPrefix = buildTmuxCommandPrefix;
|
|
6
|
+
exports.buildFriendlyTmuxDefaults = buildFriendlyTmuxDefaults;
|
|
7
|
+
function buildTmuxArgs(args, options) {
|
|
8
|
+
return ["-L", options.socketName, ...args];
|
|
9
|
+
}
|
|
10
|
+
function buildTmuxAttachArgs(target, options) {
|
|
11
|
+
return buildTmuxArgs(["attach-session", "-t", target], options);
|
|
12
|
+
}
|
|
13
|
+
function buildTmuxCommandPrefix(options) {
|
|
14
|
+
return `tmux -L ${shellQuote(options.socketName)}`;
|
|
15
|
+
}
|
|
16
|
+
function buildFriendlyTmuxDefaults(options = {}) {
|
|
17
|
+
const copyAction = options.copyCommand ? "copy-pipe-and-cancel" : "copy-selection-and-cancel";
|
|
18
|
+
const commands = [
|
|
19
|
+
["set-option", "-g", "mouse", "on"],
|
|
20
|
+
["set-option", "-s", "escape-time", "0"],
|
|
21
|
+
["set-window-option", "-g", "history-limit", "100000"],
|
|
22
|
+
["bind-key", "-T", "copy-mode", "MouseDragEnd1Pane", "send-keys", "-X", copyAction],
|
|
23
|
+
["bind-key", "-T", "copy-mode-vi", "MouseDragEnd1Pane", "send-keys", "-X", copyAction]
|
|
24
|
+
];
|
|
25
|
+
if (options.copyCommand) {
|
|
26
|
+
commands.splice(3, 0, ["set-option", "-s", "set-clipboard", "external"], ["set-option", "-s", "copy-command", options.copyCommand]);
|
|
27
|
+
}
|
|
28
|
+
return commands;
|
|
29
|
+
}
|
|
30
|
+
function shellQuote(value) {
|
|
31
|
+
if (value.length === 0) {
|
|
32
|
+
return "''";
|
|
33
|
+
}
|
|
34
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const tmuxClient_1 = require("./tmuxClient");
|
|
5
|
+
const tmuxAdapter_1 = require("./tmuxAdapter");
|
|
6
|
+
(0, vitest_1.describe)("tmuxClient", () => {
|
|
7
|
+
(0, vitest_1.it)("prefixes tmux commands with the managed socket name", () => {
|
|
8
|
+
(0, vitest_1.expect)((0, tmuxClient_1.buildTmuxArgs)(["list-sessions"], { socketName: "arcane-agents" })).toEqual([
|
|
9
|
+
"-L",
|
|
10
|
+
"arcane-agents",
|
|
11
|
+
"list-sessions"
|
|
12
|
+
]);
|
|
13
|
+
});
|
|
14
|
+
(0, vitest_1.it)("builds attach-session commands on the managed socket", () => {
|
|
15
|
+
(0, vitest_1.expect)((0, tmuxClient_1.buildTmuxAttachArgs)("arcane-agents:worker-1", { socketName: "arcane-agents" })).toEqual([
|
|
16
|
+
"-L",
|
|
17
|
+
"arcane-agents",
|
|
18
|
+
"attach-session",
|
|
19
|
+
"-t",
|
|
20
|
+
"arcane-agents:worker-1"
|
|
21
|
+
]);
|
|
22
|
+
});
|
|
23
|
+
(0, vitest_1.it)("builds a shell-safe tmux command prefix", () => {
|
|
24
|
+
(0, vitest_1.expect)((0, tmuxClient_1.buildTmuxCommandPrefix)({ socketName: "arcane-agents-demo" })).toBe("tmux -L 'arcane-agents-demo'");
|
|
25
|
+
});
|
|
26
|
+
(0, vitest_1.it)("enables friendly defaults with clipboard-aware copy bindings when a copy command is available", () => {
|
|
27
|
+
(0, vitest_1.expect)((0, tmuxClient_1.buildFriendlyTmuxDefaults)({ copyCommand: "wl-copy" })).toEqual([
|
|
28
|
+
["set-option", "-g", "mouse", "on"],
|
|
29
|
+
["set-option", "-s", "escape-time", "0"],
|
|
30
|
+
["set-window-option", "-g", "history-limit", "100000"],
|
|
31
|
+
["set-option", "-s", "set-clipboard", "external"],
|
|
32
|
+
["set-option", "-s", "copy-command", "wl-copy"],
|
|
33
|
+
["bind-key", "-T", "copy-mode", "MouseDragEnd1Pane", "send-keys", "-X", "copy-pipe-and-cancel"],
|
|
34
|
+
["bind-key", "-T", "copy-mode-vi", "MouseDragEnd1Pane", "send-keys", "-X", "copy-pipe-and-cancel"]
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
(0, vitest_1.it)("falls back to tmux buffer copy bindings when no clipboard command is available", () => {
|
|
38
|
+
(0, vitest_1.expect)((0, tmuxClient_1.buildFriendlyTmuxDefaults)()).toEqual([
|
|
39
|
+
["set-option", "-g", "mouse", "on"],
|
|
40
|
+
["set-option", "-s", "escape-time", "0"],
|
|
41
|
+
["set-window-option", "-g", "history-limit", "100000"],
|
|
42
|
+
["bind-key", "-T", "copy-mode", "MouseDragEnd1Pane", "send-keys", "-X", "copy-selection-and-cancel"],
|
|
43
|
+
["bind-key", "-T", "copy-mode-vi", "MouseDragEnd1Pane", "send-keys", "-X", "copy-selection-and-cancel"]
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.it)("prefers the Windows clipboard bridge when running inside WSL", () => {
|
|
47
|
+
(0, vitest_1.expect)((0, tmuxAdapter_1.clipboardCandidatesForEnvironment)("linux", { WSL_DISTRO_NAME: "Ubuntu" })[0]).toEqual({
|
|
48
|
+
binary: "clip.exe",
|
|
49
|
+
command: "clip.exe"
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
(0, vitest_1.it)("keeps native Linux clipboard commands first outside WSL", () => {
|
|
53
|
+
(0, vitest_1.expect)((0, tmuxAdapter_1.clipboardCandidatesForEnvironment)("linux", {})[0]).toEqual({
|
|
54
|
+
binary: "wl-copy",
|
|
55
|
+
command: "wl-copy"
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|