@bytespell/amux 0.0.10 → 0.0.12

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 (78) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/CLAUDE.md +104 -0
  3. package/LICENSE +21 -0
  4. package/README.md +204 -0
  5. package/dist/cli.d.ts +14 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +118 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/client.d.ts +68 -0
  10. package/dist/client.d.ts.map +1 -0
  11. package/dist/client.js +135 -0
  12. package/dist/client.js.map +1 -0
  13. package/dist/index.d.ts +41 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +44 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/{lib/mentions.d.ts → message-parser.d.ts} +3 -5
  18. package/dist/message-parser.d.ts.map +1 -0
  19. package/dist/message-parser.js +45 -0
  20. package/dist/message-parser.js.map +1 -0
  21. package/dist/server.d.ts +24 -0
  22. package/dist/server.d.ts.map +1 -0
  23. package/dist/server.js +356 -0
  24. package/dist/server.js.map +1 -0
  25. package/dist/session-updates.d.ts +26 -0
  26. package/dist/session-updates.d.ts.map +1 -0
  27. package/dist/session-updates.js +68 -0
  28. package/dist/session-updates.js.map +1 -0
  29. package/dist/session.d.ts +207 -0
  30. package/dist/session.d.ts.map +1 -0
  31. package/dist/session.js +563 -0
  32. package/dist/session.js.map +1 -0
  33. package/dist/state.d.ts +74 -0
  34. package/dist/state.d.ts.map +1 -0
  35. package/dist/state.js +250 -0
  36. package/dist/state.js.map +1 -0
  37. package/dist/terminal.d.ts +47 -0
  38. package/dist/terminal.d.ts.map +1 -0
  39. package/dist/terminal.js +137 -0
  40. package/dist/terminal.js.map +1 -0
  41. package/dist/types.d.ts +64 -2
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +16 -31
  44. package/dist/types.js.map +1 -1
  45. package/dist/ws-adapter.d.ts +39 -0
  46. package/dist/ws-adapter.d.ts.map +1 -0
  47. package/dist/ws-adapter.js +198 -0
  48. package/dist/ws-adapter.js.map +1 -0
  49. package/package.json +47 -24
  50. package/src/client.ts +162 -0
  51. package/src/index.ts +66 -0
  52. package/src/message-parser.ts +54 -0
  53. package/src/session-updates.ts +87 -0
  54. package/src/session.ts +719 -0
  55. package/src/state.ts +287 -0
  56. package/src/terminal.ts +164 -0
  57. package/src/types.ts +88 -0
  58. package/src/ws-adapter.ts +245 -0
  59. package/tsconfig.json +22 -0
  60. package/dist/chunk-5IPYOXBE.js +0 -32
  61. package/dist/chunk-5IPYOXBE.js.map +0 -1
  62. package/dist/chunk-C73RKCTS.js +0 -36
  63. package/dist/chunk-C73RKCTS.js.map +0 -1
  64. package/dist/chunk-VVXT4HQM.js +0 -779
  65. package/dist/chunk-VVXT4HQM.js.map +0 -1
  66. package/dist/lib/logger.d.ts +0 -24
  67. package/dist/lib/logger.js +0 -17
  68. package/dist/lib/logger.js.map +0 -1
  69. package/dist/lib/mentions.js +0 -7
  70. package/dist/lib/mentions.js.map +0 -1
  71. package/dist/streams/backends/index.d.ts +0 -88
  72. package/dist/streams/backends/index.js +0 -13
  73. package/dist/streams/backends/index.js.map +0 -1
  74. package/dist/streams/manager.d.ts +0 -55
  75. package/dist/streams/manager.js +0 -248
  76. package/dist/streams/manager.js.map +0 -1
  77. package/dist/types-DV6-SxsB.d.ts +0 -192
  78. package/scripts/fix-pty.cjs +0 -21
