arcane-agents 1.2.1 → 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-BCnWppkv.js → index-DHyA1AST.js} +20 -20
- package/dist/client/index.html +2 -2
- package/dist/server/server/bootstrap/serverContext.js +5 -3
- package/dist/server/server/cli.js +120 -2
- package/dist/server/server/config/schema.js +5 -1
- package/dist/server/server/orchestrator/orchestratorService.test.js +1 -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/engine/signalContext.js +3 -2
- package/dist/server/server/status/engine/stateMachine/constants.js +1 -1
- package/dist/server/server/status/engine/stateMachine/decision.test.js +1 -0
- package/dist/server/server/status/engine/stateMachine/helpers.js +3 -0
- package/dist/server/server/status/engine/stateMachine/helpers.test.js +4 -1
- package/dist/server/server/status/engine/stateMachine/workingEvidence.js +15 -0
- package/dist/server/server/status/statusEvaluator.js +3 -2
- package/dist/server/server/status/statusMonitor.js +14 -3
- package/dist/server/server/status/statusMonitor.test.js +3 -2
- package/dist/server/server/status/statusPipeline.js +5 -3
- 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 +5 -2
- package/dist/server/shared/mapConstants.js +4 -0
- package/package.json +4 -3
- package/dist/client/assets/index-Di_KBFPW.css +0 -32
package/dist/client/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Arcane Agents</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-DHyA1AST.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CT0NFttM.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -21,7 +21,9 @@ async function createServerContext(sessionName) {
|
|
|
21
21
|
const baseConfig = (0, loadConfig_1.loadResolvedConfig)(paths);
|
|
22
22
|
if ((0, loadConfig_1.isNonDefaultSession)(sessionName)) {
|
|
23
23
|
const baseTmuxName = baseConfig.backend.tmux.sessionName;
|
|
24
|
+
const baseSocketName = baseConfig.backend.tmux.socketName;
|
|
24
25
|
baseConfig.backend.tmux.sessionName = `${baseTmuxName}-${sessionName}`;
|
|
26
|
+
baseConfig.backend.tmux.socketName = `${baseSocketName}-${sessionName}`;
|
|
25
27
|
}
|
|
26
28
|
const discoveryService = new discovery_1.DiscoveryService();
|
|
27
29
|
const initialDiscovery = await discoveryService.discover(baseConfig);
|
|
@@ -29,8 +31,8 @@ async function createServerContext(sessionName) {
|
|
|
29
31
|
console.warn(`[arcane-agents] ${warning}`);
|
|
30
32
|
}
|
|
31
33
|
const workers = new workerRepository_1.WorkerRepository(paths.dbPath);
|
|
32
|
-
const tmux = new tmuxAdapter_1.TmuxAdapter(baseConfig.backend.tmux
|
|
33
|
-
await tmux.
|
|
34
|
+
const tmux = new tmuxAdapter_1.TmuxAdapter(baseConfig.backend.tmux);
|
|
35
|
+
await tmux.ensureManagedDefaults();
|
|
34
36
|
const orchestrator = new orchestratorService_1.OrchestratorService(baseConfig, workers, tmux);
|
|
35
37
|
orchestrator.setDiscoveredProjects(initialDiscovery.projects);
|
|
36
38
|
const hub = new realtimeHub_1.RealtimeHub();
|
|
@@ -46,7 +48,7 @@ async function createServerContext(sessionName) {
|
|
|
46
48
|
workerId
|
|
47
49
|
});
|
|
48
50
|
}, baseConfig);
|
|
49
|
-
const terminalBridge = new terminalBridge_1.TerminalBridge(workers, {
|
|
51
|
+
const terminalBridge = new terminalBridge_1.TerminalBridge(workers, baseConfig.backend.tmux, {
|
|
50
52
|
onSubmittedInput: () => {
|
|
51
53
|
statusMonitor.requestPollSoon();
|
|
52
54
|
},
|
|
@@ -10,6 +10,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
10
10
|
const node_readline_1 = __importDefault(require("node:readline"));
|
|
11
11
|
const bootstrapApp_1 = require("./bootstrapApp");
|
|
12
12
|
const loadConfig_1 = require("./config/loadConfig");
|
|
13
|
+
const prerequisites_1 = require("./setup/prerequisites");
|
|
13
14
|
const appRoot_1 = require("./utils/appRoot");
|
|
14
15
|
const path_1 = require("./utils/path");
|
|
15
16
|
function extractSessionFlag(args) {
|
|
@@ -57,6 +58,8 @@ async function runCli() {
|
|
|
57
58
|
return runStart(sessionName);
|
|
58
59
|
case "init":
|
|
59
60
|
return runInit(commandArgs);
|
|
61
|
+
case "setup":
|
|
62
|
+
return runSetup(commandArgs);
|
|
60
63
|
case "config":
|
|
61
64
|
return runConfig(commandArgs);
|
|
62
65
|
case "doctor":
|
|
@@ -126,6 +129,83 @@ function runInit(args) {
|
|
|
126
129
|
console.log("[arcane-agents] next: edit it with 'arcane-agents config edit'.");
|
|
127
130
|
return 0;
|
|
128
131
|
}
|
|
132
|
+
async function runSetup(args) {
|
|
133
|
+
if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
|
|
134
|
+
printSetupHelp();
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
if (args.length > 0) {
|
|
138
|
+
console.error(`[arcane-agents] unknown setup options: ${args.join(", ")}`);
|
|
139
|
+
printSetupHelp();
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
console.log("[arcane-agents] setup");
|
|
143
|
+
const tmuxPath = findExecutable("tmux");
|
|
144
|
+
if (tmuxPath) {
|
|
145
|
+
console.log(`[arcane-agents] tmux: ${tmuxPath}`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const installRecommendation = (0, prerequisites_1.recommendTmuxInstall)({
|
|
149
|
+
platform: process.platform,
|
|
150
|
+
lookupCommand: findExecutable,
|
|
151
|
+
isRootUser: process.getuid?.() === 0
|
|
152
|
+
});
|
|
153
|
+
console.log("[arcane-agents] tmux is required but was not found on PATH.");
|
|
154
|
+
if (installRecommendation) {
|
|
155
|
+
console.log(`[arcane-agents] suggested install (${installRecommendation.packageManager}): ${installRecommendation.command}`);
|
|
156
|
+
if (installRecommendation.note) {
|
|
157
|
+
console.log(`[arcane-agents] note: ${installRecommendation.note}`);
|
|
158
|
+
}
|
|
159
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
160
|
+
const approved = await promptConfirm(`[arcane-agents] run that command now? [y/N] `);
|
|
161
|
+
if (approved) {
|
|
162
|
+
const exitCode = runShellCommand(installRecommendation.command);
|
|
163
|
+
if (exitCode !== 0) {
|
|
164
|
+
console.error(`[arcane-agents] install command failed with exit code ${exitCode}.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
console.log("[arcane-agents] skipped tmux install.");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
console.log("[arcane-agents] non-interactive terminal detected; not running install command automatically.");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
console.log("[arcane-agents] could not determine a package-manager command for tmux on this system.");
|
|
177
|
+
if (process.platform === "win32") {
|
|
178
|
+
console.log("[arcane-agents] run Arcane Agents inside WSL2 or another Unix-like environment, then install tmux there.");
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
console.log("[arcane-agents] install tmux manually, then rerun 'arcane-agents setup' or 'arcane-agents doctor'.");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const paths = (0, loadConfig_1.getArcaneAgentsPaths)();
|
|
186
|
+
try {
|
|
187
|
+
const configResult = ensureStarterConfig(paths);
|
|
188
|
+
if (configResult.created) {
|
|
189
|
+
console.log(`[arcane-agents] wrote starter config: ${paths.configPath}`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.log(`[arcane-agents] config: ${paths.configPath}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
197
|
+
console.error(`[arcane-agents] failed to prepare config: ${detail}`);
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
const doctorExitCode = runDoctor();
|
|
201
|
+
if (doctorExitCode === 0) {
|
|
202
|
+
console.log("[arcane-agents] next: edit your config if needed with 'arcane-agents config edit', then run 'arcane-agents'.");
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
console.log("[arcane-agents] fix the issues above, then rerun 'arcane-agents setup' or 'arcane-agents doctor'.");
|
|
206
|
+
}
|
|
207
|
+
return doctorExitCode;
|
|
208
|
+
}
|
|
129
209
|
function runConfig(args) {
|
|
130
210
|
if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
|
|
131
211
|
printConfigHelp();
|
|
@@ -359,7 +439,18 @@ function runDoctor() {
|
|
|
359
439
|
checks.push({ status: "ok", label: "tmux", detail: tmuxPath });
|
|
360
440
|
}
|
|
361
441
|
else {
|
|
362
|
-
|
|
442
|
+
const installRecommendation = (0, prerequisites_1.recommendTmuxInstall)({
|
|
443
|
+
platform: process.platform,
|
|
444
|
+
lookupCommand: findExecutable,
|
|
445
|
+
isRootUser: process.getuid?.() === 0
|
|
446
|
+
});
|
|
447
|
+
checks.push({
|
|
448
|
+
status: "fail",
|
|
449
|
+
label: "tmux",
|
|
450
|
+
detail: installRecommendation
|
|
451
|
+
? `not found on PATH (install with: ${installRecommendation.command})`
|
|
452
|
+
: "not found on PATH"
|
|
453
|
+
});
|
|
363
454
|
}
|
|
364
455
|
const paths = (0, loadConfig_1.getArcaneAgentsPaths)();
|
|
365
456
|
if (node_fs_1.default.existsSync(paths.configPath)) {
|
|
@@ -369,7 +460,7 @@ function runDoctor() {
|
|
|
369
460
|
checks.push({
|
|
370
461
|
status: "warn",
|
|
371
462
|
label: "Config",
|
|
372
|
-
detail: `missing at ${paths.configPath} (auto-created on 'arcane-agents start'
|
|
463
|
+
detail: `missing at ${paths.configPath} (auto-created on 'arcane-agents start' or 'arcane-agents setup')`
|
|
373
464
|
});
|
|
374
465
|
}
|
|
375
466
|
const configResult = safeLoadConfig(paths);
|
|
@@ -468,6 +559,7 @@ function printHelp() {
|
|
|
468
559
|
Usage:
|
|
469
560
|
arcane-agents [start] [--session <name>]
|
|
470
561
|
arcane-agents init [--force]
|
|
562
|
+
arcane-agents setup
|
|
471
563
|
arcane-agents config [path|show|edit]
|
|
472
564
|
arcane-agents sessions [list|delete <name>]
|
|
473
565
|
arcane-agents doctor
|
|
@@ -477,6 +569,7 @@ Usage:
|
|
|
477
569
|
Commands:
|
|
478
570
|
start Start the Arcane Agents server
|
|
479
571
|
init Write ~/.config/arcane-agents/config.yaml from config.example.yaml
|
|
572
|
+
setup Guided first-run setup for tmux, config, and dependency checks
|
|
480
573
|
config Print, show, or edit config files
|
|
481
574
|
sessions List or delete named sessions
|
|
482
575
|
doctor Check dependencies and runtime command availability
|
|
@@ -493,6 +586,20 @@ Config paths:
|
|
|
493
586
|
local override: ${paths.localOverridePath}
|
|
494
587
|
`);
|
|
495
588
|
}
|
|
589
|
+
function printSetupHelp() {
|
|
590
|
+
console.log(`Arcane Agents setup
|
|
591
|
+
|
|
592
|
+
Usage:
|
|
593
|
+
arcane-agents setup
|
|
594
|
+
|
|
595
|
+
What it does:
|
|
596
|
+
- checks whether tmux is installed
|
|
597
|
+
- suggests a platform-specific tmux install command
|
|
598
|
+
- can run that command after confirmation in an interactive terminal
|
|
599
|
+
- ensures ~/.config/arcane-agents/config.yaml exists
|
|
600
|
+
- runs 'arcane-agents doctor'
|
|
601
|
+
`);
|
|
602
|
+
}
|
|
496
603
|
function printVersion() {
|
|
497
604
|
console.log(readPackageVersion());
|
|
498
605
|
}
|
|
@@ -556,6 +663,17 @@ function resolvePathToken(token) {
|
|
|
556
663
|
function hasFlag(args, flag) {
|
|
557
664
|
return args.includes(flag);
|
|
558
665
|
}
|
|
666
|
+
function runShellCommand(command) {
|
|
667
|
+
const result = (0, node_child_process_1.spawnSync)("sh", ["-lc", command], {
|
|
668
|
+
stdio: "inherit"
|
|
669
|
+
});
|
|
670
|
+
if (result.error) {
|
|
671
|
+
const detail = result.error.message;
|
|
672
|
+
console.error(`[arcane-agents] failed to launch shell command '${command}': ${detail}`);
|
|
673
|
+
return 1;
|
|
674
|
+
}
|
|
675
|
+
return result.status ?? 1;
|
|
676
|
+
}
|
|
559
677
|
void runCli()
|
|
560
678
|
.then((exitCode) => {
|
|
561
679
|
if (exitCode !== 0) {
|
|
@@ -21,7 +21,8 @@ const projectSchema = zod_1.z.object({
|
|
|
21
21
|
});
|
|
22
22
|
const runtimeSchema = zod_1.z.object({
|
|
23
23
|
command: zod_1.z.array(zod_1.z.string().min(1)).min(1),
|
|
24
|
-
label: zod_1.z.string().min(1)
|
|
24
|
+
label: zod_1.z.string().min(1),
|
|
25
|
+
freshnessWindowMs: zod_1.z.number().int().min(1_000).optional()
|
|
25
26
|
});
|
|
26
27
|
const shortcutSchema = zod_1.z.object({
|
|
27
28
|
label: zod_1.z.string().min(1),
|
|
@@ -41,6 +42,7 @@ const discoveryRuleSchema = zod_1.z.object({
|
|
|
41
42
|
});
|
|
42
43
|
const backendSchema = zod_1.z.object({
|
|
43
44
|
tmux: zod_1.z.object({
|
|
45
|
+
socketName: zod_1.z.string().min(1),
|
|
44
46
|
sessionName: zod_1.z.string().min(1),
|
|
45
47
|
pollIntervalMs: zod_1.z.number().int().min(250)
|
|
46
48
|
})
|
|
@@ -73,6 +75,7 @@ exports.partialConfigSchema = zod_1.z
|
|
|
73
75
|
.object({
|
|
74
76
|
tmux: zod_1.z
|
|
75
77
|
.object({
|
|
78
|
+
socketName: zod_1.z.string().min(1).optional(),
|
|
76
79
|
sessionName: zod_1.z.string().min(1).optional(),
|
|
77
80
|
pollIntervalMs: zod_1.z.number().int().min(250).optional()
|
|
78
81
|
})
|
|
@@ -128,6 +131,7 @@ function createDefaultConfig() {
|
|
|
128
131
|
},
|
|
129
132
|
backend: {
|
|
130
133
|
tmux: {
|
|
134
|
+
socketName: "arcane-agents",
|
|
131
135
|
sessionName: "arcane-agents",
|
|
132
136
|
pollIntervalMs: 2500
|
|
133
137
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.recommendTmuxInstall = recommendTmuxInstall;
|
|
4
|
+
function recommendTmuxInstall(options) {
|
|
5
|
+
const sudoPrefix = options.isRootUser ? "" : "sudo ";
|
|
6
|
+
if (options.platform === "darwin" && options.lookupCommand("brew")) {
|
|
7
|
+
return {
|
|
8
|
+
dependency: "tmux",
|
|
9
|
+
packageManager: "Homebrew",
|
|
10
|
+
command: "brew install tmux",
|
|
11
|
+
note: "This installs tmux only."
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (options.platform !== "linux") {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
if (options.lookupCommand("brew")) {
|
|
18
|
+
return {
|
|
19
|
+
dependency: "tmux",
|
|
20
|
+
packageManager: "Homebrew",
|
|
21
|
+
command: "brew install tmux",
|
|
22
|
+
note: "This installs tmux only."
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (options.lookupCommand("apt")) {
|
|
26
|
+
return {
|
|
27
|
+
dependency: "tmux",
|
|
28
|
+
packageManager: "apt",
|
|
29
|
+
command: `${sudoPrefix}apt install -y tmux`,
|
|
30
|
+
note: "This installs tmux only and does not run a full system upgrade."
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (options.lookupCommand("apt-get")) {
|
|
34
|
+
return {
|
|
35
|
+
dependency: "tmux",
|
|
36
|
+
packageManager: "apt-get",
|
|
37
|
+
command: `${sudoPrefix}apt-get install -y tmux`,
|
|
38
|
+
note: "This installs tmux only and does not run a full system upgrade."
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (options.lookupCommand("dnf")) {
|
|
42
|
+
return {
|
|
43
|
+
dependency: "tmux",
|
|
44
|
+
packageManager: "dnf",
|
|
45
|
+
command: `${sudoPrefix}dnf install -y tmux`,
|
|
46
|
+
note: "This installs tmux only."
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (options.lookupCommand("pacman")) {
|
|
50
|
+
return {
|
|
51
|
+
dependency: "tmux",
|
|
52
|
+
packageManager: "pacman",
|
|
53
|
+
command: `${sudoPrefix}pacman -S --needed tmux`,
|
|
54
|
+
note: "This installs tmux only and skips reinstalling it if already present."
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (options.lookupCommand("zypper")) {
|
|
58
|
+
return {
|
|
59
|
+
dependency: "tmux",
|
|
60
|
+
packageManager: "zypper",
|
|
61
|
+
command: `${sudoPrefix}zypper install -y tmux`,
|
|
62
|
+
note: "This installs tmux only."
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (options.lookupCommand("apk")) {
|
|
66
|
+
return {
|
|
67
|
+
dependency: "tmux",
|
|
68
|
+
packageManager: "apk",
|
|
69
|
+
command: `${sudoPrefix}apk add tmux`,
|
|
70
|
+
note: "This installs tmux only."
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const prerequisites_1 = require("./prerequisites");
|
|
5
|
+
function lookupFor(commands) {
|
|
6
|
+
const available = new Set(commands);
|
|
7
|
+
return (command) => (available.has(command) ? `/usr/bin/${command}` : undefined);
|
|
8
|
+
}
|
|
9
|
+
(0, vitest_1.describe)("recommendTmuxInstall", () => {
|
|
10
|
+
(0, vitest_1.it)("prefers Homebrew on macOS", () => {
|
|
11
|
+
(0, vitest_1.expect)((0, prerequisites_1.recommendTmuxInstall)({
|
|
12
|
+
platform: "darwin",
|
|
13
|
+
lookupCommand: lookupFor(["brew"])
|
|
14
|
+
})).toEqual({
|
|
15
|
+
dependency: "tmux",
|
|
16
|
+
packageManager: "Homebrew",
|
|
17
|
+
command: "brew install tmux",
|
|
18
|
+
note: "This installs tmux only."
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
(0, vitest_1.it)("returns an apt install command on Debian-like systems", () => {
|
|
22
|
+
(0, vitest_1.expect)((0, prerequisites_1.recommendTmuxInstall)({
|
|
23
|
+
platform: "linux",
|
|
24
|
+
lookupCommand: lookupFor(["apt"])
|
|
25
|
+
})).toEqual({
|
|
26
|
+
dependency: "tmux",
|
|
27
|
+
packageManager: "apt",
|
|
28
|
+
command: "sudo apt install -y tmux",
|
|
29
|
+
note: "This installs tmux only and does not run a full system upgrade."
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
(0, vitest_1.it)("omits sudo when already running as root", () => {
|
|
33
|
+
(0, vitest_1.expect)((0, prerequisites_1.recommendTmuxInstall)({
|
|
34
|
+
platform: "linux",
|
|
35
|
+
lookupCommand: lookupFor(["pacman"]),
|
|
36
|
+
isRootUser: true
|
|
37
|
+
})).toEqual({
|
|
38
|
+
dependency: "tmux",
|
|
39
|
+
packageManager: "pacman",
|
|
40
|
+
command: "pacman -S --needed tmux",
|
|
41
|
+
note: "This installs tmux only and skips reinstalling it if already present."
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
(0, vitest_1.it)("returns undefined when no supported package manager is available", () => {
|
|
45
|
+
(0, vitest_1.expect)((0, prerequisites_1.recommendTmuxInstall)({
|
|
46
|
+
platform: "linux",
|
|
47
|
+
lookupCommand: lookupFor([])
|
|
48
|
+
})).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.buildWorkerStatusSignalContext = buildWorkerStatusSignalContext;
|
|
4
4
|
const activityParser_1 = require("../activityParser");
|
|
5
5
|
const runtimeSignals_1 = require("../runtimeSignals");
|
|
6
|
-
function buildWorkerStatusSignalContext({ worker, currentCommand, output, observation, transcriptSnapshot, runtimeProcess, nowMs, interactiveCommands }) {
|
|
6
|
+
function buildWorkerStatusSignalContext({ worker, currentCommand, output, observation, transcriptSnapshot, runtimeProcess, nowMs, interactiveCommands, runtimeFreshnessWindowMs }) {
|
|
7
7
|
const parsed = (0, activityParser_1.parseActivity)(currentCommand, output);
|
|
8
8
|
const commandLower = currentCommand.toLowerCase();
|
|
9
9
|
const wrappedRuntime = runtimeProcess?.runtime;
|
|
@@ -49,6 +49,7 @@ function buildWorkerStatusSignalContext({ worker, currentCommand, output, observ
|
|
|
49
49
|
outputQuietForMs,
|
|
50
50
|
commandQuietForMs,
|
|
51
51
|
workerAgeMs,
|
|
52
|
-
interactiveCommands
|
|
52
|
+
interactiveCommands,
|
|
53
|
+
runtimeFreshnessWindowMs
|
|
53
54
|
};
|
|
54
55
|
}
|
|
@@ -17,7 +17,7 @@ const openCodeSpawnGraceMs = 5_000;
|
|
|
17
17
|
exports.openCodeSpawnGraceMs = openCodeSpawnGraceMs;
|
|
18
18
|
const codexSpawnGraceMs = 5_000;
|
|
19
19
|
exports.codexSpawnGraceMs = codexSpawnGraceMs;
|
|
20
|
-
const genericWorkingFreshWindowMs =
|
|
20
|
+
const genericWorkingFreshWindowMs = 20_000;
|
|
21
21
|
exports.genericWorkingFreshWindowMs = genericWorkingFreshWindowMs;
|
|
22
22
|
const claudeWorkingFreshWindowMs = 10_000;
|
|
23
23
|
exports.claudeWorkingFreshWindowMs = claudeWorkingFreshWindowMs;
|
|
@@ -53,6 +53,9 @@ function looksLikeActiveRuntimeText(activityText) {
|
|
|
53
53
|
return normalized.startsWith("thinking");
|
|
54
54
|
}
|
|
55
55
|
function statusFreshnessWindowMs(context) {
|
|
56
|
+
if (context.runtimeFreshnessWindowMs !== undefined) {
|
|
57
|
+
return context.runtimeFreshnessWindowMs;
|
|
58
|
+
}
|
|
56
59
|
if (context.isClaudeSession) {
|
|
57
60
|
return constants_1.claudeWorkingFreshWindowMs;
|
|
58
61
|
}
|
|
@@ -64,6 +64,7 @@ function createContext(overrides = {}) {
|
|
|
64
64
|
commandQuietForMs: 300,
|
|
65
65
|
workerAgeMs: 10_000,
|
|
66
66
|
interactiveCommands: new Set(),
|
|
67
|
+
runtimeFreshnessWindowMs: undefined,
|
|
67
68
|
...overrides
|
|
68
69
|
};
|
|
69
70
|
}
|
|
@@ -94,7 +95,9 @@ function createContext(overrides = {}) {
|
|
|
94
95
|
(0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext({ isClaudeSession: true }))).toBe(10_000);
|
|
95
96
|
(0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext({ isOpenCodeSession: true }))).toBe(12_000);
|
|
96
97
|
(0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext({ isCodexSession: true }))).toBe(10_000);
|
|
97
|
-
(0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext())).toBe(
|
|
98
|
+
(0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext())).toBe(20_000);
|
|
99
|
+
(0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext({ runtimeFreshnessWindowMs: 45_000 }))).toBe(45_000);
|
|
100
|
+
(0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext({ isClaudeSession: true, runtimeFreshnessWindowMs: 30_000 }))).toBe(30_000);
|
|
98
101
|
});
|
|
99
102
|
(0, vitest_1.it)("handles helper utility behavior", () => {
|
|
100
103
|
const values = [];
|
|
@@ -93,6 +93,21 @@ function collectWorkingEvidence(context, hasRecoverableParserError) {
|
|
|
93
93
|
activityToolCandidates.push(context.parsed.activity.tool);
|
|
94
94
|
(0, helpers_1.pushMaybe)(activityPathCandidates, context.parsed.activity.filePath);
|
|
95
95
|
}
|
|
96
|
+
if (context.worker.status === "working" &&
|
|
97
|
+
!(0, helpers_1.isShellCommand)(context.commandLower) &&
|
|
98
|
+
!(0, helpers_1.isInteractiveCommand)(context) &&
|
|
99
|
+
context.outputQuietForMs <= (0, helpers_1.statusFreshnessWindowMs)(context) &&
|
|
100
|
+
strongReasons.length === 0 &&
|
|
101
|
+
weakReasons.length === 0) {
|
|
102
|
+
weakReasons.push({
|
|
103
|
+
code: "output-still-fresh",
|
|
104
|
+
message: "Output is still within the freshness window; maintaining working status.",
|
|
105
|
+
detail: `${Math.round(context.outputQuietForMs)}ms quiet (window: ${(0, helpers_1.statusFreshnessWindowMs)(context)}ms)`
|
|
106
|
+
});
|
|
107
|
+
(0, helpers_1.pushMaybe)(activityTextCandidates, context.runtimeActivityText ?? context.worker.activityText);
|
|
108
|
+
activityToolCandidates.push(context.worker.activityTool ?? "terminal");
|
|
109
|
+
(0, helpers_1.pushMaybe)(activityPathCandidates, context.worker.activityPath);
|
|
110
|
+
}
|
|
96
111
|
return {
|
|
97
112
|
strongReasons,
|
|
98
113
|
weakReasons,
|
|
@@ -3,7 +3,7 @@ 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, runtimeProcess, 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,
|
|
@@ -12,7 +12,8 @@ function evaluateWorkerStatus({ worker, currentCommand, output, observation, tra
|
|
|
12
12
|
transcriptSnapshot,
|
|
13
13
|
runtimeProcess,
|
|
14
14
|
nowMs: Date.now(),
|
|
15
|
-
interactiveCommands
|
|
15
|
+
interactiveCommands,
|
|
16
|
+
runtimeFreshnessWindowMs
|
|
16
17
|
});
|
|
17
18
|
return (0, stateMachine_1.deriveWorkerStatusDecision)(context);
|
|
18
19
|
}
|
|
@@ -46,6 +46,7 @@ class StatusMonitor {
|
|
|
46
46
|
traceMode = resolveStatusTraceMode();
|
|
47
47
|
workerPollConcurrency = resolveStatusPollConcurrency();
|
|
48
48
|
interactiveCommands;
|
|
49
|
+
runtimeFreshnessOverrides;
|
|
49
50
|
constructor(workers, tmux, pollIntervalMs, onWorkerUpdated, onWorkerRemoved, config) {
|
|
50
51
|
this.workers = workers;
|
|
51
52
|
this.tmux = tmux;
|
|
@@ -53,6 +54,13 @@ class StatusMonitor {
|
|
|
53
54
|
this.onWorkerUpdated = onWorkerUpdated;
|
|
54
55
|
this.onWorkerRemoved = onWorkerRemoved;
|
|
55
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;
|
|
56
64
|
}
|
|
57
65
|
start() {
|
|
58
66
|
if (this.intervalId) {
|
|
@@ -175,7 +183,8 @@ class StatusMonitor {
|
|
|
175
183
|
tmux: this.tmux,
|
|
176
184
|
paneObservation: this.paneObservation,
|
|
177
185
|
claudeTranscript: this.claudeTranscript,
|
|
178
|
-
interactiveCommands: this.interactiveCommands
|
|
186
|
+
interactiveCommands: this.interactiveCommands,
|
|
187
|
+
runtimeFreshnessWindowMs: this.runtimeFreshnessOverrides.get(worker.runtimeId)
|
|
179
188
|
});
|
|
180
189
|
if (!signals) {
|
|
181
190
|
this.removeWorker(worker.id);
|
|
@@ -281,7 +290,8 @@ class StatusMonitor {
|
|
|
281
290
|
if (this.traceMode !== "verbose") {
|
|
282
291
|
return;
|
|
283
292
|
}
|
|
284
|
-
|
|
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 ` +
|
|
285
295
|
`avgWorker=${Math.round(timing.averageWorkerDurationMs)}ms maxWorker=${Math.round(timing.maxWorkerDurationMs)}ms ` +
|
|
286
296
|
`outcomes={updated:${timing.outcomeCounts.updated},unchanged:${timing.outcomeCounts.unchanged},removed:${timing.outcomeCounts.removed},failed:${timing.outcomeCounts.failed}}`);
|
|
287
297
|
}
|
|
@@ -313,7 +323,8 @@ class StatusMonitor {
|
|
|
313
323
|
`opencode=${evaluation.facts.isOpenCodeSession ? 1 : 0} ` +
|
|
314
324
|
`codex=${evaluation.facts.isCodexSession ? 1 : 0} ` +
|
|
315
325
|
`runtimeProc=${evaluation.facts.hasActiveRuntimeProcess ? 1 : 0}`;
|
|
316
|
-
|
|
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}`);
|
|
317
328
|
}
|
|
318
329
|
recordStatusTransition(worker, evaluation) {
|
|
319
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,
|
|
@@ -96,7 +96,8 @@ function createSignals() {
|
|
|
96
96
|
},
|
|
97
97
|
transcriptSnapshot: undefined,
|
|
98
98
|
runtimeProcess: undefined,
|
|
99
|
-
interactiveCommands: new Set()
|
|
99
|
+
interactiveCommands: new Set(),
|
|
100
|
+
runtimeFreshnessWindowMs: undefined
|
|
100
101
|
};
|
|
101
102
|
}
|
|
102
103
|
function createEvaluation(status) {
|
|
@@ -7,7 +7,7 @@ const runtimeSignals_1 = require("./runtimeSignals");
|
|
|
7
7
|
const runtimeProcess_1 = require("./runtime/runtimeProcess");
|
|
8
8
|
const statusEvaluator_1 = require("./statusEvaluator");
|
|
9
9
|
const paneObservation_1 = require("./paneObservation");
|
|
10
|
-
async function collectWorkerStatusSignals({ worker, tmux, paneObservation, claudeTranscript, interactiveCommands }) {
|
|
10
|
+
async function collectWorkerStatusSignals({ worker, tmux, paneObservation, claudeTranscript, interactiveCommands, runtimeFreshnessWindowMs }) {
|
|
11
11
|
const paneState = await tmux.getPaneState(worker.tmuxRef);
|
|
12
12
|
if (paneState.isDead) {
|
|
13
13
|
return undefined;
|
|
@@ -28,7 +28,8 @@ async function collectWorkerStatusSignals({ worker, tmux, paneObservation, claud
|
|
|
28
28
|
observation,
|
|
29
29
|
transcriptSnapshot,
|
|
30
30
|
runtimeProcess,
|
|
31
|
-
interactiveCommands
|
|
31
|
+
interactiveCommands,
|
|
32
|
+
runtimeFreshnessWindowMs
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
function evaluateWorkerStatusSignals(worker, signals) {
|
|
@@ -39,7 +40,8 @@ function evaluateWorkerStatusSignals(worker, signals) {
|
|
|
39
40
|
observation: signals.observation,
|
|
40
41
|
transcriptSnapshot: signals.transcriptSnapshot,
|
|
41
42
|
runtimeProcess: signals.runtimeProcess,
|
|
42
|
-
interactiveCommands: signals.interactiveCommands
|
|
43
|
+
interactiveCommands: signals.interactiveCommands,
|
|
44
|
+
runtimeFreshnessWindowMs: signals.runtimeFreshnessWindowMs
|
|
43
45
|
});
|
|
44
46
|
}
|
|
45
47
|
function normalizeWorkerStatusEvaluation(evaluation) {
|