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.
Files changed (84) hide show
  1. package/README.md +2 -2
  2. package/apps/dashboard/dist/assets/{chat-core-CMoqlR6D.js → chat-core-Cx4sTxDd.js} +1 -1
  3. package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
  4. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
  5. package/apps/dashboard/dist/assets/{components-CSUb80ze.js → components-BS9fQjE_.js} +1 -1
  6. package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
  7. package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js +1 -0
  8. package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
  9. package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
  10. package/apps/dashboard/dist/assets/{index-DqVVQLTW.js → index-DnClJ1ee.js} +2 -2
  11. package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
  12. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
  13. package/apps/dashboard/dist/assets/{setup-wizard-D4g5DMhW.js → setup-wizard-CA0Or47w.js} +1 -1
  14. package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
  15. package/apps/dashboard/dist/assets/{tab-agents-tab-BThdsdJY.js → tab-agents-tab-BgpIsjkw.js} +1 -1
  16. package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
  17. package/apps/dashboard/dist/assets/{tab-benchmarks-tab-DfCuAClu.js → tab-benchmarks-tab-BHjKCPm3.js} +1 -1
  18. package/apps/dashboard/dist/assets/{tab-comms-tab-eHpOSBhG.js → tab-comms-tab-kguqTIzD.js} +1 -1
  19. package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
  20. package/apps/dashboard/dist/assets/{tab-contacts-tab-5LHSthJM.js → tab-contacts-tab-DiOyMYth.js} +1 -1
  21. package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
  22. package/apps/dashboard/dist/assets/{tab-engines-tab-C3DYxTwy.js → tab-engines-tab-BsdZVvU0.js} +1 -1
  23. package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
  24. package/apps/dashboard/dist/assets/{tab-memory-tab-C59BYFQD.js → tab-memory-tab-Cu6u13EQ.js} +1 -1
  25. package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
  26. package/apps/dashboard/dist/assets/{tab-models-tab-CQzvaeVh.js → tab-models-tab-BLEjmd19.js} +1 -1
  27. package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
  28. package/apps/dashboard/dist/assets/{tab-pm-loop-tab-D7mnDelU.js → tab-pm-loop-tab-Bfd449B4.js} +1 -1
  29. package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
  30. package/apps/dashboard/dist/assets/{tab-projects-tab-C6h2Mv1K.js → tab-projects-tab-DhNWnlzt.js} +1 -1
  31. package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
  32. package/apps/dashboard/dist/assets/{tab-prompts-tab-C0wZvWK3.js → tab-prompts-tab-DVkUNaJd.js} +1 -1
  33. package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
  34. package/apps/dashboard/dist/assets/{tab-services-tab-DBj_w3bc.js → tab-services-tab-DU_LH3uG.js} +1 -1
  35. package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
  36. package/apps/dashboard/dist/assets/{tab-settings-tab-ezeqAjZk.js → tab-settings-tab-Bn4nXtDe.js} +1 -1
  37. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
  38. package/apps/dashboard/dist/assets/{tab-skills-tab-BYdU2whk.js → tab-skills-tab-BpY0uZHW.js} +1 -1
  39. package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
  40. package/apps/dashboard/dist/assets/{tab-spending-tab-Bg6w9t_p.js → tab-spending-tab-DEccQHnt.js} +1 -1
  41. package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
  42. package/apps/dashboard/dist/assets/{tab-swarm-chat-tab-BBV9HB2X.js → tab-swarm-chat-tab-BNrd88-r.js} +1 -1
  43. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
  44. package/apps/dashboard/dist/assets/{tab-swarm-tab-ChqLlEVs.js → tab-swarm-tab-B1AcjL1W.js} +1 -1
  45. package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
  46. package/apps/dashboard/dist/assets/{tab-usage-tab-B2UWXenJ.js → tab-usage-tab-BIOOnB-Y.js} +1 -1
  47. package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
  48. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
  49. package/apps/dashboard/dist/assets/{tab-workflows-tab-6QSXLJ0i.js → tab-workflows-tab-B-soSy1k.js} +1 -1
  50. package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
  51. package/apps/dashboard/dist/index.html +23 -23
  52. package/apps/dashboard/dist/index.html.br +0 -0
  53. package/apps/dashboard/dist/index.html.gz +0 -0
  54. package/apps/dashboard/index.html +71 -1
  55. package/apps/dashboard/src/app.js +5 -0
  56. package/apps/dashboard/src/core/dom.js +8 -0
  57. package/apps/dashboard/src/tabs/settings-tab.js +58 -0
  58. package/apps/vibe/.crew/agent-memory/pipeline.json +12 -1
  59. package/apps/vibe/.crew/cost.json +3 -3
  60. package/apps/vibe/.crew/json-parse-metrics.jsonl +1 -0
  61. package/apps/vibe/.crew/pipeline-metrics.jsonl +1 -0
  62. package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +5 -0
  63. package/apps/vibe/.crew/session.json +10 -1
  64. package/apps/vibe/.studio-data/project-messages/general.jsonl +3 -0
  65. package/apps/vibe/index.html +4 -2
  66. package/apps/vibe/server.mjs +75 -3
  67. package/apps/vibe/src/main.js +126 -53
  68. package/crew-lead.mjs +14 -1
  69. package/lib/bridges/cli-executor.mjs +0 -2
  70. package/lib/bridges/tmux-bridge.mjs +200 -0
  71. package/lib/chat/unified-history.mjs +1 -1
  72. package/lib/cli-process-tracker.mjs +2 -1
  73. package/lib/crew-lead/http-server.mjs +286 -1
  74. package/lib/crew-lead/wave-dispatcher.mjs +40 -3
  75. package/lib/engines/crew-cli.mjs +3 -2
  76. package/lib/engines/llm-direct.mjs +4 -1
  77. package/lib/engines/rt-envelope.mjs +14 -5
  78. package/lib/engines/runners.mjs +30 -4
  79. package/lib/runtime/config.mjs +7 -0
  80. package/lib/sessions/session-manager.mjs +287 -0
  81. package/package.json +1 -1
  82. package/scripts/bench/performance_optimization.py +81 -0
  83. package/whatsapp-bridge.mjs +54 -10
  84. package/apps/dashboard/dist/assets/core-utils-CAVnDoe1.js +0 -1
@@ -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
- const existingSession = readClaudeSessionId(agentId);
1182
- if (existingSession) {
1183
- args.push("--resume", existingSession);
1184
- console.error(`[ClaudeCode:${agentId}] Resuming session ${existingSession}`);
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 = "";
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crewswarm",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Local-first multi-agent orchestration platform — coordinate AI coding agents, LLMs, and tools from a single dashboard",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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()
@@ -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 && !ALLOWED_JIDS.has(jid)) {
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 && !ALLOWED_JIDS.has(jid)) {
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 for self-chat messages.
808
- // These arrive as fromMe:true with a @lid suffix this is the personal bot pattern.
809
- const isSelfChatLid = msg.key.fromMe && jid.endsWith("@lid");
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 (i.e. bot's own replies going out)
814
- if (msg.key.fromMe && !isSelfChatLid && !isSelfChatOwn) continue;
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
- if (!isSelfChatLid && !isSelfChatOwn) {
818
- if (ALLOWLIST_ENABLED && !ALLOWED_JIDS.has(jid)) {
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 && !ALLOWED_JIDS.has(targetJid)) {
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}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};