package/src/session.ts ADDED
@@ -0,0 +1,719 @@
1
+ import { spawn, type ChildProcess } from 'child_process';
2
+ import { EventEmitter } from 'events';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { Writable, Readable } from 'stream';
6
+
7
+ import * as acp from '@agentclientprotocol/sdk';
8
+
9
+ import { AmuxClient } from './client.js';
10
+ import { TerminalManager } from './terminal.js';
11
+ import { StateManager } from './state.js';
12
+ import { parseMessageToContentBlocks } from './message-parser.js';
13
+ import type {
14
+ AgentSessionConfig,
15
+ AgentConfig,
16
+ SessionRestoreInfo,
17
+ SessionMetadata,
18
+ } from './types.js';
19
+ import { AGENTS as DEFAULT_AGENTS } from './types.js';
20
+
21
+ const INIT_TIMEOUT_MS = 90000;
22
+
23
+ /**
24
+ * Events emitted by AgentSession
25
+ */
26
+ export interface AgentSessionEvents {
27
+ ready: {
28
+ cwd: string;
29
+ sessionId: string | null;
30
+ sessionRestore?: SessionRestoreInfo;
31
+ capabilities: acp.AgentCapabilities | null;
32
+ agent: { type: string; name: string };
33
+ availableAgents: Array<{ id: string; name: string }>;
34
+ };
35
+ update: acp.SessionUpdate;
36
+ turn_start: Record<string, never>;
37
+ turn_end: Record<string, never>;
38
+ permission_request: {
39
+ requestId: string;
40
+ toolCall?: acp.ToolCall;
41
+ options: acp.PermissionOption[];
42
+ };
43
+ prompt_complete: { stopReason?: string };
44
+ session_created: { sessionId: string | null };
45
+ session_switched: { sessionId: string };
46
+ history_replay: {
47
+ previousSessionId: string;
48
+ events: unknown[];
49
+ eventCount: number;
50
+ };
51
+ error: { message: string };
52
+ agent_exit: { code: number | null; signal: string | null };
53
+ connecting: Record<string, never>;
54
+ }
55
+
56
+ /**
57
+ * Helper to wrap promises with timeout
58
+ */
59
+ function withTimeout<T>(promise: Promise<T>, ms: number, operation: string): Promise<T> {
60
+ return Promise.race([
61
+ promise,
62
+ new Promise<T>((_, reject) =>
63
+ setTimeout(() => reject(new Error(`${operation} timed out after ${ms}ms`)), ms)
64
+ ),
65
+ ]);
66
+ }
67
+
68
+ /**
69
+ * AgentSession - Core ACP session management with EventEmitter interface.
70
+ *
71
+ * Emits events for all session activity. Transport-agnostic - wire up
72
+ * to WebSocket, HTTP, IPC, or anything else.
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const session = new AgentSession({
77
+ * instanceId: 'my-instance',
78
+ * basePath: __dirname,
79
+ * });
80
+ *
81
+ * session.on('ready', (data) => console.log('Ready:', data));
82
+ * session.on('update', (data) => console.log('Update:', data));
83
+ *
84
+ * await session.spawnAgent();
85
+ * await session.prompt('Hello!');
86
+ * ```
87
+ */
88
+ export class AgentSession extends EventEmitter {
89
+ // Session state
90
+ private _cwd: string;
91
+ private _sessionId: string | null;
92
+ private _agentType: string;
93
+ private _systemContext?: string;
94
+
95
+ // Agent process state
96
+ private agentProcess: ChildProcess | null = null;
97
+ private acpConnection: acp.ClientSideConnection | null = null;
98
+ private _agentCapabilities: acp.AgentCapabilities | null = null;
99
+
100
+ // Components
101
+ private terminalManager: TerminalManager;
102
+ private amuxClient: AmuxClient | null = null;
103
+ private stateManager: StateManager;
104
+
105
+ // Config
106
+ private instanceId: string;
107
+ private basePath: string;
108
+ private fixedCwd?: string;
109
+ private agents: Record<string, AgentConfig>;
110
+
111
+ constructor(config: AgentSessionConfig) {
112
+ super();
113
+
114
+ this.instanceId = config.instanceId;
115
+ this.basePath = config.basePath;
116
+ this.fixedCwd = config.fixedCwd;
117
+ this._systemContext = config.systemContext;
118
+ this.agents = DEFAULT_AGENTS;
119
+
120
+ // Create state manager
121
+ this.stateManager = new StateManager(config.stateDir);
122
+
123
+ // Load persisted state
124
+ const persistedState = this.stateManager.loadState(this.instanceId);
125
+ this._cwd = config.fixedCwd ?? persistedState.cwd ?? os.homedir();
126
+ this._sessionId = persistedState.sessionId ?? null;
127
+ this._agentType = config.agentType ?? persistedState.agentType ?? 'claude-code';
128
+
129
+ // Create terminal manager with cwd getter
130
+ this.terminalManager = new TerminalManager(() => this._cwd);
131
+ }
132
+
133
+ // Type-safe event emitter methods
134
+ override emit<K extends keyof AgentSessionEvents>(
135
+ event: K,
136
+ data: AgentSessionEvents[K]
137
+ ): boolean {
138
+ return super.emit(event, data);
139
+ }
140
+
141
+ override on<K extends keyof AgentSessionEvents>(
142
+ event: K,
143
+ listener: (data: AgentSessionEvents[K]) => void
144
+ ): this {
145
+ return super.on(event, listener);
146
+ }
147
+
148
+ override once<K extends keyof AgentSessionEvents>(
149
+ event: K,
150
+ listener: (data: AgentSessionEvents[K]) => void
151
+ ): this {
152
+ return super.once(event, listener);
153
+ }
154
+
155
+ /**
156
+ * Emit update and store to history
157
+ */
158
+ private emitUpdate(update: acp.SessionUpdate): void {
159
+ this.emit('update', update);
160
+ this.stateManager.storeEvent(this._sessionId, { type: 'session_update', update });
161
+ }
162
+
163
+ /**
164
+ * Emit turn marker and store to history
165
+ */
166
+ private emitTurnMarker(event: 'turn_start' | 'turn_end'): void {
167
+ this.emit(event, {});
168
+ this.stateManager.storeEvent(this._sessionId, { type: event });
169
+ }
170
+
171
+ // Getters for read-only access
172
+ get cwd(): string { return this._cwd; }
173
+ get sessionId(): string | null { return this._sessionId; }
174
+ get agentType(): string { return this._agentType; }
175
+ get agentCapabilities(): acp.AgentCapabilities | null { return this._agentCapabilities; }
176
+ get isConnected(): boolean { return this.acpConnection !== null; }
177
+ get systemContext(): string | undefined { return this._systemContext; }
178
+
179
+ /**
180
+ * Get current agent info
181
+ */
182
+ getAgentInfo(): { type: string; name: string } {
183
+ return {
184
+ type: this._agentType,
185
+ name: this.agents[this._agentType]?.name ?? 'Unknown',
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Get available agents list
191
+ */
192
+ getAvailableAgents(): Array<{ id: string; name: string }> {
193
+ return Object.entries(this.agents).map(([id, a]) => ({ id, name: a.name }));
194
+ }
195
+
196
+ /**
197
+ * Save current state to disk
198
+ */
199
+ private saveState(): void {
200
+ this.stateManager.saveState(this.instanceId, {
201
+ cwd: this._cwd,
202
+ sessionId: this._sessionId,
203
+ agentType: this._agentType,
204
+ });
205
+ }
206
+
207
+ /**
208
+ * Name the session after the first user message
209
+ */
210
+ private maybeNameSession(userMessage: string): void {
211
+ if (!this._sessionId) return;
212
+
213
+ const session = this.stateManager.getSession(this._sessionId);
214
+ if (!session) return;
215
+
216
+ const isDefaultName = session.name.startsWith('Session ') && session.name.length <= 16;
217
+ if (!isDefaultName) return;
218
+
219
+ let displayName = userMessage.trim();
220
+ if (displayName.length > 50) {
221
+ displayName = displayName.slice(0, 50);
222
+ const lastSpace = displayName.lastIndexOf(' ');
223
+ if (lastSpace > 30) {
224
+ displayName = displayName.slice(0, lastSpace);
225
+ }
226
+ displayName += '...';
227
+ }
228
+
229
+ this.stateManager.updateSessionMetadata(this._sessionId, { name: displayName });
230
+ console.log(`[amux] Named session: "${displayName}"`);
231
+ }
232
+
233
+ /**
234
+ * Spawn and initialize the ACP agent
235
+ */
236
+ async spawnAgent(): Promise<void> {
237
+ if (this.agentProcess) return;
238
+
239
+ const agent = this.agents[this._agentType];
240
+ if (!agent) {
241
+ console.error('[amux] Unknown agent type:', this._agentType);
242
+ this.emit('error', { message: `Unknown agent type: ${this._agentType}` });
243
+ return;
244
+ }
245
+
246
+ console.log(`[amux] Spawning ${agent.name} agent in cwd:`, this._cwd);
247
+ this.emit('connecting', {});
248
+
249
+ const agentBin = path.join(this.basePath, 'node_modules', '.bin', agent.bin);
250
+ const newProcess = spawn(agentBin, [], {
251
+ stdio: ['pipe', 'pipe', 'pipe'],
252
+ env: { ...process.env },
253
+ cwd: this._cwd,
254
+ });
255
+ this.agentProcess = newProcess;
256
+
257
+ newProcess.stderr?.on('data', (data: Buffer) => {
258
+ console.error('[amux] Agent stderr:', data.toString());
259
+ });
260
+
261
+ newProcess.on('error', (err) => {
262
+ console.error('[amux] Agent process error:', err);
263
+ if (this.agentProcess === newProcess) {
264
+ this.emit('error', { message: `Failed to start agent: ${err.message}` });
265
+ this.agentProcess = null;
266
+ this.acpConnection = null;
267
+ }
268
+ });
269
+
270
+ newProcess.on('exit', (code, signal) => {
271
+ console.log(`[amux] Agent exited: code=${code}, signal=${signal}`);
272
+ if (this.agentProcess === newProcess) {
273
+ this.agentProcess = null;
274
+ this.acpConnection = null;
275
+ this.emit('agent_exit', { code, signal });
276
+ }
277
+ });
278
+
279
+ // Create ACP streams
280
+ const input = Writable.toWeb(newProcess.stdin!);
281
+ const output = Readable.toWeb(newProcess.stdout!);
282
+
283
+ // Create the ACP client connection
284
+ this.amuxClient = new AmuxClient(this.terminalManager, (event) => {
285
+ // Route client events through our emitter
286
+ if (event.type === 'session_update' && event.update) {
287
+ this.emitUpdate( event.update as acp.SessionUpdate);
288
+ } else if (event.type === 'permission_request') {
289
+ this.emit('permission_request', {
290
+ requestId: event.requestId as string,
291
+ toolCall: event.toolCall as acp.ToolCall | undefined,
292
+ options: event.options as acp.PermissionOption[],
293
+ });
294
+ }
295
+ });
296
+ const stream = acp.ndJsonStream(input, output);
297
+ this.acpConnection = new acp.ClientSideConnection(() => this.amuxClient!, stream);
298
+
299
+ try {
300
+ const initResult = await withTimeout(
301
+ this.acpConnection.initialize({
302
+ protocolVersion: acp.PROTOCOL_VERSION,
303
+ clientCapabilities: {
304
+ fs: {
305
+ readTextFile: true,
306
+ writeTextFile: true,
307
+ },
308
+ terminal: true,
309
+ },
310
+ }),
311
+ INIT_TIMEOUT_MS,
312
+ 'Agent initialization'
313
+ );
314
+
315
+ console.log(`[amux] Connected to agent (protocol v${initResult.protocolVersion})`);
316
+ this._agentCapabilities = initResult.agentCapabilities ?? null;
317
+
318
+ const sessionRestore = await this.initializeSession(initResult.agentCapabilities);
319
+
320
+ this.emit('ready', {
321
+ cwd: this._cwd,
322
+ sessionId: this._sessionId,
323
+ sessionRestore,
324
+ capabilities: this._agentCapabilities,
325
+ agent: this.getAgentInfo(),
326
+ availableAgents: this.getAvailableAgents(),
327
+ });
328
+
329
+ } catch (err) {
330
+ console.error('[amux] Failed to initialize agent:', err);
331
+ this.emit('error', { message: `Failed to initialize agent: ${(err as Error).message}` });
332
+ newProcess.kill();
333
+ if (this.agentProcess === newProcess) {
334
+ this.agentProcess = null;
335
+ this.acpConnection = null;
336
+ }
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Initialize or restore session
342
+ */
343
+ private async initializeSession(
344
+ capabilities: acp.AgentCapabilities | undefined
345
+ ): Promise<SessionRestoreInfo | undefined> {
346
+ if (!this.acpConnection) return undefined;
347
+
348
+ const canLoadSession = capabilities?.loadSession === true;
349
+ const hasExistingSession = Boolean(this._sessionId);
350
+ const previousSessionId = this._sessionId;
351
+
352
+ let sessionRestore: SessionRestoreInfo | undefined = undefined;
353
+
354
+ if (this._sessionId && canLoadSession) {
355
+ let loadSucceeded = false;
356
+ try {
357
+ await withTimeout(
358
+ this.acpConnection.loadSession({
359
+ sessionId: this._sessionId,
360
+ cwd: this._cwd,
361
+ mcpServers: [],
362
+ }),
363
+ INIT_TIMEOUT_MS,
364
+ 'Session load'
365
+ );
366
+
367
+ await new Promise(resolve => setTimeout(resolve, 100));
368
+ sessionRestore = { restored: true };
369
+ loadSucceeded = true;
370
+
371
+ if (this._sessionId) {
372
+ this.stateManager.registerSession(this._sessionId, this._cwd, this._agentType);
373
+ }
374
+ } catch (loadErr) {
375
+ console.log(`[amux] Session load failed: ${(loadErr as Error).message}`);
376
+ }
377
+
378
+ if (!loadSucceeded) {
379
+ const sessionResult = await this.createNewSession();
380
+ sessionRestore = await this.replayHistoryFromPreviousSession(previousSessionId);
381
+ this.broadcastModeInfo(sessionResult);
382
+ }
383
+ } else {
384
+ const sessionResult = await this.createNewSession();
385
+
386
+ if (hasExistingSession && previousSessionId) {
387
+ sessionRestore = await this.replayHistoryFromPreviousSession(previousSessionId);
388
+ }
389
+ this.broadcastModeInfo(sessionResult);
390
+ }
391
+
392
+ if (this._systemContext && sessionRestore?.restored !== true) {
393
+ await this.injectSystemContext();
394
+ }
395
+
396
+ return sessionRestore;
397
+ }
398
+
399
+ /**
400
+ * Inject system context as the first message
401
+ */
402
+ private async injectSystemContext(): Promise<void> {
403
+ if (!this._systemContext || !this.acpConnection || !this._sessionId) return;
404
+
405
+ console.log('[amux] Injecting system context');
406
+
407
+ const contextPrompt = `<system-context>\n${this._systemContext}\n</system-context>\n\nI understand. I'm ready to help based on the context provided above.`;
408
+
409
+ try {
410
+ await this.acpConnection.prompt({
411
+ sessionId: this._sessionId,
412
+ prompt: [{ type: 'text', text: contextPrompt }],
413
+ });
414
+ console.log('[amux] System context injected successfully');
415
+ } catch (err) {
416
+ console.error('[amux] Failed to inject system context:', err);
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Replay history events from a previous session
422
+ */
423
+ private async replayHistoryFromPreviousSession(
424
+ previousSessionId: string | null
425
+ ): Promise<SessionRestoreInfo> {
426
+ if (!previousSessionId) {
427
+ return { restored: false, reason: 'No previous session to restore.' };
428
+ }
429
+
430
+ const history = this.stateManager.loadHistory(previousSessionId);
431
+ if (history.length === 0) {
432
+ return { restored: false, reason: 'No history found for previous session.' };
433
+ }
434
+
435
+ this.emit('history_replay', {
436
+ previousSessionId,
437
+ events: history,
438
+ eventCount: history.length,
439
+ });
440
+
441
+ return {
442
+ restored: true,
443
+ reason: `Restored ${history.length} events from previous session.`,
444
+ };
445
+ }
446
+
447
+ /**
448
+ * Create a new session
449
+ */
450
+ private async createNewSession(): Promise<acp.NewSessionResponse> {
451
+ if (!this.acpConnection) {
452
+ throw new Error('No ACP connection');
453
+ }
454
+
455
+ const sessionResult = await withTimeout(
456
+ this.acpConnection.newSession({ cwd: this._cwd, mcpServers: [] }),
457
+ INIT_TIMEOUT_MS,
458
+ 'New session creation'
459
+ );
460
+ this._sessionId = sessionResult.sessionId;
461
+ this.saveState();
462
+
463
+ if (this._sessionId) {
464
+ this.stateManager.registerSession(this._sessionId, this._cwd, this._agentType);
465
+ }
466
+
467
+ return sessionResult;
468
+ }
469
+
470
+ /**
471
+ * Broadcast mode info if available
472
+ */
473
+ private broadcastModeInfo(sessionResult: acp.NewSessionResponse): void {
474
+ if (sessionResult?.modes) {
475
+ this.emit('update', {
476
+ sessionUpdate: 'current_mode_update',
477
+ currentModeId: sessionResult.modes.currentModeId,
478
+ });
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Send a prompt to the agent
484
+ */
485
+ async prompt(message: string): Promise<{ stopReason?: string }> {
486
+ if (!this.acpConnection || !this._sessionId) {
487
+ throw new Error('Agent not ready');
488
+ }
489
+
490
+ console.log('[amux] Sending prompt:', message.slice(0, 100));
491
+
492
+ this.emitTurnMarker('turn_start');
493
+ this.stateManager.incrementSessionMessageCount(this._sessionId);
494
+ this.maybeNameSession(message);
495
+
496
+ // Emit and store user message
497
+ this.emitUpdate( {
498
+ sessionUpdate: 'user_message_chunk',
499
+ content: { type: 'text', text: message },
500
+ });
501
+
502
+ try {
503
+ // Parse @mentions into resource_link blocks
504
+ const content = parseMessageToContentBlocks(message, this._cwd);
505
+ const result = await this.acpConnection.prompt({
506
+ sessionId: this._sessionId,
507
+ prompt: content,
508
+ });
509
+ this.emit('prompt_complete', { stopReason: result.stopReason });
510
+ return { stopReason: result.stopReason };
511
+ } catch (err) {
512
+ console.error('[amux] Prompt error:', err);
513
+ this.emit('error', { message: `Prompt failed: ${(err as Error).message}` });
514
+ throw err;
515
+ } finally {
516
+ this.emitTurnMarker('turn_end');
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Cancel current operation
522
+ */
523
+ async cancel(): Promise<void> {
524
+ if (this.acpConnection && this._sessionId) {
525
+ await this.acpConnection.cancel({ sessionId: this._sessionId });
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Handle permission response from UI
531
+ */
532
+ respondToPermission(requestId: string, optionId: string): void {
533
+ this.amuxClient?.handlePermissionResponse(requestId, optionId);
534
+ }
535
+
536
+ /**
537
+ * List available sessions
538
+ */
539
+ listSessions(): SessionMetadata[] {
540
+ const sessions = this.stateManager.listSessions(this._cwd);
541
+ return sessions.map(s => ({
542
+ ...s,
543
+ isCurrent: s.id === this._sessionId,
544
+ }));
545
+ }
546
+
547
+ /**
548
+ * Load history for current session
549
+ */
550
+ loadHistory(): unknown[] {
551
+ return this.stateManager.loadHistory(this._sessionId);
552
+ }
553
+
554
+ /**
555
+ * Switch to a different session
556
+ */
557
+ async switchSession(sessionId: string): Promise<void> {
558
+ if (!this.acpConnection) {
559
+ throw new Error('Agent not ready');
560
+ }
561
+
562
+ const canLoadSession = this._agentCapabilities?.loadSession === true;
563
+ const canResume = this._agentCapabilities?.sessionCapabilities?.resume !== undefined;
564
+
565
+ if (!canLoadSession && !canResume) {
566
+ throw new Error('Agent does not support session switching');
567
+ }
568
+
569
+ try {
570
+ if (canLoadSession) {
571
+ await withTimeout(
572
+ this.acpConnection.loadSession({
573
+ sessionId,
574
+ cwd: this._cwd,
575
+ mcpServers: [],
576
+ }),
577
+ INIT_TIMEOUT_MS,
578
+ 'Session load'
579
+ );
580
+ } else {
581
+ await withTimeout(
582
+ this.acpConnection.unstable_resumeSession({
583
+ sessionId,
584
+ cwd: this._cwd,
585
+ mcpServers: [],
586
+ }),
587
+ INIT_TIMEOUT_MS,
588
+ 'Session resume'
589
+ );
590
+ }
591
+
592
+ this._sessionId = sessionId;
593
+ this.saveState();
594
+
595
+ this.emit('session_switched', { sessionId: this._sessionId });
596
+
597
+ const history = this.stateManager.loadHistory(sessionId);
598
+ if (history.length > 0) {
599
+ this.emit('history_replay', {
600
+ previousSessionId: sessionId,
601
+ events: history,
602
+ eventCount: history.length,
603
+ });
604
+ }
605
+
606
+ } catch (err) {
607
+ console.error(`[amux] Failed to switch session:`, err);
608
+ throw err;
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Change working directory (requires agent restart)
614
+ */
615
+ async changeCwd(newPath: string): Promise<void> {
616
+ if (this.fixedCwd) {
617
+ throw new Error('Working directory is fixed for this session');
618
+ }
619
+
620
+ this._cwd = newPath;
621
+ this._sessionId = null;
622
+ this.saveState();
623
+
624
+ await this.killAgent();
625
+ await this.spawnAgent();
626
+ }
627
+
628
+ /**
629
+ * Create a new session
630
+ */
631
+ async newSession(): Promise<void> {
632
+ if (!this.acpConnection) {
633
+ throw new Error('No ACP connection');
634
+ }
635
+
636
+ const sessionResult = await this.acpConnection.newSession({
637
+ cwd: this._cwd,
638
+ mcpServers: [],
639
+ });
640
+ this._sessionId = sessionResult.sessionId;
641
+ this.saveState();
642
+
643
+ if (this._sessionId) {
644
+ this.stateManager.registerSession(this._sessionId, this._cwd, this._agentType);
645
+ }
646
+
647
+ this.emit('session_created', { sessionId: this._sessionId });
648
+ this.broadcastModeInfo(sessionResult);
649
+
650
+ if (this._systemContext) {
651
+ await this.injectSystemContext();
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Set session mode
657
+ */
658
+ async setMode(modeId: string): Promise<void> {
659
+ if (!this.acpConnection || !this._sessionId) {
660
+ throw new Error('Agent not ready');
661
+ }
662
+
663
+ await this.acpConnection.setSessionMode({
664
+ sessionId: this._sessionId,
665
+ modeId,
666
+ });
667
+ }
668
+
669
+ /**
670
+ * Set session model
671
+ */
672
+ async setModel(modelId: string): Promise<void> {
673
+ if (!this.acpConnection || !this._sessionId) {
674
+ throw new Error('Agent not ready');
675
+ }
676
+
677
+ await this.acpConnection.unstable_setSessionModel({
678
+ sessionId: this._sessionId,
679
+ modelId,
680
+ });
681
+ }
682
+
683
+ /**
684
+ * Change agent type (requires agent restart)
685
+ */
686
+ async changeAgent(agentType: string): Promise<void> {
687
+ if (!this.agents[agentType]) {
688
+ throw new Error(`Unknown agent type: ${agentType}`);
689
+ }
690
+
691
+ if (agentType !== this._agentType) {
692
+ this._agentType = agentType;
693
+ this._sessionId = null;
694
+ this.saveState();
695
+
696
+ await this.killAgent();
697
+ await this.spawnAgent();
698
+ }
699
+ }
700
+
701
+ /**
702
+ * Kill the agent process
703
+ */
704
+ async killAgent(): Promise<void> {
705
+ if (this.agentProcess) {
706
+ this.agentProcess.kill();
707
+ this.agentProcess = null;
708
+ this.acpConnection = null;
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Shutdown (cleanup all resources)
714
+ */
715
+ shutdown(): void {
716
+ this.terminalManager.killAll();
717
+ this.agentProcess?.kill();
718
+ }
719
+ }