@desplega.ai/agent-swarm 1.10.3 → 1.10.8

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.
@@ -0,0 +1,76 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+ push:
8
+ branches:
9
+ - main
10
+ workflow_dispatch:
11
+
12
+ jobs:
13
+ lint-and-typecheck:
14
+ name: Lint and Type Check
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Setup Bun
22
+ uses: oven-sh/setup-bun@v2
23
+ with:
24
+ bun-version: latest
25
+
26
+ - name: Install dependencies
27
+ run: bun install --frozen-lockfile
28
+
29
+ - name: Run Biome linter
30
+ run: bun run lint
31
+
32
+ - name: Run TypeScript type check
33
+ run: bun run tsc:check
34
+
35
+ test:
36
+ name: Run Tests
37
+ runs-on: ubuntu-latest
38
+
39
+ steps:
40
+ - name: Checkout repository
41
+ uses: actions/checkout@v4
42
+
43
+ - name: Setup Bun
44
+ uses: oven-sh/setup-bun@v2
45
+ with:
46
+ bun-version: latest
47
+
48
+ - name: Install dependencies
49
+ run: bun install --frozen-lockfile
50
+
51
+ - name: Run tests
52
+ run: bun test
53
+
54
+ docker-build:
55
+ name: Docker Build Test
56
+ runs-on: ubuntu-latest
57
+ if: github.event_name == 'pull_request'
58
+
59
+ strategy:
60
+ matrix:
61
+ dockerfile: [Dockerfile, Dockerfile.worker]
62
+
63
+ steps:
64
+ - name: Checkout repository
65
+ uses: actions/checkout@v4
66
+
67
+ - name: Set up Docker Buildx
68
+ uses: docker/setup-buildx-action@v3
69
+
70
+ - name: Build Docker image
71
+ uses: docker/build-push-action@v5
72
+ with:
73
+ context: .
74
+ file: ./${{ matrix.dockerfile }}
75
+ push: false
76
+ tags: test-image:latest
@@ -1,4 +1,4 @@
1
- name: Docker Build and Publish
1
+ name: Docker Build + Publish + Deploy
2
2
 
3
3
  on:
4
4
  push:
@@ -50,6 +50,7 @@ jobs:
50
50
  push: true
51
51
  tags: ${{ steps.meta.outputs.tags }}
52
52
  labels: ${{ steps.meta.outputs.labels }}
53
+ no-cache: true
53
54
 
54
55
  build-and-push-worker:
55
56
  runs-on: ubuntu-latest
@@ -90,3 +91,27 @@ jobs:
90
91
  push: true
91
92
  tags: ${{ steps.meta.outputs.tags }}
92
93
  labels: ${{ steps.meta.outputs.labels }}
94
+ no-cache: true
95
+
96
+ # deploy:
97
+ # name: Deploy
98
+ # runs-on: ubuntu-latest
99
+ #
100
+ # needs:
101
+ # - build-and-push-server
102
+ # - build-and-push-worker
103
+ #
104
+ # steps:
105
+ # - name: Checkout
106
+ # id: checkout
107
+ # uses: actions/checkout@v4
108
+ #
109
+ # - name: Deploy Agent Swarm
110
+ # uses: tarasyarema/dokploy-deploy-action@main
111
+ # with:
112
+ # application_id: h4or-knfw2HWkTzjLmrn8
113
+ # application_name: agent-swarm-nrz8v0
114
+ # dokploy_url: https://app.dokploy.com
115
+ # auth_token: ${{ secrets.DOKPLOY_TOKEN }}
116
+ # wait_for_completion: true
117
+ # restart: true
package/Dockerfile CHANGED
@@ -35,10 +35,13 @@ RUN chmod +x /usr/local/bin/agent-swarm-api
35
35
  # Copy package.json for version info
36
36
  COPY package.json ./
37
37
 
38
- # Database will be created in /app (mount /app for persistence)
39
- VOLUME /app
38
+ # Create data directory for SQLite (WAL mode needs .sqlite, .sqlite-wal, .sqlite-shm on same filesystem)
39
+ RUN mkdir -p /app/data
40
40
 
