@bytespell/amux 0.0.11 → 0.0.13

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