aegis-bridge 2.3.4 → 2.3.6

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 CHANGED
@@ -103,7 +103,7 @@ All endpoints under `/v1/`.
103
103
  | Method | Endpoint | Description |
104
104
  |--------|----------|-------------|
105
105
  | `GET` | `/v1/health` | Server health & uptime |
106
- | `POST` | `/v1/sessions` | Create a session |
106
+ | `POST` | `/v1/sessions` | Create (or reuse) a session |
107
107
  | `GET` | `/v1/sessions` | List sessions |
108
108
  | `GET` | `/v1/sessions/:id` | Session details |
109
109
  | `GET` | `/v1/sessions/:id/read` | Parsed transcript |
@@ -144,9 +144,38 @@ All endpoints under `/v1/`.
144
144
 
145
145
  </details>
146
146
 
147
- ---
147
+ <details>
148
+ <summary>Session Reuse</summary>
149
+
150
+ When you `POST /v1/sessions` (or `POST /sessions`) with a `workDir` that already has an **idle** session, Aegis reuses that session instead of creating a duplicate. The existing session's prompt is delivered and you get the same session object back.
151
+
152
+ **Response differences:**
153
+
154
+ | | New Session | Reused Session |
155
+ |---|---|---|
156
+ | Status | `201 Created` | `200 OK` |
157
+ | `reused` | `false` | `true` |
158
+ | `promptDelivery` | `{ delivered, attempts }` | `{ delivered, attempts }` |
159
+
160
+ ```bash
161
+ # First call → creates session (201)
162
+ curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:9100/v1/sessions \
163
+ -H "Content-Type: application/json" \
164
+ -d '{"workDir": "/home/user/project", "prompt": "Fix the tests"}'
165
+ # → 201
148
166
 
149
- ## Integrations
167
+ # Same workDir while idle → reuses session (200)
168
+ curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:9100/v1/sessions \
169
+ -H "Content-Type: application/json" \
170
+ -d '{"workDir": "/home/user/project", "prompt": "Now add error handling"}'
171
+ # → 200, body includes "reused": true
172
+ ```
173
+
174
+ Only **idle** sessions are reused. Working, stalled, or permission-prompt sessions are ignored — a new one is created.
175
+
176
+ </details>
177
+
178
+ ---
150
179
 
151
180
  ### Telegram
152
181
 