41
41
  ENV PORT=3013
42
+ ENV DATABASE_PATH=/app/data/agent-swarm-db.sqlite
43
+
44
+ VOLUME /app/data
42
45
 
43
46
  EXPOSE 3013
44
47
 
@@ -49,6 +52,6 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
49
52
  # Print version on startup and run the server
50
53
  CMD echo "=== Agent Swarm API v$(cat /app/package.json | grep '\"version\"' | cut -d'"' -f4) ===" && \
51
54
  echo "Port: $PORT" && \
52
- echo "Database: /app/agent-swarm-db.sqlite" && \
55
+ echo "Database: $DATABASE_PATH" && \
53
56
  echo "==============================" && \
54
57
  exec /usr/local/bin/agent-swarm-api
package/README.md CHANGED
@@ -4,6 +4,14 @@
4
4
  <img src="assets/agent-swarm.png" alt="Agent Swarm" width="800">
5
5
  </p>
6
6
 
7
+ https://github.com/user-attachments/assets/bd308567-d21e-44a5-87ec-d25aeb1de3d3
8
+
9
+ <p align="center">
10
+ <a href="https://discord.gg/3XtmPdXm">
11
+ <img src="https://img.shields.io/badge/Discord-Join%20Community-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Join Discord">
12
+ </a>
13
+ </p>
14
+
7
15
  > Agent orchestration layer MCP for Claude Code, Codex, Gemini CLI, and more!
8
16
 
9
17
  ## Table of Contents
Binary file
package/biome.json CHANGED
@@ -17,7 +17,10 @@
17
17
  "linter": {
18
18
  "enabled": true,
19
19
  "rules": {
20
- "recommended": true
20
+ "recommended": true,
21
+ "style": {
22
+ "noNonNullAssertion": "off"
23
+ }
21
24
  }
22
25
  },
