aegis-bridge 2.3.8 → 2.3.10

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/dist/pipeline.js CHANGED
@@ -179,6 +179,11 @@ export class PipelineManager {
179
179
  setTimeout(() => {
180
180
  this.pipelines.delete(pipelineId);
181
181
  this.pipelineConfigs.delete(pipelineId); // #219: clean up stored config
182
+ // #578: Stop polling when no pipelines remain
183
+ if (this.pipelines.size === 0 && this.pollInterval) {
184
+ clearInterval(this.pollInterval);
185
+ this.pollInterval = null;
186
+ }
182
187
  }, 30_000);
183
188
  }
184
189
  }
package/dist/server.js CHANGED
@@ -34,6 +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 { killAllSessions } from './signal-cleanup-helper.js';
37
38
  import { execFileSync } from 'node:child_process';
38
39
  import { authKeySchema, sendMessageSchema, commandSchema, bashSchema, screenshotSchema, permissionHookSchema, stopHookSchema, batchSessionSchema, pipelineSchema, parseIntSafe, isValidUUID, } from './validation.js';
39
40
  const __filename = fileURLToPath(import.meta.url);
@@ -1503,6 +1504,13 @@ async function main() {
1503
1504
  clearInterval(metricsSaveInterval);
1504
1505
  clearInterval(ipPruneInterval);
1505
1506
  clearInterval(authSweepInterval);
1507
+ // Issue #569: Kill all CC sessions and tmux windows before exit
1508
+ try {
1509
+ await killAllSessions(sessions, tmux);
1510
+ }
1511
+ catch (e) {
1512
+ console.error('Error killing sessions:', e);
1513
+ }
1506
1514
  // 3. Destroy channels (awaits Telegram poll loop)
1507
1515
  try {
1508
1516
  await channels.destroy();
@@ -0,0 +1,48 @@
1
+ /**
2
+ * signal-cleanup-helper.ts — Signal handler cleanup logic for Issue #569.
3
+ *
4
+ * Provides killAllSessions() and createSignalHandler() for graceful shutdown.
5
+ * Separated from server.ts for testability.
6
+ */
7
+ import type { SessionManager } from './session.js';
8
+ import type { TmuxManager } from './tmux.js';
9
+ /** Result of killAllSessions operation. */
10
+ export interface KillAllResult {
11
+ /** Number of sessions successfully killed. */
12
+ killed: number;
13
+ /** Number of sessions that failed to kill. */
14
+ errors: number;
15
+ }
16
+ /** Result of killAllSessionsWithTimeout operation. */
17
+ export interface KillAllWithTimeoutResult extends KillAllResult {
18
+ /** Whether any session kill timed out. */
19
+ timedOut: boolean;
20
+ }
21
+ /**
22
+ * Kill all active CC sessions and the tmux session.
23
+ * Best-effort: continues even if individual session kills fail.
24
+ *
25
+ * @param sessions - SessionManager instance
26
+ * @param tmux - TmuxManager instance
27
+ * @returns Number of sessions killed and errors encountered
28
+ */
29
+ export declare function killAllSessions(sessions: SessionManager, tmux: TmuxManager): Promise<KillAllResult>;
30
+ /**
31
+ * Kill all sessions with per-session timeout protection.
32
+ * If a session kill hangs beyond the timeout, it is skipped.
33
+ *
34
+ * @param sessions - SessionManager instance
35
+ * @param tmux - TmuxManager instance
36
+ * @param perSessionTimeoutMs - Maximum time to wait per session kill (default 5000ms)
37
+ * @returns Result including timeout status
38
+ */
39
+ export declare function killAllSessionsWithTimeout(sessions: SessionManager, tmux: TmuxManager, perSessionTimeoutMs?: number): Promise<KillAllWithTimeoutResult>;
40
+ /**
41
+ * Create a signal handler that kills all sessions on SIGTERM/SIGINT.
42
+ * Includes reentrance guard to prevent double cleanup on rapid signals.
43
+ *
44
+ * @param sessions - SessionManager instance
45
+ * @param tmux - TmuxManager instance
46
+ * @returns Signal handler function
47
+ */
48
+ export declare function createSignalHandler(sessions: SessionManager, tmux: TmuxManager): (signal: string) => void;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * signal-cleanup-helper.ts — Signal handler cleanup logic for Issue #569.
3
+ *
4
+ * Provides killAllSessions() and createSignalHandler() for graceful shutdown.
5
+ * Separated from server.ts for testability.
6
+ */
7
+ /**
8
+ * Kill all active CC sessions and the tmux session.
9
+ * Best-effort: continues even if individual session kills fail.
10
+ *
11
+ * @param sessions - SessionManager instance
12
+ * @param tmux - TmuxManager instance
13
+ * @returns Number of sessions killed and errors encountered
14
+ */
15
+ export async function killAllSessions(sessions, tmux) {
16
+ const allSessions = sessions.listSessions();
17
+ let killed = 0;
18
+ let errors = 0;
19
+ // Kill each session individually (restores settings, cleans up temp files)
20
+ for (const session of allSessions) {
21
+ try {
22
+ await sessions.killSession(session.id);
23
+ killed++;
24
+ }
25
+ catch (e) {
26
+ errors++;
27
+ console.error(`Signal cleanup: failed to kill session ${session.windowName} (${session.id.slice(0, 8)}): ${e.message}`);
28
+ }
29
+ }
30
+ // Final fallback: kill the entire tmux session to ensure nothing is left
31
+ try {
32
+ await tmux.killSession();
33
+ }
34
+ catch (e) {
35
+ console.error(`Signal cleanup: failed to kill tmux session: ${e.message}`);
36
+ }
37
+ console.log(`Signal cleanup: killed ${killed} sessions (${errors} errors)`);
38
+ return { killed, errors };
39
+ }
40
+ /**
41
+ * Kill all sessions with per-session timeout protection.
42
+ * If a session kill hangs beyond the timeout, it is skipped.
43
+ *
44
+ * @param sessions - SessionManager instance
45
+ * @param tmux - TmuxManager instance
46
+ * @param perSessionTimeoutMs - Maximum time to wait per session kill (default 5000ms)
47
+ * @returns Result including timeout status
48
+ */
49
+ export async function killAllSessionsWithTimeout(sessions, tmux, perSessionTimeoutMs = 5_000) {
50
+ const allSessions = sessions.listSessions();
51
+ let killed = 0;
52
+ let errors = 0;
53
+ let timedOut = false;
54
+ for (const session of allSessions) {
55
+ try {
56
+ await withTimeout(sessions.killSession(session.id), perSessionTimeoutMs, `Session kill timeout for ${session.windowName}`);
57
+ killed++;
58
+ }
59
+ catch (e) {
60
+ if (e instanceof TimeoutError) {
61
+ timedOut = true;
62
+ console.error(`Signal cleanup: TIMED OUT killing session ${session.windowName}`);
63
+ }
64
+ else {
65
+ console.error(`Signal cleanup: failed to kill session ${session.windowName}: ${e.message}`);
66
+ }
67
+ errors++;
68
+ }
69
+ }
70
+ // Final fallback: kill entire tmux session
71
+ try {
72
+ await tmux.killSession();
73
+ }
74
+ catch (e) {
75
+ console.error(`Signal cleanup: failed to kill tmux session: ${e.message}`);
76
+ }
77
+ console.log(`Signal cleanup: killed ${killed}/${allSessions.length} sessions (${errors} errors, ${timedOut ? 'some timed out' : 'no timeouts'})`);
78
+ return { killed, errors, timedOut };
79
+ }
80
+ /** Error thrown when an operation exceeds its timeout. */
81
+ class TimeoutError extends Error {
82
+ constructor(message) {
83
+ super(message);
84
+ this.name = 'TimeoutError';
85
+ }
86
+ }
87
+ /** Wrap a promise with a timeout. */
88
+ function withTimeout(promise, ms, message) {
89
+ return new Promise((resolve, reject) => {
90
+ const timer = setTimeout(() => reject(new TimeoutError(message)), ms);
91
+ promise.then((val) => { clearTimeout(timer); resolve(val); }, (err) => { clearTimeout(timer); reject(err); });
92
+ });
93
+ }
94
+ /**
95
+ * Create a signal handler that kills all sessions on SIGTERM/SIGINT.
96
+ * Includes reentrance guard to prevent double cleanup on rapid signals.
97
+ *
98
+ * @param sessions - SessionManager instance
99
+ * @param tmux - TmuxManager instance
100
+ * @returns Signal handler function
101
+ */
102
+ export function createSignalHandler(sessions, tmux) {
103
+ let shuttingDown = false;
104
+ return (signal) => {
105
+ if (shuttingDown)
106
+ return;
107
+ shuttingDown = true;
108
+ console.log(`${signal} received — cleaning up ${sessions.listSessions().length} active sessions...`);
109
+ void killAllSessions(sessions, tmux)
110
+ .then((result) => {
111
+ console.log(`${signal} cleanup complete: ${result.killed} sessions killed`);
112
+ })
113
+ .catch((e) => {
114
+ console.error(`${signal} cleanup error:`, e);
115
+ });
116
+ };
117
+ }
118
+ //# sourceMappingURL=signal-cleanup-helper.js.map
@@ -179,12 +179,19 @@ export async function readNewEntries(filePath, fromOffset) {
179
179
  finally {
180
180
  await fd.close();
181
181
  }
182
+ let foundNewline = false;
182
183
  for (let i = scanBuf.length - 1; i >= 0; i--) {
183
184
  if (scanBuf[i] === 0x0a) { // '\n'
184
185
  effectiveOffset = scanStart + i + 1;
186
+ foundNewline = true;
185
187
  break;
186
188
  }
187
189
  }
190
+ // Issue #579: If no newline found and we didn't scan from byte 0,
191
+ // fall back to offset 0 to avoid starting mid-line.
192
+ if (!foundNewline && scanStart > 0) {
193
+ effectiveOffset = 0;
194
+ }
188
195
  }
189
196
  const slicedContent = await new Promise((resolve, reject) => {
190
197
  const chunks = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aegis-bridge",
3
- "version": "2.3.8",
3
+ "version": "2.3.10",
4
4
  "type": "module",
5
5
  "description": "Orchestrate Claude Code sessions via API. Create, brief, monitor, refine, ship.",
6
6
  "main": "dist/server.js",