package/dist/auth.js CHANGED
@@ -155,8 +155,9 @@ export class AuthManager {
155
155
  const previous = this.sseMutex;
156
156
  this.sseMutex = lock;
157
157
  // #509: await + try/finally together so release() fires even if previous rejects
158
+ // #573: catch prior rejection so it doesn't propagate and block subsequent callers
158
159
  try {
159
- await previous;
160
+ await previous.catch(() => { });
160
161
  // Cleanup expired tokens first
161
162
  this.cleanExpiredSSETokens();
162
163
  // Enforce per-key limit
package/dist/server.js CHANGED
@@ -34,7 +34,7 @@ import { MetricsCollector } from './metrics.js';
34
34
  import { registerHookRoutes } from './hooks.js';
35
35
  import { registerWsTerminalRoute } from './ws-terminal.js';
36
36
  import { SwarmMonitor } from './swarm-monitor.js';
37
- import { execSync } from 'node:child_process';
37
+ import { execFileSync } from 'node:child_process';
38
38
  import { authKeySchema, sendMessageSchema, commandSchema, bashSchema, screenshotSchema, permissionHookSchema, stopHookSchema, batchSessionSchema, pipelineSchema, parseIntSafe, isValidUUID, } from './validation.js';
39
39
  const __filename = fileURLToPath(import.meta.url);
40
40
  const __dirname = path.dirname(__filename);
@@ -448,19 +448,30 @@ app.get('/v1/sessions', async (req) => {
448
448
  app.get('/sessions', async () => sessions.listSessions());
449
449
  /** Validate workDir — delegates to validation.ts (Issue #435). */
450
450
  const validateWorkDirWithConfig = (workDir) => validateWorkDir(workDir, config.allowedWorkDirs);
451
- // Create session
451
+ // Create session (Issue #607: reuse idle session for same workDir)
452
452
  app.post('/v1/sessions', async (req, reply) => {
453
453
  const parsed = createSessionSchema.safeParse(req.body);
454
454
  if (!parsed.success) {
455
455
  return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
456
456
  }
457
457
  const { workDir, name, prompt, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove } = parsed.data;
458
- console.time("POST_CREATE_SESSION");
459
458
  if (!workDir)
460
459
  return reply.status(400).send({ error: 'workDir is required' });
461
460
  const safeWorkDir = await validateWorkDirWithConfig(workDir);
462
461
  if (typeof safeWorkDir === 'object')
463
462
  return reply.status(400).send({ error: safeWorkDir.error, code: safeWorkDir.code });
463
+ // Issue #607: Check for an existing idle session with the same workDir
464
+ const existing = sessions.findIdleSessionByWorkDir(safeWorkDir);
465
+ if (existing) {
466
+ // Send prompt to the existing session if provided
467
+ let promptDelivery;
468
+ if (prompt) {
469
+ promptDelivery = await sessions.sendInitialPrompt(existing.id, prompt);
470
+ metrics.promptSent(promptDelivery.delivered);
471
+ }
472
+ return reply.status(200).send({ ...existing, reused: true, promptDelivery });
473
+ }
474
+ console.time("POST_CREATE_SESSION");
464
475
  const session = await sessions.createSession({ workDir: safeWorkDir, name, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove });
465
476
  console.timeEnd("POST_CREATE_SESSION");
466
477
  console.time("POST_CHANNEL_CREATED");
@@ -490,7 +501,7 @@ app.post('/v1/sessions', async (req, reply) => {
490
501
  }
491
502
  return reply.status(201).send({ ...session, promptDelivery });
492
503
  });
493
- // Backwards compat
504
+ // Backwards compat (Issue #607: same reuse logic as v1 route)
494
505
  app.post('/sessions', async (req, reply) => {
495
506
  const parsed = createSessionSchema.safeParse(req.body);
496
507
  if (!parsed.success) {
@@ -502,6 +513,16 @@ app.post('/sessions', async (req, reply) => {
502
513
  const safeWorkDir = await validateWorkDirWithConfig(workDir);
503
514
  if (typeof safeWorkDir === 'object')
504
515
  return reply.status(400).send({ error: safeWorkDir.error, code: safeWorkDir.code });
516
+ // Issue #607: Check for an existing idle session with the same workDir
517
+ const existing = sessions.findIdleSessionByWorkDir(safeWorkDir);
518
+ if (existing) {
519
+ let promptDelivery;
520
+ if (prompt) {
521
+ promptDelivery = await sessions.sendInitialPrompt(existing.id, prompt);
522
+ metrics.promptSent(promptDelivery.delivered);
523
+ }
524
+ return reply.status(200).send({ ...existing, reused: true, promptDelivery });
525
+ }
505
526
  const session = await sessions.createSession({ workDir: safeWorkDir, name, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove });
506
527
  // Issue #46: Topic first, then prompt (same fix as v1 route)
507
528
  await channels.sessionCreated({
@@ -1323,7 +1344,7 @@ async function killStalePortHolder(port) {
1323
1344
  // Small random delay to reduce race window with systemd restarts
1324
1345
  await new Promise(resolve => setTimeout(resolve, 100 + Math.random() * 400));
1325
1346
  try {
1326
- const output = execSync(`lsof -ti tcp:${port}`, { encoding: 'utf-8', timeout: 5_000 }).trim();
1347
+ const output = execFileSync('lsof', ['-ti', `tcp:${port}`], { encoding: 'utf-8', timeout: 5_000 }).trim();
1327
1348
  if (!output)
1328
1349
  return false;
1329
1350
  const pids = output.split('\n').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n));
package/dist/session.d.ts CHANGED
@@ -154,6 +154,10 @@ export declare class SessionManager {
154
154
  isWindowAlive(id: string): Promise<boolean>;
155
155
  /** List all sessions. */
156
156
  listSessions(): SessionInfo[];
157
+ /** Issue #607: Find an idle session for the given workDir.
158
+ * Returns the most recently active idle session, or null if none found.
159
+ * Used to resume existing sessions instead of creating duplicates. */
160
+ findIdleSessionByWorkDir(workDir: string): SessionInfo | null;
157
161
  /** Get health info for a session.
158
162
  * Issue #2: Returns comprehensive health status for orchestrators.
159
163
  */
package/dist/session.js CHANGED
@@ -489,12 +489,13 @@ export class SessionManager {
489
489
  await this.save();
490
490
  // Issue #353: Fetch CC process PID for swarm parent matching.
491
491
  // Fire-and-forget — PID is not needed synchronously.
492
+ // Issue #574: Add .catch() to prevent unhandled rejection if tmux fails mid-lookup.
492
493
  void this.tmux.listPanePid(windowId).then(pid => {
493
494
  if (pid !== null) {
494
495
  session.ccPid = pid;
495
- void this.save();
496
+ void this.save().catch(e => console.error(`Session: failed to save PID for ${id}:`, e));
496
497
  }
497
- });
498
+ }).catch(e => console.error(`Session: failed to list pane PID for ${id}:`, e));
498
499
  // Start BOTH discovery methods in parallel:
499
500
  // 1. Hook-based: fast, relies on SessionStart hook writing session_map.json
500
501
  // 2. Filesystem-based: slower, scans for new .jsonl files — works when hooks fail
@@ -652,6 +653,17 @@ export class SessionManager {
652
653
  listSessions() {
653
654
  return Object.values(this.state.sessions);
654
655
  }
656
+ /** Issue #607: Find an idle session for the given workDir.
657
+ * Returns the most recently active idle session, or null if none found.
658
+ * Used to resume existing sessions instead of creating duplicates. */
659
+ findIdleSessionByWorkDir(workDir) {
660
+ const candidates = Object.values(this.state.sessions).filter((s) => s.workDir === workDir && s.status === 'idle');
661
+ if (candidates.length === 0)
662
+ return null;
663
+ // Return the most recently active session
664
+ candidates.sort((a, b) => b.lastActivity - a.lastActivity);
665
+ return candidates[0];
666
+ }
655
667
  /** Get health info for a session.
656
668
  * Issue #2: Returns comprehensive health status for orchestrators.
657
669
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aegis-bridge",
3
- "version": "2.3.4",
3
+ "version": "2.3.6",
4
4
  "type": "module",
5
5
  "description": "Orchestrate Claude Code sessions via API. Create, brief, monitor, refine, ship.",
6
6
  "main": "dist/server.js",