23
26
  "javascript": {
package/deploy/prod-db.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import { $ } from "bun";
4
4
  import * as readline from "node:readline";
5
5
 
6
- const DB_PATH = "/var/lib/docker/volumes/agent-swarm-nrz8v0_swarm_api/_data/agent-swarm-db.sqlite";
6
+ const DB_PATH = "/var/lib/docker/volumes/agent-swarm-nrz8v0_swarm_db/_data/agent-swarm-db.sqlite";
7
7
  const SSH_HOST = process.argv[2] || "swarm";
8
8
 
9
9
  console.log(`Connected to ${SSH_HOST}:${DB_PATH}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.10.3",
3
+ "version": "1.10.8",
4
4
  "description": "Agent orchestration layer MCP for Claude Code, Codex, Gemini CLI, and more!",
5
5
  "license": "MIT",
6
6
  "author": "desplega.ai <contact@desplega.ai>",
@@ -61,7 +61,7 @@
61
61
  "docs:mcp": "bun scripts/generate-mcp-docs.ts"
62
62
  },
63
63
  "devDependencies": {
64
- "@biomejs/biome": "^2.3.9",
64
+ "@biomejs/biome": "^2.3.10",
65
65
  "@types/bun": "latest"
66
66
  },
67
67
  "peerDependencies": {
package/pyproject.toml ADDED
@@ -0,0 +1,9 @@
1
+ # Minimal pyproject.toml to satisfy GitHub Actions setup-python cache requirement
2
+ # This file is a workaround for: https://github.com/actions/setup-python/issues/XXX
3
+ # The dokploy-deploy-action uses setup-python with pip caching, which requires
4
+ # a pyproject.toml or requirements.txt even though this is a Node.js/TypeScript project.
5
+
6
+ [project]
7
+ name = "agent-swarm-deploy-workaround"
8
+ version = "0.0.0"
9
+ description = "Workaround file for GitHub Actions deployment"
package/src/be/db.ts CHANGED
@@ -24,6 +24,8 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
24
24
  }
25
25
 
26
26
  db = new Database(dbPath, { create: true });
27
+ console.log(`Database initialized at ${dbPath}`);
28
+
27
29
  db.run("PRAGMA journal_mode = WAL;");
28
30
  db.run("PRAGMA foreign_keys = ON;");
29
31
 
@@ -333,9 +335,9 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
333
335
  return db;
334
336
  }
335
337
 
336
- export function getDb(): Database {
338
+ export function getDb(path?: string): Database {
337
339
  if (!db) {
338
- return initDb();
340
+ return initDb(path ?? process.env.DATABASE_PATH);
339
341
  }
340
342
  return db;
341
343
  }
@@ -728,6 +730,34 @@ export function getCompletedSlackTasks(): AgentTask[] {
728
730
  .map(rowToAgentTask);
729
731
  }
730
732
 
733
+ /**
734
+ * Get tasks that were recently finished (completed/failed) by workers (non-lead agents).
735
+ * Used by leads to know when workers complete tasks.
736
+ */
737
+ export function getRecentlyFinishedWorkerTasks(since?: string): AgentTask[] {
738
+ const query = since
739
+ ? `SELECT t.* FROM agent_tasks t
740
+ LEFT JOIN agents a ON t.agentId = a.id
741
+ WHERE t.status IN ('completed', 'failed')
742
+ AND t.finishedAt > ?
743
+ AND (a.isLead = 0 OR a.isLead IS NULL)
744
+ ORDER BY t.finishedAt DESC
745
+ LIMIT 50`
746
+ : `SELECT t.* FROM agent_tasks t
747
+ LEFT JOIN agents a ON t.agentId = a.id
748
+ WHERE t.status IN ('completed', 'failed')
749
+ AND t.finishedAt IS NOT NULL
750
+ AND (a.isLead = 0 OR a.isLead IS NULL)
751
+ ORDER BY t.finishedAt DESC
752
+ LIMIT 10`;
753
+
754
+ if (since) {
755
+ return getDb().prepare<AgentTaskRow, [string]>(query).all(since).map(rowToAgentTask);
756
+ }
757
+
758
+ return getDb().prepare<AgentTaskRow, []>(query).all().map(rowToAgentTask);
759
+ }
760
+
731
761
  export function getInProgressSlackTasks(): AgentTask[] {
732
762
  return getDb()
733
763
  .prepare<AgentTaskRow, []>(
package/src/claude.ts CHANGED
@@ -7,23 +7,23 @@ type RunCladeOptions = {
7
7
  };
8
8
 
9
9
  export const runClaude = async (opts: RunCladeOptions) => {
10
- const CMD = ["claude"];
10
+ const Cmd = ["claude"];
11
11
 
12
12
  if (opts.headless) {
13
- CMD.push("--verbose");
14
- CMD.push("--output-format");
15
- CMD.push("stream-json");
16
- CMD.push("-p");
17
- CMD.push(opts.msg);
13
+ Cmd.push("--verbose");
14
+ Cmd.push("--output-format");
15
+ Cmd.push("stream-json");
16
+ Cmd.push("-p");
17
+ Cmd.push(opts.msg);
18
18
  }
19
19
 
20
20
  if (opts.additionalArgs && opts.additionalArgs.length > 0) {
21
- CMD.push(...opts.additionalArgs);
21
+ Cmd.push(...opts.additionalArgs);
22
22
  }
23
23
 
24
24
  console.debug(`[debug] Running in ${opts.headless ? "headless" : "normal"} mode`);
25
25
 
26
- const proc = Bun.spawn(CMD, {
26
+ const proc = Bun.spawn(Cmd, {
27
27
  env: process.env,
28
28
  stdout: opts.headless ? "pipe" : "inherit",
29
29
  stdin: opts.headless ? undefined : "inherit",
@@ -13,10 +13,60 @@ async function savePm2State(role: string): Promise<void> {
13
13
  }
14
14
  }
15
15
 
16
+ /** API configuration for ping/close */
17
+ interface ApiConfig {
18
+ apiUrl: string;
19
+ apiKey: string;
20
+ agentId: string;
21
+ }
22
+
23
+ /** Ping the server to indicate activity and update status */
24
+ async function pingServer(config: ApiConfig, _role: string): Promise<void> {
25
+ const headers: Record<string, string> = {
26
+ "X-Agent-ID": config.agentId,
27
+ };
28
+ if (config.apiKey) {
29
+ headers.Authorization = `Bearer ${config.apiKey}`;
30
+ }
31
+
32
+ try {
33
+ await fetch(`${config.apiUrl}/ping`, {
34
+ method: "POST",
35
+ headers,
36
+ });
37
+ } catch {
38
+ // Silently fail - server might not be running
39
+ }
40
+ }
41
+
42
+ /** Mark agent as offline on shutdown */
43
+ async function closeAgent(config: ApiConfig, role: string): Promise<void> {
44
+ const headers: Record<string, string> = {
45
+ "X-Agent-ID": config.agentId,
46
+ };
47
+ if (config.apiKey) {
48
+ headers.Authorization = `Bearer ${config.apiKey}`;
49
+ }
50
+
51
+ try {
52
+ console.log(`[${role}] Marking agent as offline...`);
53
+ await fetch(`${config.apiUrl}/close`, {
54
+ method: "POST",
55
+ headers,
56
+ });
57
+ console.log(`[${role}] Agent marked as offline`);
58
+ } catch {
59
+ // Silently fail - server might not be running
60
+ }
61
+ }
62
+
16
63
  /** Setup signal handlers for graceful shutdown */
17
- function setupShutdownHandlers(role: string): void {
64
+ function setupShutdownHandlers(role: string, apiConfig?: ApiConfig): void {
18
65
  const shutdown = async (signal: string) => {
19
66
  console.log(`\n[${role}] Received ${signal}, shutting down...`);
67
+ if (apiConfig) {
68
+ await closeAgent(apiConfig, role);
69
+ }
20
70
  await savePm2State(role);
21
71
  process.exit(0);
22
72
  };
@@ -122,11 +172,22 @@ async function flushLogBuffer(
122
172
 
123
173
  /** Trigger types returned by the poll API */
124
174
  interface Trigger {
125
- type: "task_assigned" | "task_offered" | "unread_mentions" | "pool_tasks_available";
175
+ type:
176
+ | "task_assigned"
177
+ | "task_offered"
178
+ | "unread_mentions"
179
+ | "pool_tasks_available"
180
+ | "tasks_finished";
126
181
  taskId?: string;
127
182
  task?: unknown;
128
183
  mentionsCount?: number;
129
184
  count?: number;
185
+ tasks?: Array<{
186
+ id: string;
187
+ agentId?: string;
188
+ task: string;
189
+ status: string;
190
+ }>;
130
191
  }
131
192
 
132
193
  /** Options for polling */
@@ -136,6 +197,7 @@ interface PollOptions {
136
197
  agentId: string;
137
198
  pollInterval: number;
138
199
  pollTimeout: number;
200
+ since?: string; // Optional: for filtering finished tasks
139
201
  }
140
202
 
141
203
  /** Register agent via HTTP API */
@@ -183,7 +245,13 @@ async function pollForTrigger(opts: PollOptions): Promise<Trigger | null> {
183
245
 
184
246
  while (Date.now() - startTime < opts.pollTimeout) {
185
247
  try {
186
- const response = await fetch(`${opts.apiUrl}/api/poll`, {
248
+ // Build URL with optional since parameter
249
+ let url = `${opts.apiUrl}/api/poll`;
250
+ if (opts.since) {
251
+ url += `?since=${encodeURIComponent(opts.since)}`;
252
+ }
253
+
254
+ const response = await fetch(url, {
187
255
  method: "GET",
188
256
  headers,
189
257
  });
@@ -224,8 +292,23 @@ function buildPromptForTrigger(trigger: Trigger, defaultPrompt: string): string
224
292
  return "/swarm-chat";
225
293
 
226
294
  case "pool_tasks_available":
227
- // Let lead review and assign tasks
228
- return defaultPrompt;
295
+ // Worker: claim a task from the pool
296
+ // Include the count so worker knows there are tasks available
297
+ return `There are ${trigger.count} unassigned task(s) available in the pool. Use get-tasks with unassigned: true to see them, then use task-action with action: "claim" to claim one. The claim is first-come-first-serve, so if your claim fails, try another task.`;
298
+
299
+ case "tasks_finished":
300
+ // Lead: simple notification about finished tasks
301
+ if (trigger.tasks && Array.isArray(trigger.tasks) && trigger.tasks.length > 0) {
302
+ const taskSummaries = trigger.tasks
303
+ .map((t) => {
304
+ const status = t.status === "completed" ? "completed" : "failed";
305
+ const agentName = t.agentId ? `Agent ${t.agentId.slice(0, 8)}` : "Unknown agent";
306
+ return `- ${agentName} ${status} task "${t.task?.slice(0, 50)}..." (ID: ${t.id})`;
307
+ })
308
+ .join("\n");
309
+ return `Workers have finished ${trigger.count} task(s):\n${taskSummaries}\n\nReview these results and decide if any follow-up actions are needed.`;
310
+ }
311
+ return `Workers have finished ${trigger.count} task(s). Use get-tasks with status "completed" or "failed" to review them.`;
229
312
 
230
313
  default:
231
314
  return defaultPrompt;
@@ -234,7 +317,7 @@ function buildPromptForTrigger(trigger: Trigger, defaultPrompt: string): string
234
317
 
235
318
  async function runClaudeIteration(opts: RunClaudeIterationOptions): Promise<number> {
236
319
  const { role } = opts;
237
- const CMD = [
320
+ const Cmd = [
238
321
  "claude",
239
322
  "--verbose",
240
323
  "--output-format",
@@ -248,11 +331,11 @@ async function runClaudeIteration(opts: RunClaudeIterationOptions): Promise<numb
248
331
  ];
249
332
 
250
333
  if (opts.additionalArgs && opts.additionalArgs.length > 0) {
251
- CMD.push(...opts.additionalArgs);
334
+ Cmd.push(...opts.additionalArgs);
252
335
  }
253
336
 
254
337
  if (opts.systemPrompt) {
255
- CMD.push("--append-system-prompt", opts.systemPrompt);
338
+ Cmd.push("--append-system-prompt", opts.systemPrompt);
256
339
  }
257
340
 
258
341
  console.log(`\x1b[2m[${role}]\x1b[0m \x1b[36m▸\x1b[0m Starting Claude (PID will follow)`);
@@ -260,7 +343,7 @@ async function runClaudeIteration(opts: RunClaudeIterationOptions): Promise<numb
260
343
  const logFileHandle = Bun.file(opts.logFile).writer();
261
344
  let stderrOutput = "";
262
345
 
263
- const proc = Bun.spawn(CMD, {
346
+ const proc = Bun.spawn(Cmd, {
264
347
  env: process.env,
265
348
  stdout: "pipe",
266
349
  stderr: "pipe",
@@ -355,9 +438,6 @@ async function runClaudeIteration(opts: RunClaudeIterationOptions): Promise<numb
355
438
  export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
356
439
  const { role, defaultPrompt, metadataType } = config;
357
440
 
358
- // Setup graceful shutdown handlers (saves PM2 state on Ctrl+C)
359
- setupShutdownHandlers(role);
360
-
361
441
  const sessionId = process.env.SESSION_ID || crypto.randomUUID().slice(0, 8);
362
442
  const baseLogDir = opts.logsDir || process.env.LOG_DIR || "/logs";
363
443
  const logDir = `${baseLogDir}/${sessionId}`;
@@ -430,8 +510,8 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
430
510
  const apiKey = process.env.API_KEY || "";
431
511
 
432
512
  // Constants for polling
433
- const POLL_INTERVAL_MS = 2000; // 2 seconds between polls
434
- const POLL_TIMEOUT_MS = 60000; // 1 minute timeout before retrying
513
+ const PollIntervalMs = 2000; // 2 seconds between polls
514
+ const PollTimeoutMs = 60000; // 1 minute timeout before retrying
435
515
 
436
516
  let iteration = 0;
437
517
 
@@ -439,6 +519,12 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
439
519
  // NEW: Runner-level polling mode
440
520
  console.log(`[${role}] Mode: runner-level polling (use --ai-loop for AI-based polling)`);
441
521
 
522
+ // Create API config for ping/close
523
+ const apiConfig: ApiConfig = { apiUrl, apiKey, agentId };
524
+
525
+ // Setup graceful shutdown handlers with API config for close on exit
526
+ setupShutdownHandlers(role, apiConfig);
527
+
442
528
  // Register agent before starting
443
529
  const agentName = process.env.AGENT_NAME || `${role}-${agentId.slice(0, 8)}`;
444
530
  try {
@@ -456,15 +542,22 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
456
542
  process.exit(1);
457
543
  }
458
544
 
545
+ // Track last finished task check for leads (to avoid re-processing)
546
+ let lastFinishedTaskCheck: string | undefined;
547
+
459
548
  while (true) {
549
+ // Ping server on each iteration to keep status updated
550
+ await pingServer(apiConfig, role);
551
+
460
552
  console.log(`\n[${role}] Polling for triggers...`);
461
553
 
462
554
  const trigger = await pollForTrigger({
463
555
  apiUrl,
464
556
  apiKey,
465
557
  agentId,
466
- pollInterval: POLL_INTERVAL_MS,
467
- pollTimeout: POLL_TIMEOUT_MS,
558
+ pollInterval: PollIntervalMs,
559
+ pollTimeout: PollTimeoutMs,
560
+ since: lastFinishedTaskCheck,
468
561
  });
469
562
 
470
563
  if (!trigger) {
@@ -472,6 +565,11 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
472
565
  continue;
473
566
  }
474
567
 
568
+ // After getting a tasks_finished trigger, update the timestamp
569
+ if (trigger.type === "tasks_finished") {
570
+ lastFinishedTaskCheck = new Date().toISOString();
571
+ }
572
+
475
573
  console.log(`[${role}] Trigger received: ${trigger.type}`);
476
574
 
477
575
  // Build prompt based on trigger
@@ -540,7 +638,16 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
540
638
  // Original AI-loop mode (existing behavior)
541
639
  console.log(`[${role}] Mode: AI-based polling (legacy)`);
542
640
 
641
+ // Create API config for ping/close
642
+ const apiConfig: ApiConfig = { apiUrl, apiKey, agentId };
643
+
644
+ // Setup graceful shutdown handlers with API config for close on exit
645
+ setupShutdownHandlers(role, apiConfig);
646
+
543
647
  while (true) {
648
+ // Ping server on each iteration to keep status updated
649
+ await pingServer(apiConfig, role);
650
+
544
651
  iteration++;
545
652
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
546
653
  const logFile = `${logDir}/${timestamp}.jsonl`;
package/src/http.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  closeDb,
13
13
  createAgent,
14
14
  createSessionLogs,
15
+ createTaskExtended,
15
16
  getAgentById,
16
17
  getAgentWithTasks,
17
18
  getAllAgents,
@@ -28,6 +29,7 @@ import {
28
29
  getLogsByTaskId,
29
30
  getOfferedTasksForAgent,
30
31
  getPendingTaskForAgent,
32
+ getRecentlyFinishedWorkerTasks,
31
33
  getServicesByAgentId,
32
34
  getSessionLogsByTaskId,
33
35
  getTaskById,
@@ -284,6 +286,10 @@ const httpServer = createHttpServer(async (req, res) => {
284
286
  return;
285
287
  }
286
288
 
289
+ // Get optional 'since' parameter for finished tasks
290
+ const queryParams = parseQueryParams(req.url || "");
291
+ const since = queryParams.get("since") || undefined;
292
+
287
293
  // Use transaction for consistent reads across all trigger checks
288
294
  const result = getDb().transaction(() => {
289
295
  const agent = getAgentById(myAgentId);
@@ -291,7 +297,7 @@ const httpServer = createHttpServer(async (req, res) => {
291
297
  return { error: "Agent not found", status: 404 };
292
298
  }
293
299
 
294
- // Check for offered tasks first (highest priority)
300
+ // Check for offered tasks first (highest priority for both workers and leads)
295
301
  const offeredTasks = getOfferedTasksForAgent(myAgentId);
296
302
  const firstOfferedTask = offeredTasks[0];
297
303
  if (firstOfferedTask) {
@@ -316,8 +322,10 @@ const httpServer = createHttpServer(async (req, res) => {
316
322
  };
317
323
  }
318
324
 
319
- // For lead agents, check for unread mentions
320
325
  if (agent.isLead) {
326
+ // === LEAD-SPECIFIC TRIGGERS ===
327
+
328
+ // Check for unread mentions
321
329
  const inbox = getInboxSummary(myAgentId);
322
330
  if (inbox.mentionsCount > 0) {
323
331
  return {
@@ -328,7 +336,21 @@ const httpServer = createHttpServer(async (req, res) => {
328
336
  };
329
337
  }
330
338
 
331
- // Check for tasks needing assignment (unassigned tasks in pool)
339
+ // Check for recently finished worker tasks
340
+ const finishedTasks = getRecentlyFinishedWorkerTasks(since);
341
+ if (finishedTasks.length > 0) {
342
+ return {
343
+ trigger: {
344
+ type: "tasks_finished",
345
+ count: finishedTasks.length,
346
+ tasks: finishedTasks,
347
+ },
348
+ };
349
+ }
350
+ } else {
351
+ // === WORKER-SPECIFIC TRIGGERS ===
352
+
353
+ // Check for unassigned tasks in pool (workers can claim)
332
354
  const unassignedCount = getUnassignedTasksCount();
333
355
  if (unassignedCount > 0) {
334
356
  return {
@@ -524,6 +546,50 @@ const httpServer = createHttpServer(async (req, res) => {
524
546
  return;
525
547
  }
526
548
 
549
+ // POST /api/tasks - Create a new task
550
+ if (
551
+ req.method === "POST" &&
552
+ pathSegments[0] === "api" &&
553
+ pathSegments[1] === "tasks" &&
554
+ !pathSegments[2]
555
+ ) {
556
+ // Parse request body
557
+ const chunks: Buffer[] = [];
558
+ for await (const chunk of req) {
559
+ chunks.push(chunk);
560
+ }
561
+ const body = JSON.parse(Buffer.concat(chunks).toString());
562
+
563
+ // Validate required fields
564
+ if (!body.task || typeof body.task !== "string") {
565
+ res.writeHead(400, { "Content-Type": "application/json" });
566
+ res.end(JSON.stringify({ error: "Missing or invalid 'task' field" }));
567
+ return;
568
+ }
569
+
570
+ try {
571
+ // Create task with provided options
572
+ const task = createTaskExtended(body.task, {
573
+ agentId: body.agentId || undefined,
574
+ creatorAgentId: myAgentId || undefined,
575
+ taskType: body.taskType || undefined,
576
+ tags: body.tags || undefined,
577
+ priority: body.priority || 50,
578
+ dependsOn: body.dependsOn || undefined,
579
+ offeredTo: body.offeredTo || undefined,
580
+ source: body.source || "api",
581
+ });
582
+
583
+ res.writeHead(201, { "Content-Type": "application/json" });
584
+ res.end(JSON.stringify(task));
585
+ } catch (error) {
586
+ console.error("[HTTP] Failed to create task:", error);
587
+ res.writeHead(500, { "Content-Type": "application/json" });
588
+ res.end(JSON.stringify({ error: "Failed to create task" }));
589
+ }
590
+ return;
591
+ }
592
+
527
593
  // GET /api/tasks/:id - Get single task with logs
528
594
  if (
529
595
  req.method === "GET" &&
package/src/server.ts CHANGED
@@ -41,7 +41,8 @@ export function getEnabledCapabilities(): string[] {
41
41
 
42
42
  export function createServer() {
43
43
  // Initialize database with WAL mode
44
- initDb();
44
+ // Uses DATABASE_PATH env var for Docker volume compatibility (WAL needs .sqlite, .sqlite-wal, .sqlite-shm on same filesystem)
45
+ initDb(process.env.DATABASE_PATH);
45
46
 
46
47
  const server = new McpServer(
47
48
  {