crewswarm 0.9.0 → 0.9.2
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 +2 -2
- package/apps/dashboard/dist/assets/{chat-core-CMoqlR6D.js → chat-core-Cx4sTxDd.js} +1 -1
- package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
- package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
- package/apps/dashboard/dist/assets/{components-CSUb80ze.js → components-BS9fQjE_.js} +1 -1
- package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
- package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js +1 -0
- package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
- package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
- package/apps/dashboard/dist/assets/{index-DqVVQLTW.js → index-DnClJ1ee.js} +2 -2
- package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
- package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
- package/apps/dashboard/dist/assets/{setup-wizard-D4g5DMhW.js → setup-wizard-CA0Or47w.js} +1 -1
- package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-agents-tab-BThdsdJY.js → tab-agents-tab-BgpIsjkw.js} +1 -1
- package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-benchmarks-tab-DfCuAClu.js → tab-benchmarks-tab-BHjKCPm3.js} +1 -1
- package/apps/dashboard/dist/assets/{tab-comms-tab-eHpOSBhG.js → tab-comms-tab-kguqTIzD.js} +1 -1
- package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-contacts-tab-5LHSthJM.js → tab-contacts-tab-DiOyMYth.js} +1 -1
- package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-engines-tab-C3DYxTwy.js → tab-engines-tab-BsdZVvU0.js} +1 -1
- package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-memory-tab-C59BYFQD.js → tab-memory-tab-Cu6u13EQ.js} +1 -1
- package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-models-tab-CQzvaeVh.js → tab-models-tab-BLEjmd19.js} +1 -1
- package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-pm-loop-tab-D7mnDelU.js → tab-pm-loop-tab-Bfd449B4.js} +1 -1
- package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-projects-tab-C6h2Mv1K.js → tab-projects-tab-DhNWnlzt.js} +1 -1
- package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-prompts-tab-C0wZvWK3.js → tab-prompts-tab-DVkUNaJd.js} +1 -1
- package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-services-tab-DBj_w3bc.js → tab-services-tab-DU_LH3uG.js} +1 -1
- package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-settings-tab-ezeqAjZk.js → tab-settings-tab-Bn4nXtDe.js} +1 -1
- package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-skills-tab-BYdU2whk.js → tab-skills-tab-BpY0uZHW.js} +1 -1
- package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-spending-tab-Bg6w9t_p.js → tab-spending-tab-DEccQHnt.js} +1 -1
- package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-swarm-chat-tab-BBV9HB2X.js → tab-swarm-chat-tab-BNrd88-r.js} +1 -1
- package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-swarm-tab-ChqLlEVs.js → tab-swarm-tab-B1AcjL1W.js} +1 -1
- package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-usage-tab-B2UWXenJ.js → tab-usage-tab-BIOOnB-Y.js} +1 -1
- package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-workflows-tab-6QSXLJ0i.js → tab-workflows-tab-B-soSy1k.js} +1 -1
- package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
- package/apps/dashboard/dist/index.html +23 -23
- package/apps/dashboard/dist/index.html.br +0 -0
- package/apps/dashboard/dist/index.html.gz +0 -0
- package/apps/dashboard/index.html +71 -1
- package/apps/dashboard/src/app.js +5 -0
- package/apps/dashboard/src/core/dom.js +8 -0
- package/apps/dashboard/src/tabs/settings-tab.js +58 -0
- package/apps/vibe/.crew/agent-memory/pipeline.json +12 -1
- package/apps/vibe/.crew/cost.json +3 -3
- package/apps/vibe/.crew/json-parse-metrics.jsonl +1 -0
- package/apps/vibe/.crew/pipeline-metrics.jsonl +1 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +5 -0
- package/apps/vibe/.crew/session.json +10 -1
- package/apps/vibe/.studio-data/project-messages/general.jsonl +3 -0
- package/apps/vibe/index.html +4 -2
- package/apps/vibe/server.mjs +75 -3
- package/apps/vibe/src/main.js +126 -53
- package/crew-lead.mjs +14 -1
- package/lib/bridges/cli-executor.mjs +0 -2
- package/lib/bridges/tmux-bridge.mjs +200 -0
- package/lib/chat/unified-history.mjs +1 -1
- package/lib/cli-process-tracker.mjs +2 -1
- package/lib/crew-lead/http-server.mjs +286 -1
- package/lib/crew-lead/wave-dispatcher.mjs +40 -3
- package/lib/engines/crew-cli.mjs +3 -2
- package/lib/engines/llm-direct.mjs +4 -1
- package/lib/engines/rt-envelope.mjs +14 -5
- package/lib/engines/runners.mjs +30 -4
- package/lib/runtime/config.mjs +7 -0
- package/lib/sessions/session-manager.mjs +287 -0
- package/package.json +1 -1
- package/scripts/bench/performance_optimization.py +81 -0
- package/whatsapp-bridge.mjs +54 -10
- package/apps/dashboard/dist/assets/core-utils-CAVnDoe1.js +0 -1
package/lib/engines/runners.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { initEngineRegistry, selectEngine as registrySelectEngine, getEngineById
|
|
|
21
21
|
import { runCrewCLITask } from "./crew-cli.mjs";
|
|
22
22
|
import { normalizeProjectDir } from "../runtime/project-dir.mjs";
|
|
23
23
|
import { resolveCursorLaunchSpec } from "./cursor-launcher.mjs";
|
|
24
|
+
import * as tmuxBridge from "../bridges/tmux-bridge.mjs";
|
|
24
25
|
|
|
25
26
|
function which(bin) {
|
|
26
27
|
try { execSync(`which ${bin}`, { stdio: "ignore" }); return true; } catch { return false; }
|
|
@@ -357,6 +358,11 @@ export async function runGeminiCliTask(prompt, payload = {}) {
|
|
|
357
358
|
stdio: ["ignore", "pipe", "pipe"],
|
|
358
359
|
});
|
|
359
360
|
|
|
361
|
+
// Label tmux pane with agent ID for cross-agent discovery
|
|
362
|
+
if (payload?.tmuxSessionId && tmuxBridge.detect()) {
|
|
363
|
+
try { tmuxBridge.label(agentId, payload.tmuxSessionId); } catch {}
|
|
364
|
+
}
|
|
365
|
+
|
|
360
366
|
let lineBuffer = "";
|
|
361
367
|
let accumulatedText = "";
|
|
362
368
|
let orphanStream = "";
|
|
@@ -683,6 +689,11 @@ export async function runCursorCliTask(prompt, payload = {}) {
|
|
|
683
689
|
stdio: ["ignore", "pipe", "pipe"],
|
|
684
690
|
});
|
|
685
691
|
|
|
692
|
+
// Label tmux pane with agent ID for cross-agent discovery
|
|
693
|
+
if (payload?.tmuxSessionId && tmuxBridge.detect()) {
|
|
694
|
+
try { tmuxBridge.label(agentId, payload.tmuxSessionId); } catch {}
|
|
695
|
+
}
|
|
696
|
+
|
|
686
697
|
let lineBuffer = "";
|
|
687
698
|
let accumulatedText = "";
|
|
688
699
|
let lastCursorAssistantNorm = "";
|
|
@@ -915,6 +926,11 @@ export async function runCodexTask(prompt, payload = {}) {
|
|
|
915
926
|
stdio: ["ignore", "pipe", "pipe"],
|
|
916
927
|
});
|
|
917
928
|
|
|
929
|
+
// Label tmux pane with agent ID for cross-agent discovery
|
|
930
|
+
if (payload?.tmuxSessionId && tmuxBridge.detect()) {
|
|
931
|
+
try { tmuxBridge.label(agentId, payload.tmuxSessionId); } catch {}
|
|
932
|
+
}
|
|
933
|
+
|
|
918
934
|
let lineBuffer = "";
|
|
919
935
|
let accumulatedText = "";
|
|
920
936
|
/** Non-JSON lines (stderr, errors, usage) — previously swallowed by catch {} */
|
|
@@ -1178,10 +1194,15 @@ export async function runClaudeCodeTask(prompt, payload = {}) {
|
|
|
1178
1194
|
args.push("--model", model);
|
|
1179
1195
|
}
|
|
1180
1196
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1197
|
+
// Only resume session if explicitly requested (e.g. conversational follow-up).
|
|
1198
|
+
// Pipeline/dispatch tasks should start fresh to avoid prior context poisoning simple tasks.
|
|
1199
|
+
const shouldResume = payload?.resumeSession === true;
|
|
1200
|
+
if (shouldResume) {
|
|
1201
|
+
const existingSession = readClaudeSessionId(agentId);
|
|
1202
|
+
if (existingSession) {
|
|
1203
|
+
args.push("--resume", existingSession);
|
|
1204
|
+
console.error(`[ClaudeCode:${agentId}] Resuming session ${existingSession}`);
|
|
1205
|
+
}
|
|
1185
1206
|
}
|
|
1186
1207
|
|
|
1187
1208
|
// CRITICAL: Claude Code expects the prompt as a command-line argument, NOT via stdin
|
|
@@ -1202,6 +1223,11 @@ export async function runClaudeCodeTask(prompt, payload = {}) {
|
|
|
1202
1223
|
stdio: ["ignore", "pipe", "pipe"], // Changed from "pipe" to "ignore" for stdin since we use args
|
|
1203
1224
|
});
|
|
1204
1225
|
|
|
1226
|
+
// Label tmux pane with agent ID for cross-agent discovery
|
|
1227
|
+
if (payload?.tmuxSessionId && tmuxBridge.detect()) {
|
|
1228
|
+
try { tmuxBridge.label(agentId, payload.tmuxSessionId); } catch {}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1205
1231
|
let lineBuffer = "";
|
|
1206
1232
|
let accumulatedText = "";
|
|
1207
1233
|
let stderrText = "";
|
package/lib/runtime/config.mjs
CHANGED
|
@@ -290,6 +290,13 @@ export function loadClaudeCodeEnabled() {
|
|
|
290
290
|
if (typeof cfg.claudeCode === "boolean") return cfg.claudeCode;
|
|
291
291
|
return false;
|
|
292
292
|
}
|
|
293
|
+
|
|
294
|
+
export function loadTmuxBridgeEnabled() {
|
|
295
|
+
if (process.env.CREWSWARM_TMUX_BRIDGE) return /^1|true|yes$/i.test(String(process.env.CREWSWARM_TMUX_BRIDGE));
|
|
296
|
+
const cfg = loadSystemConfig();
|
|
297
|
+
if (typeof cfg.tmuxBridge === "boolean") return cfg.tmuxBridge;
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
293
300
|
// ── Configuration Parsers (Migrated from registry.mjs) ───────────────────
|
|
294
301
|
export function resolveConfig() {
|
|
295
302
|
const paths = [CREWSWARM_CONFIG_PATH, path.join(LEGACY_STATE_DIR, "openclaw.json")];
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager — persistent tmux sessions as first-class execution resources
|
|
3
|
+
*
|
|
4
|
+
* Manages session lifecycle: create, attach, exec, lock, handoff, terminate.
|
|
5
|
+
* One writer per session (lock enforcement). Transcripts logged for auditability.
|
|
6
|
+
*
|
|
7
|
+
* Sessions are stored as metadata files under ~/.crewswarm/state/sessions/.
|
|
8
|
+
* The actual tmux sessions are managed via tmux CLI.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { getStatePath } from "../runtime/paths.mjs";
|
|
16
|
+
import * as tmuxBridge from "../bridges/tmux-bridge.mjs";
|
|
17
|
+
|
|
18
|
+
const SESSION_DIR = getStatePath("sessions");
|
|
19
|
+
const TRANSCRIPT_DIR = getStatePath("sessions", "transcripts");
|
|
20
|
+
|
|
21
|
+
try { fs.mkdirSync(SESSION_DIR, { recursive: true }); } catch {}
|
|
22
|
+
try { fs.mkdirSync(TRANSCRIPT_DIR, { recursive: true }); } catch {}
|
|
23
|
+
|
|
24
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
function sessionMetaPath(sessionId) {
|
|
27
|
+
return path.join(SESSION_DIR, `${sessionId}.json`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function transcriptPath(sessionId) {
|
|
31
|
+
return path.join(TRANSCRIPT_DIR, `${sessionId}.jsonl`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadMeta(sessionId) {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(fs.readFileSync(sessionMetaPath(sessionId), "utf8"));
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function saveMeta(sessionId, meta) {
|
|
43
|
+
try {
|
|
44
|
+
fs.writeFileSync(sessionMetaPath(sessionId), JSON.stringify(meta, null, 2));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error(`[session-manager] Failed to save meta for ${sessionId}: ${e.message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function appendTranscript(sessionId, entry) {
|
|
51
|
+
try {
|
|
52
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), ...entry });
|
|
53
|
+
fs.appendFileSync(transcriptPath(sessionId), line + "\n");
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function tmuxExec(cmd, timeout = 5000) {
|
|
58
|
+
try {
|
|
59
|
+
return execSync(cmd, { encoding: "utf8", timeout, stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create a new persistent tmux session for agent work.
|
|
69
|
+
* @param {object} opts
|
|
70
|
+
* @param {string} opts.workspaceId - Logical workspace name
|
|
71
|
+
* @param {string} opts.agentId - Owning agent
|
|
72
|
+
* @param {string} [opts.cwd] - Working directory
|
|
73
|
+
* @param {Record<string, string>} [opts.env] - Extra env vars
|
|
74
|
+
* @returns {string|null} sessionId or null on failure
|
|
75
|
+
*/
|
|
76
|
+
export function create({ workspaceId, agentId, cwd, env } = {}) {
|
|
77
|
+
if (!tmuxBridge.detect()) return null;
|
|
78
|
+
|
|
79
|
+
const sessionId = `cs-${workspaceId}-${randomUUID().slice(0, 8)}`;
|
|
80
|
+
const sessionName = sessionId;
|
|
81
|
+
|
|
82
|
+
// Create a new tmux session (detached)
|
|
83
|
+
const envStr = env
|
|
84
|
+
? Object.entries(env).map(([k, v]) => `-e ${k}=${v}`).join(" ")
|
|
85
|
+
: "";
|
|
86
|
+
const cwdFlag = cwd ? `-c "${cwd}"` : "";
|
|
87
|
+
const result = tmuxExec(`tmux new-session -d -s "${sessionName}" ${cwdFlag} ${envStr}`);
|
|
88
|
+
if (result === null) {
|
|
89
|
+
console.error(`[session-manager] Failed to create tmux session: ${sessionName}`);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Label the session's first pane with the agent ID
|
|
94
|
+
const paneId = tmuxExec(`tmux list-panes -t "${sessionName}" -F "#{pane_id}" | head -1`);
|
|
95
|
+
if (paneId) {
|
|
96
|
+
tmuxBridge.label(agentId, paneId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const meta = {
|
|
100
|
+
sessionId,
|
|
101
|
+
sessionName,
|
|
102
|
+
workspaceId,
|
|
103
|
+
owner: agentId,
|
|
104
|
+
lockedBy: agentId,
|
|
105
|
+
paneId: paneId || null,
|
|
106
|
+
cwd: cwd || null,
|
|
107
|
+
env: env || null,
|
|
108
|
+
createdAt: new Date().toISOString(),
|
|
109
|
+
status: "active",
|
|
110
|
+
};
|
|
111
|
+
saveMeta(sessionId, meta);
|
|
112
|
+
appendTranscript(sessionId, { action: "created", agent: agentId, cwd });
|
|
113
|
+
|
|
114
|
+
console.log(`[session-manager] Created session ${sessionId} for ${agentId} (pane=${paneId})`);
|
|
115
|
+
return sessionId;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Attach an agent to an existing session (for handoff or observation).
|
|
120
|
+
* @param {string} sessionId
|
|
121
|
+
* @param {string} agentId
|
|
122
|
+
* @returns {{ paneId: string, sessionName: string }|null}
|
|
123
|
+
*/
|
|
124
|
+
export function attach(sessionId, agentId) {
|
|
125
|
+
const meta = loadMeta(sessionId);
|
|
126
|
+
if (!meta || meta.status !== "active") return null;
|
|
127
|
+
|
|
128
|
+
appendTranscript(sessionId, { action: "attached", agent: agentId });
|
|
129
|
+
console.log(`[session-manager] ${agentId} attached to session ${sessionId}`);
|
|
130
|
+
|
|
131
|
+
return { paneId: meta.paneId, sessionName: meta.sessionName };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Execute a command in a session's tmux pane.
|
|
136
|
+
* Only the lock owner can execute.
|
|
137
|
+
* @param {string} sessionId
|
|
138
|
+
* @param {string} command
|
|
139
|
+
* @param {object} [opts]
|
|
140
|
+
* @param {string} opts.actorId - Agent executing the command
|
|
141
|
+
* @param {number} [opts.timeout=30000] - Timeout in ms
|
|
142
|
+
* @returns {{ output: string }|null}
|
|
143
|
+
*/
|
|
144
|
+
export function exec(sessionId, command, { actorId, timeout = 30000 } = {}) {
|
|
145
|
+
const meta = loadMeta(sessionId);
|
|
146
|
+
if (!meta || meta.status !== "active") return null;
|
|
147
|
+
|
|
148
|
+
// Enforce lock
|
|
149
|
+
if (meta.lockedBy && meta.lockedBy !== actorId) {
|
|
150
|
+
console.warn(`[session-manager] ${actorId} cannot exec in ${sessionId} — locked by ${meta.lockedBy}`);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const paneId = meta.paneId;
|
|
155
|
+
if (!paneId) return null;
|
|
156
|
+
|
|
157
|
+
// Send keys to the pane
|
|
158
|
+
tmuxExec(`tmux send-keys -t "${paneId}" "${command.replace(/"/g, '\\"')}" Enter`, timeout);
|
|
159
|
+
appendTranscript(sessionId, { action: "exec", agent: actorId, command: command.slice(0, 500) });
|
|
160
|
+
|
|
161
|
+
// Read back output after a short delay
|
|
162
|
+
const output = tmuxBridge.read(meta.owner, 50);
|
|
163
|
+
return { output: output || "" };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Lock a session for exclusive write access.
|
|
168
|
+
* @param {string} sessionId
|
|
169
|
+
* @param {string} ownerId - Agent requesting the lock
|
|
170
|
+
* @returns {boolean} true if lock acquired
|
|
171
|
+
*/
|
|
172
|
+
export function lock(sessionId, ownerId) {
|
|
173
|
+
const meta = loadMeta(sessionId);
|
|
174
|
+
if (!meta || meta.status !== "active") return false;
|
|
175
|
+
|
|
176
|
+
if (meta.lockedBy && meta.lockedBy !== ownerId) {
|
|
177
|
+
console.warn(`[session-manager] Lock denied for ${ownerId} on ${sessionId} — held by ${meta.lockedBy}`);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
meta.lockedBy = ownerId;
|
|
182
|
+
meta.lockedAt = new Date().toISOString();
|
|
183
|
+
saveMeta(sessionId, meta);
|
|
184
|
+
appendTranscript(sessionId, { action: "locked", agent: ownerId });
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Unlock a session.
|
|
190
|
+
* @param {string} sessionId
|
|
191
|
+
* @param {string} ownerId - Must match current lock holder
|
|
192
|
+
* @returns {boolean}
|
|
193
|
+
*/
|
|
194
|
+
export function unlock(sessionId, ownerId) {
|
|
195
|
+
const meta = loadMeta(sessionId);
|
|
196
|
+
if (!meta) return false;
|
|
197
|
+
|
|
198
|
+
if (meta.lockedBy && meta.lockedBy !== ownerId) {
|
|
199
|
+
console.warn(`[session-manager] Unlock denied for ${ownerId} on ${sessionId} — held by ${meta.lockedBy}`);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
meta.lockedBy = null;
|
|
204
|
+
meta.lockedAt = null;
|
|
205
|
+
saveMeta(sessionId, meta);
|
|
206
|
+
appendTranscript(sessionId, { action: "unlocked", agent: ownerId });
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Hand off a session from one agent to another.
|
|
212
|
+
* Transfers lock ownership and re-labels the pane.
|
|
213
|
+
* @param {string} sessionId
|
|
214
|
+
* @param {string} fromAgent
|
|
215
|
+
* @param {string} toAgent
|
|
216
|
+
* @returns {boolean}
|
|
217
|
+
*/
|
|
218
|
+
export function handoff(sessionId, fromAgent, toAgent) {
|
|
219
|
+
const meta = loadMeta(sessionId);
|
|
220
|
+
if (!meta || meta.status !== "active") return false;
|
|
221
|
+
|
|
222
|
+
// Only the current lock holder (or unlocked session) can hand off
|
|
223
|
+
if (meta.lockedBy && meta.lockedBy !== fromAgent) {
|
|
224
|
+
console.warn(`[session-manager] Handoff denied: ${sessionId} locked by ${meta.lockedBy}, not ${fromAgent}`);
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
meta.owner = toAgent;
|
|
229
|
+
meta.lockedBy = toAgent;
|
|
230
|
+
meta.lockedAt = new Date().toISOString();
|
|
231
|
+
saveMeta(sessionId, meta);
|
|
232
|
+
|
|
233
|
+
// Re-label pane for the new agent
|
|
234
|
+
if (meta.paneId) {
|
|
235
|
+
tmuxBridge.label(toAgent, meta.paneId);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
appendTranscript(sessionId, { action: "handoff", from: fromAgent, to: toAgent });
|
|
239
|
+
console.log(`[session-manager] Session ${sessionId} handed off: ${fromAgent} → ${toAgent}`);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Terminate a session and clean up its tmux pane.
|
|
245
|
+
* @param {string} sessionId
|
|
246
|
+
* @returns {boolean}
|
|
247
|
+
*/
|
|
248
|
+
export function terminate(sessionId) {
|
|
249
|
+
const meta = loadMeta(sessionId);
|
|
250
|
+
if (!meta) return false;
|
|
251
|
+
|
|
252
|
+
// Kill the tmux session
|
|
253
|
+
if (meta.sessionName) {
|
|
254
|
+
tmuxExec(`tmux kill-session -t "${meta.sessionName}"`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
meta.status = "terminated";
|
|
258
|
+
meta.terminatedAt = new Date().toISOString();
|
|
259
|
+
saveMeta(sessionId, meta);
|
|
260
|
+
appendTranscript(sessionId, { action: "terminated" });
|
|
261
|
+
console.log(`[session-manager] Session ${sessionId} terminated`);
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get metadata for a session.
|
|
267
|
+
* @param {string} sessionId
|
|
268
|
+
* @returns {object|null}
|
|
269
|
+
*/
|
|
270
|
+
export function getSession(sessionId) {
|
|
271
|
+
return loadMeta(sessionId);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* List all active sessions.
|
|
276
|
+
* @returns {Array<object>}
|
|
277
|
+
*/
|
|
278
|
+
export function listSessions() {
|
|
279
|
+
try {
|
|
280
|
+
const files = fs.readdirSync(SESSION_DIR).filter(f => f.endsWith(".json"));
|
|
281
|
+
return files
|
|
282
|
+
.map(f => loadMeta(f.replace(".json", "")))
|
|
283
|
+
.filter(m => m && m.status === "active");
|
|
284
|
+
} catch {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Minimal performance benchmark script (synthetic fallback mode).
|
|
4
|
+
|
|
5
|
+
When --force-synthetic is passed, skips real HTTP requests and emits
|
|
6
|
+
synthetic benchmark data. This allows the test suite to verify the
|
|
7
|
+
tooling pipeline without a running server.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python3 scripts/bench/performance_optimization.py \
|
|
11
|
+
--url http://127.0.0.1:4319/api/health \
|
|
12
|
+
--profile all \
|
|
13
|
+
--force-synthetic
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def synthetic_baseline(url):
|
|
22
|
+
return {
|
|
23
|
+
"url": url,
|
|
24
|
+
"mode": "synthetic-fallback",
|
|
25
|
+
"fallback_reason": "synthetic mode requested via --force-synthetic",
|
|
26
|
+
"latency_ms": 0.1,
|
|
27
|
+
"status": 200,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def synthetic_profile(name, url):
|
|
32
|
+
return {
|
|
33
|
+
"name": name,
|
|
34
|
+
"url": url,
|
|
35
|
+
"metrics": {
|
|
36
|
+
"mode": "synthetic-fallback",
|
|
37
|
+
"latency_p50_ms": 0.1,
|
|
38
|
+
"latency_p99_ms": 0.5,
|
|
39
|
+
"success_rate": 1.0,
|
|
40
|
+
"requests": 10,
|
|
41
|
+
},
|
|
42
|
+
"recommendations": [
|
|
43
|
+
f"Enable caching for {name} profile to reduce latency",
|
|
44
|
+
],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
PROFILE_NAMES = ["throughput", "latency", "reliability"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main():
|
|
52
|
+
parser = argparse.ArgumentParser(description="CrewSwarm performance benchmark")
|
|
53
|
+
parser.add_argument("--url", required=True, help="Target URL to benchmark")
|
|
54
|
+
parser.add_argument("--profile", default="all", help="Profile to run (or 'all')")
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--force-synthetic",
|
|
57
|
+
action="store_true",
|
|
58
|
+
help="Use synthetic data instead of real HTTP requests",
|
|
59
|
+
)
|
|
60
|
+
args = parser.parse_args()
|
|
61
|
+
|
|
62
|
+
if not args.force_synthetic:
|
|
63
|
+
print(
|
|
64
|
+
"Error: live benchmarking not implemented yet. Use --force-synthetic.",
|
|
65
|
+
file=sys.stderr,
|
|
66
|
+
)
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
profiles = PROFILE_NAMES if args.profile == "all" else [args.profile]
|
|
70
|
+
|
|
71
|
+
result = {
|
|
72
|
+
"baseline": synthetic_baseline(args.url),
|
|
73
|
+
"profiles": [synthetic_profile(p, args.url) for p in profiles],
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
json.dump(result, sys.stdout, indent=2)
|
|
77
|
+
sys.stdout.write("\n")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
main()
|
package/whatsapp-bridge.mjs
CHANGED
|
@@ -144,6 +144,19 @@ const ALLOWED_JIDS = new Set(
|
|
|
144
144
|
);
|
|
145
145
|
const ALLOWLIST_ENABLED = ALLOWED_JIDS.size > 0;
|
|
146
146
|
|
|
147
|
+
// Map @lid JIDs to their @s.whatsapp.net equivalents (populated on first message)
|
|
148
|
+
const LID_TO_JID = new Map();
|
|
149
|
+
|
|
150
|
+
function isJidAllowed(jid) {
|
|
151
|
+
if (!ALLOWLIST_ENABLED) return true;
|
|
152
|
+
if (ALLOWED_JIDS.has(jid)) return true;
|
|
153
|
+
// Check if this @lid JID is mapped to an allowed @s.whatsapp.net
|
|
154
|
+
if (jid.endsWith("@lid") && LID_TO_JID.has(jid)) {
|
|
155
|
+
return ALLOWED_JIDS.has(LID_TO_JID.get(jid));
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
147
160
|
// Per-user routing: maps "+1234..." or "1234...@s.whatsapp.net" → agent name
|
|
148
161
|
const USER_ROUTING = loadUserRouting();
|
|
149
162
|
|
|
@@ -424,7 +437,7 @@ function connectRT(sendToJid) {
|
|
|
424
437
|
if (sessionId && activeSessions.has(sessionId)) {
|
|
425
438
|
const jid = sessionId;
|
|
426
439
|
// Allowlist check on outbound — never send to unauthorized JIDs
|
|
427
|
-
if (ALLOWLIST_ENABLED && !
|
|
440
|
+
if (ALLOWLIST_ENABLED && !isJidAllowed(jid)) {
|
|
428
441
|
log("warn", "RT reply blocked by allowlist — not sending to unauthorized JID", { jid, from });
|
|
429
442
|
return;
|
|
430
443
|
}
|
|
@@ -524,7 +537,7 @@ async function listenForAgentReplies(sendToJid) {
|
|
|
524
537
|
|
|
525
538
|
const jid = whatsappJid;
|
|
526
539
|
// Allowlist check on outbound — never send to unauthorized JIDs
|
|
527
|
-
if (ALLOWLIST_ENABLED && !
|
|
540
|
+
if (ALLOWLIST_ENABLED && !isJidAllowed(jid)) {
|
|
528
541
|
log("warn", "SSE reply blocked by allowlist — not sending to unauthorized JID", { jid, from: d.from });
|
|
529
542
|
continue;
|
|
530
543
|
}
|
|
@@ -804,18 +817,49 @@ async function main() {
|
|
|
804
817
|
const isGroup = jid.endsWith("@g.us");
|
|
805
818
|
if (isGroup) continue;
|
|
806
819
|
|
|
807
|
-
// WhatsApp multi-device uses @lid (Linked Identity) JIDs
|
|
808
|
-
//
|
|
809
|
-
|
|
820
|
+
// WhatsApp multi-device uses @lid (Linked Identity) JIDs.
|
|
821
|
+
// CRITICAL: Not all fromMe + @lid messages are self-chat!
|
|
822
|
+
// When you message ANYONE from your phone, it arrives as fromMe:true with THEIR @lid.
|
|
823
|
+
// Self-chat is ONLY when the @lid matches the bot's own linked identity.
|
|
810
824
|
const ownJid = sock.user?.id?.split(":")[0] + "@s.whatsapp.net";
|
|
825
|
+
// Detect the bot's own @lid JID. Baileys may expose sock.user.lid,
|
|
826
|
+
// or we detect it from the first self-addressed message.
|
|
827
|
+
const ownLid = sock.user?.lid || sock._ownLid || null;
|
|
828
|
+
let isSelfChatLid = false;
|
|
829
|
+
if (msg.key.fromMe && jid.endsWith("@lid")) {
|
|
830
|
+
if (ownLid && jid === ownLid) {
|
|
831
|
+
// Known self-chat LID
|
|
832
|
+
isSelfChatLid = true;
|
|
833
|
+
} else if (!ownLid && msg.key.participant) {
|
|
834
|
+
// First @lid message — check if participant matches our own number
|
|
835
|
+
const participantNum = msg.key.participant?.split(":")[0]?.split("@")[0];
|
|
836
|
+
const ownNum = sock.user?.id?.split(":")[0];
|
|
837
|
+
if (participantNum === ownNum) {
|
|
838
|
+
isSelfChatLid = true;
|
|
839
|
+
sock._ownLid = jid; // Cache for future messages
|
|
840
|
+
log("info", "Detected own @lid JID", { lid: jid, ownNum });
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
// If we still can't determine, it's NOT self-chat (safe default)
|
|
844
|
+
}
|
|
811
845
|
const isSelfChatOwn = msg.key.fromMe && jid === ownJid;
|
|
846
|
+
const isSelfChat = isSelfChatLid || isSelfChatOwn;
|
|
812
847
|
|
|
813
|
-
// Block outgoing messages that aren't self-chat
|
|
814
|
-
|
|
848
|
+
// Block ALL outgoing messages that aren't self-chat.
|
|
849
|
+
// This prevents the bot from processing your messages to other people.
|
|
850
|
+
if (msg.key.fromMe && !isSelfChat) continue;
|
|
851
|
+
|
|
852
|
+
// For self-chat @lid messages, map to the owner's real @s.whatsapp.net JID
|
|
853
|
+
if (isSelfChatLid && ownJid) {
|
|
854
|
+
LID_TO_JID.set(jid, ownJid);
|
|
855
|
+
log("info", "Mapped self-chat @lid to @s.whatsapp.net", { lid: jid, jid: ownJid });
|
|
856
|
+
}
|
|
815
857
|
|
|
816
858
|
// ── Allowlist check (before any media processing) ─────────────────
|
|
817
|
-
|
|
818
|
-
|
|
859
|
+
// Self-chat always passes (it's you talking to yourself).
|
|
860
|
+
// Other messages: check if sender is in allowlist.
|
|
861
|
+
if (!isSelfChat) {
|
|
862
|
+
if (ALLOWLIST_ENABLED && !isJidAllowed(jid)) {
|
|
819
863
|
log("warn", "Silently ignored unauthorized sender", { jid });
|
|
820
864
|
continue;
|
|
821
865
|
}
|
|
@@ -1444,7 +1488,7 @@ async function main() {
|
|
|
1444
1488
|
}
|
|
1445
1489
|
if (!targetJid) { res.writeHead(400); res.end(JSON.stringify({ error: "jid or phone required" })); return; }
|
|
1446
1490
|
// Allowlist check on outbound — never send to unauthorized JIDs
|
|
1447
|
-
if (ALLOWLIST_ENABLED && !
|
|
1491
|
+
if (ALLOWLIST_ENABLED && !isJidAllowed(targetJid)) {
|
|
1448
1492
|
log("warn", "HTTP /send blocked by allowlist", { targetJid });
|
|
1449
1493
|
res.writeHead(403); res.end(JSON.stringify({ error: "JID not in allowlist" })); return;
|
|
1450
1494
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
async function e(e,{ttl:t=0,bust:r=!1}={}){const s=await fetch(e);if(!s.ok)throw new Error(await s.text());return await s.json()}async function t(e,t,r){const s=await fetch(e,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(t),signal:r}),n=await s.text();if(!s.ok)throw new Error(n.slice(0,120));try{return JSON.parse(n)}catch{throw new Error("Bad response: "+n.slice(0,80))}}function r(e,t){return"online"===e?'<span title="● online — heartbeat <90s" style="display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 5px var(--green);margin-right:4px;flex-shrink:0;"></span>':"stale"===e?'<span title="● stale — last seen >'+(t||"?")+'s ago" style="display:inline-block;width:7px;height:7px;border-radius:50%;background:#f59e0b;margin-right:4px;flex-shrink:0;"></span>':"offline"===e?'<span title="● offline — no heartbeat in 5min" style="display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--red-hi);margin-right:4px;flex-shrink:0;"></span>':'<span title="● unknown — never seen" style="display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--text-3);margin-right:4px;flex-shrink:0;"></span>'}function s(e,t){e&&(e.innerHTML='<div class="meta" style="padding:20px;">'+(t||"Loading…")+"</div>")}function n(e,t){e&&(e.innerHTML='<div class="meta" style="padding:20px;">'+(t||"No items found.")+"</div>")}function a(e,t){e&&(e.innerHTML='<div class="meta" style="padding:20px;color:var(--red-hi);">'+(t||"An error occurred.")+"</div>")}function i(e){return String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function o(e,t){const r=document.createElement("div");r.className="notification"+("error"===t||!0===t?" error":"warning"===t?" warning":""),r.setAttribute("role","alert"),r.setAttribute("aria-live","polite"),r.textContent=e,document.body.appendChild(r),setTimeout(()=>r.remove(),4500)}function c(e){try{return new Date(e).toLocaleTimeString()}catch{return String(e)}}function l(e){return e&&e.time&&e.time.created||""}function d(e,t,r,s,n,a,i,o){const c=document.getElementById("chatMessages");if(!c)return;const l="user"===e,d=o&&"object"==typeof o&&!0===o.force;if(!l&&!d){const e=c.lastElementChild;if(e&&e.children.length>=2){if(e.children[1].textContent.trim()===String(t).trim())return}}const p=document.createElement("div");p.style.cssText="display:flex;flex-direction:column;align-items:"+(l?"flex-end":"flex-start")+";gap:4px;";const u=document.createElement("div");u.style.cssText="font-size:11px;color:var(--text-3);padding:0 6px;display:flex;align-items:center;gap:6px;";const g=window._crewLeadInfo||{emoji:"🧠",name:"crew-lead"};if(i){let e="crew-lead";l?e="You":i.agentName?e=i.agentName:i.agent?e=i.agent:"cli"===i.source?e=i.engine||"cli":"sub-agent"===i.source?e="sub-agent":"agent"===i.source?e=i.targetAgent||"agent":"dashboard"===i.source&&(e="crew-lead");const t=!l&&i.engine&&i.engine!==e?` · ${i.engine}`:"";u.textContent=`${i.emoji||"🤖"} ${e}${t}`;const r=document.createElement("span");r.style.cssText="opacity:0.6;",r.textContent=i.timestamp?" · "+i.timestamp:"",u.appendChild(r)}else{const t=l?"You":"assistant"===e?g.emoji+" "+g.name:e;u.textContent=t}if(!l){const e=r||n;if(e){const t=document.createElement("span");r?(t.title="Primary failed ("+(s||"error")+") — running on fallback",t.style.cssText="font-size:10px;padding:1px 6px;border-radius:999px;background:rgba(245,158,11,0.15);color:#f59e0b;border:1px solid rgba(245,158,11,0.3);cursor:default;",t.textContent="⚡ fallback: "+r):(t.title="Primary model",t.style.cssText="font-size:10px;padding:1px 6px;border-radius:999px;background:rgba(52,211,153,0.1);color:#34d399;border:1px solid rgba(52,211,153,0.2);cursor:default;",t.textContent=e),u.appendChild(t)}if(a){const e={claude:"#e07a5f",codex:"#8338ec",cursor:"#3d405b",opencode:"#06d6a0",gemini:"#4285f4","docker-sandbox":"#0db7ed"},t={claude:"🤖 Claude Code",codex:"🟣 Codex",cursor:"🖱 Cursor",opencode:"⚡ OpenCode",gemini:"✨ Gemini","docker-sandbox":"🐳 Docker"},r=document.createElement("span");r.title="Executed by "+(t[a]||a),r.style.cssText="font-size:10px;padding:1px 6px;border-radius:999px;color:#fff;background:"+(e[a]||"var(--text-3)")+";cursor:default;",r.textContent=t[a]||a,u.appendChild(r)}}const h=document.createElement("div");h.style.cssText="max-width:80%;padding:10px 14px;border-radius:"+(l?"14px 14px 4px 14px":"14px 14px 14px 4px")+";background:"+(l?"var(--purple)":"var(--surface-2)")+";color:"+(l?"#fff":"var(--text-2)")+";font-size:14px;line-height:1.5;white-space:pre-wrap;word-break:break-word;border:1px solid var(--border);",h.textContent=t,p.appendChild(u),p.appendChild(h),c.appendChild(p),c.scrollTop=c.scrollHeight}const p="crewswarm_ui_state";const u=function(){try{const e=sessionStorage.getItem(p);return e?JSON.parse(e):{}}catch{return{}}}(),g={selected:u.selected||null,selectedEngine:u.selectedEngine||"opencode",agents:u.agents||[],chatActiveProjectId:u.chatActiveProjectId||"",swarmChatProjectId:u.swarmChatProjectId||"",projectsData:u.projectsData||{},activeTab:u.activeTab||"chat",scrollPositions:u.scrollPositions||{}};function h(){try{sessionStorage.setItem(p,JSON.stringify({selected:g.selected,selectedEngine:g.selectedEngine,chatActiveProjectId:g.chatActiveProjectId,swarmChatProjectId:g.swarmChatProjectId,projectsData:g.projectsData,activeTab:g.activeTab,scrollPositions:g.scrollPositions}))}catch{}}function f(e){const t=document.querySelector(".view.active");t&&(g.scrollPositions[e||g.activeTab]=t.scrollTop,h())}function x(e){const t=g.scrollPositions[e];null!=t&&requestAnimationFrame(()=>{const e=document.querySelector(".view.active");e&&(e.scrollTop=t)})}const m={"crew-lead":0,"crew-orchestrator":1,orchestrator:1,"crew-main":2,"crew-pm":3,"crew-architect":4,"crew-coder":5,"crew-coder-back":6,"crew-coder-front":7,"crew-frontend":8,"crew-ml":9,"crew-fixer":10,"crew-qa":11,"crew-security":12,"crew-researcher":13,"crew-copywriter":14,"crew-seo":15,"crew-github":16,"crew-db-migrator":17,"crew-telegram":18,"crew-mega":19};function w(e){return(e||[]).sort((e,t)=>(m[e.id]??50)-(m[t.id]??50))}const b=new class{constructor(){this.activeTasks=new Map,this.listeners=new Set}registerTask(e,t){this.activeTasks.set(e,{...t,startTime:Date.now(),status:"running"}),this.notifyListeners()}stopTask(e){const t=this.activeTasks.get(e);return!!t&&(t.controller&&t.controller.abort(),t.status="stopped",this.activeTasks.delete(e),this.notifyListeners(),!0)}completeTask(e){const t=this.activeTasks.get(e);t&&(t.status="completed",this.activeTasks.delete(e),this.notifyListeners())}failTask(e,t){const r=this.activeTasks.get(e);r&&(r.status="failed",r.error=t,this.activeTasks.delete(e),this.notifyListeners())}getActiveTasks(){return Array.from(this.activeTasks.entries()).map(([e,t])=>({id:e,...t}))}isAgentBusy(e){return Array.from(this.activeTasks.values()).some(t=>t.agent===e&&"running"===t.status)}stopAll(){for(const[e]of this.activeTasks)this.stopTask(e)}stopAgent(e){for(const[t,r]of this.activeTasks)r.agent===e&&this.stopTask(t)}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}notifyListeners(){for(const t of this.listeners)try{t(this.getActiveTasks())}catch(e){console.error("TaskManager listener error:",e)}}};export{g as a,h as b,n as c,a as d,i as e,w as f,e as g,x as h,c as i,l as j,s as k,f as l,d as m,t as p,r,o as s,b as t};
|