adhdev 0.8.24 → 0.8.27

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.
@@ -238,6 +238,20 @@ var PtySessionRuntime = class {
238
238
  if (!this.ptyProcess) return;
239
239
  this.ptyProcess.kill();
240
240
  }
241
+ sendSignal(signal) {
242
+ if (!this.ptyProcess) throw new Error(`Session not running: ${this.sessionId}`);
243
+ const normalized = String(signal || "").trim().toUpperCase();
244
+ if (!normalized) throw new Error("signal is required");
245
+ try {
246
+ process.kill(this.ptyProcess.pid, normalized);
247
+ } catch {
248
+ if (normalized === "SIGTERM" || normalized === "SIGKILL") {
249
+ this.ptyProcess.kill();
250
+ return;
251
+ }
252
+ throw new Error(`Unsupported signal for runtime ${this.sessionId}: ${normalized}`);
253
+ }
254
+ }
241
255
  getSnapshotText() {
242
256
  return this.screenMirror?.formatVT() || "";
243
257
  }
@@ -306,7 +320,8 @@ var SessionHostStorage = class {
306
320
  };
307
321
 
308
322
  // src/server.ts
309
- var SessionHostServer = class extends EventEmitter {
323
+ var SessionHostServer = class _SessionHostServer extends EventEmitter {
324
+ static MAX_RECENT_DIAGNOSTICS = 200;
310
325
  endpoint;
311
326
  registry = new SessionHostRegistry();
312
327
  runtimes = /* @__PURE__ */ new Map();
@@ -314,6 +329,11 @@ var SessionHostServer = class extends EventEmitter {
314
329
  ipcServer = null;
315
330
  sockets = /* @__PURE__ */ new Set();
316
331
  persistTimers = /* @__PURE__ */ new Map();
332
+ startedAt = Date.now();
333
+ recentLogs = [];
334
+ recentRequests = [];
335
+ recentTransitions = [];
336
+ exitWaiters = /* @__PURE__ */ new Map();
317
337
  constructor(options = {}) {
318
338
  super();
319
339
  this.endpoint = options.endpoint || getDefaultSessionHostEndpoint(options.appName || "adhdev");
@@ -341,12 +361,12 @@ var SessionHostServer = class extends EventEmitter {
341
361
  this.ipcServer?.once("error", reject);
342
362
  this.ipcServer?.listen(this.endpoint.path);
343
363
  });
344
- this.emit("log", `session host endpoint ready: ${this.endpoint.path}`);
364
+ this.recordHostLog("info", `session host endpoint ready: ${this.endpoint.path}`);
345
365
  setTimeout(() => {
346
366
  try {
347
367
  this.restorePersistedRuntimes();
348
368
  } catch (error) {
349
- this.emit("log", `session host restore failed: ${error?.message || String(error)}`);
369
+ this.recordHostLog("error", `session host restore failed: ${error?.message || String(error)}`);
350
370
  }
351
371
  }, 0);
352
372
  }
@@ -387,12 +407,14 @@ var SessionHostServer = class extends EventEmitter {
387
407
  const record = this.registry.createSession(request.payload);
388
408
  this.schedulePersist(record.sessionId);
389
409
  this.emitEvent({ type: "session_created", sessionId: record.sessionId, record });
410
+ this.recordRuntimeTransition(record.sessionId, "create_session", "starting", `provider=${record.providerType}`, true);
390
411
  try {
391
412
  const startedRecord = this.startRuntime(record, request.payload, "session_started");
392
413
  return { success: true, result: startedRecord };
393
414
  } catch (error) {
394
415
  this.registry.markStopped(record.sessionId, "failed");
395
416
  this.persistNow(record.sessionId);
417
+ this.recordRuntimeTransition(record.sessionId, "create_session_failed", "failed", void 0, false, error?.message || String(error));
396
418
  return { success: false, error: error?.message || String(error) };
397
419
  }
398
420
  }
@@ -405,32 +427,39 @@ var SessionHostServer = class extends EventEmitter {
405
427
  if (client) {
406
428
  this.emitEvent({ type: "client_attached", sessionId: record.sessionId, client });
407
429
  }
430
+ this.recordRuntimeTransition(record.sessionId, "attach_client", record.lifecycle, request.payload.clientId, true);
408
431
  return { success: true, result: record };
409
432
  }
410
433
  case "detach_session": {
411
434
  const record = this.registry.detachClient(request.payload);
412
435
  this.schedulePersist(record.sessionId);
413
436
  this.emitEvent({ type: "client_detached", sessionId: record.sessionId, clientId: request.payload.clientId });
437
+ this.recordRuntimeTransition(record.sessionId, "detach_client", record.lifecycle, request.payload.clientId, true);
414
438
  return { success: true, result: record };
415
439
  }
416
440
  case "acquire_write": {
417
441
  const record = this.registry.acquireWrite(request.payload);
418
442
  this.persistNow(record.sessionId);
419
443
  this.emitEvent({ type: "write_owner_changed", sessionId: record.sessionId, owner: record.writeOwner });
444
+ this.recordRuntimeTransition(record.sessionId, "acquire_write", record.lifecycle, request.payload.clientId, true);
420
445
  return { success: true, result: record };
421
446
  }
422
447
  case "release_write": {
423
448
  const record = this.registry.releaseWrite(request.payload);
424
449
  this.persistNow(record.sessionId);
425
450
  this.emitEvent({ type: "write_owner_changed", sessionId: record.sessionId, owner: record.writeOwner });
451
+ this.recordRuntimeTransition(record.sessionId, "release_write", record.lifecycle, request.payload.clientId, true);
426
452
  return { success: true, result: record };
427
453
  }
428
454
  case "get_snapshot":
429
455
  return { success: true, result: this.getSnapshot(request.payload.sessionId, request.payload.sinceSeq) };
456
+ case "get_host_diagnostics":
457
+ return { success: true, result: this.getHostDiagnostics(request.payload) };
430
458
  case "clear_session_buffer": {
431
459
  const record = this.registry.clearBuffer(request.payload.sessionId);
432
460
  this.persistNow(record.sessionId);
433
461
  this.emitEvent({ type: "session_cleared", sessionId: record.sessionId });
462
+ this.recordRuntimeTransition(record.sessionId, "clear_buffer", record.lifecycle, void 0, true);
434
463
  return { success: true, result: record };
435
464
  }
436
465
  case "update_session_meta": {
@@ -440,6 +469,7 @@ var SessionHostServer = class extends EventEmitter {
440
469
  request.payload.replace === true
441
470
  );
442
471
  this.persistNow(record.sessionId);
472
+ this.recordRuntimeTransition(record.sessionId, "update_meta", record.lifecycle, void 0, true);
443
473
  return { success: true, result: record };
444
474
  }
445
475
  case "send_input": {
@@ -484,6 +514,7 @@ var SessionHostServer = class extends EventEmitter {
484
514
  this.persistNow(request.payload.sessionId);
485
515
  this.requireRuntime(request.payload.sessionId).stop();
486
516
  this.emitEvent({ type: "session_stopped", sessionId: request.payload.sessionId });
517
+ this.recordRuntimeTransition(request.payload.sessionId, "stop_session", "stopping", void 0, true);
487
518
  return { success: true, result: this.registry.getSession(request.payload.sessionId) };
488
519
  }
489
520
  case "resume_session": {
@@ -495,8 +526,38 @@ var SessionHostServer = class extends EventEmitter {
495
526
  return { success: true, result: existing };
496
527
  }
497
528
  const resumed = this.startRuntime(existing, this.buildPayloadFromRecord(existing), "session_resumed");
529
+ this.recordRuntimeTransition(request.payload.sessionId, "resume_session", resumed.lifecycle, void 0, true);
498
530
  return { success: true, result: resumed };
499
531
  }
532
+ case "restart_session": {
533
+ const restarted = await this.restartRuntime(request.payload.sessionId);
534
+ return { success: true, result: restarted };
535
+ }
536
+ case "send_signal": {
537
+ const runtime = this.requireRuntime(request.payload.sessionId);
538
+ runtime.sendSignal(request.payload.signal);
539
+ const record = this.registry.getSession(request.payload.sessionId);
540
+ this.recordRuntimeTransition(request.payload.sessionId, "send_signal", record?.lifecycle, request.payload.signal, true);
541
+ return { success: true, result: record };
542
+ }
543
+ case "force_detach_client": {
544
+ const session = this.registry.getSession(request.payload.sessionId);
545
+ if (session?.writeOwner?.clientId === request.payload.clientId) {
546
+ const released = this.registry.releaseWrite({
547
+ sessionId: request.payload.sessionId,
548
+ clientId: request.payload.clientId
549
+ });
550
+ this.emitEvent({ type: "write_owner_changed", sessionId: released.sessionId, owner: released.writeOwner });
551
+ }
552
+ const record = this.registry.detachClient({
553
+ sessionId: request.payload.sessionId,
554
+ clientId: request.payload.clientId
555
+ });
556
+ this.schedulePersist(record.sessionId);
557
+ this.emitEvent({ type: "client_detached", sessionId: record.sessionId, clientId: request.payload.clientId });
558
+ this.recordRuntimeTransition(record.sessionId, "force_detach_client", record.lifecycle, request.payload.clientId, true);
559
+ return { success: true, result: record };
560
+ }
500
561
  default:
501
562
  return { success: false, error: `Unsupported session host request: ${request?.type || "unknown"}` };
502
563
  }
@@ -523,7 +584,18 @@ var SessionHostServer = class extends EventEmitter {
523
584
  this.emit("event", event);
524
585
  }
525
586
  async handleIncomingRequest(socket, envelope) {
587
+ const startedAt = Date.now();
526
588
  const response = await this.handleRequest(envelope.request);
589
+ this.recordRequestTrace({
590
+ timestamp: startedAt,
591
+ requestId: envelope.requestId,
592
+ type: envelope.request.type,
593
+ sessionId: this.getRequestSessionId(envelope.request),
594
+ clientId: this.getRequestClientId(envelope.request),
595
+ success: response.success,
596
+ durationMs: Math.max(0, Date.now() - startedAt),
597
+ error: response.success ? void 0 : response.error
598
+ });
527
599
  writeEnvelope(socket, createResponseEnvelope(envelope.requestId, response));
528
600
  }
529
601
  schedulePersist(sessionId) {
@@ -540,6 +612,99 @@ var SessionHostServer = class extends EventEmitter {
540
612
  const snapshot = this.getSnapshot(sessionId);
541
613
  this.storage.save(record, snapshot);
542
614
  }
615
+ getHostDiagnostics(payload) {
616
+ const limit = Math.max(1, Math.min(200, Number(payload?.limit) || 50));
617
+ return {
618
+ hostStartedAt: this.startedAt,
619
+ endpoint: this.endpoint.path,
620
+ runtimeCount: this.runtimes.size,
621
+ sessions: payload?.includeSessions === false ? void 0 : this.registry.listSessions(),
622
+ recentLogs: this.recentLogs.slice(-limit),
623
+ recentRequests: this.recentRequests.slice(-limit),
624
+ recentTransitions: this.recentTransitions.slice(-limit)
625
+ };
626
+ }
627
+ getRequestSessionId(request) {
628
+ const payload = request.payload;
629
+ return typeof payload?.sessionId === "string" ? payload.sessionId : void 0;
630
+ }
631
+ getRequestClientId(request) {
632
+ const payload = request.payload;
633
+ return typeof payload?.clientId === "string" ? payload.clientId : void 0;
634
+ }
635
+ pushRecent(bucket, entry) {
636
+ bucket.push(entry);
637
+ if (bucket.length > _SessionHostServer.MAX_RECENT_DIAGNOSTICS) {
638
+ bucket.splice(0, bucket.length - _SessionHostServer.MAX_RECENT_DIAGNOSTICS);
639
+ }
640
+ }
641
+ recordHostLog(level, message, sessionId, data) {
642
+ const entry = {
643
+ timestamp: Date.now(),
644
+ level,
645
+ message,
646
+ sessionId,
647
+ data
648
+ };
649
+ this.pushRecent(this.recentLogs, entry);
650
+ this.emitEvent({ type: "host_log", entry });
651
+ this.emit("log", `[${level}] ${message}`);
652
+ }
653
+ recordRequestTrace(trace) {
654
+ this.pushRecent(this.recentRequests, trace);
655
+ this.emitEvent({ type: "request_trace", trace });
656
+ if (!trace.success) {
657
+ this.recordHostLog(
658
+ "warn",
659
+ `request ${trace.type} failed after ${trace.durationMs}ms${trace.error ? `: ${trace.error}` : ""}`,
660
+ trace.sessionId,
661
+ { requestId: trace.requestId, clientId: trace.clientId }
662
+ );
663
+ }
664
+ }
665
+ recordRuntimeTransition(sessionId, action, lifecycle, detail, success = true, error) {
666
+ const transition = {
667
+ timestamp: Date.now(),
668
+ sessionId,
669
+ action,
670
+ lifecycle,
671
+ detail,
672
+ success,
673
+ error
674
+ };
675
+ this.pushRecent(this.recentTransitions, transition);
676
+ this.emitEvent({ type: "runtime_transition", transition });
677
+ }
678
+ waitForRuntimeExit(sessionId, timeoutMs = 5e3) {
679
+ if (!this.runtimes.has(sessionId)) {
680
+ return Promise.resolve(this.registry.getSession(sessionId)?.lifecycle === "failed" ? 1 : 0);
681
+ }
682
+ return new Promise((resolve, reject) => {
683
+ const timeout = setTimeout(() => {
684
+ const waiters2 = this.exitWaiters.get(sessionId) || [];
685
+ this.exitWaiters.set(sessionId, waiters2.filter((waiter) => waiter !== onExit));
686
+ reject(new Error(`Timed out waiting for runtime ${sessionId} to exit`));
687
+ }, timeoutMs);
688
+ const onExit = (exitCode) => {
689
+ clearTimeout(timeout);
690
+ resolve(exitCode);
691
+ };
692
+ const waiters = this.exitWaiters.get(sessionId) || [];
693
+ waiters.push(onExit);
694
+ this.exitWaiters.set(sessionId, waiters);
695
+ });
696
+ }
697
+ resolveExitWaiters(sessionId, exitCode) {
698
+ const waiters = this.exitWaiters.get(sessionId);
699
+ if (!waiters?.length) return;
700
+ this.exitWaiters.delete(sessionId);
701
+ for (const waiter of waiters) {
702
+ try {
703
+ waiter(exitCode);
704
+ } catch {
705
+ }
706
+ }
707
+ }
543
708
  getSnapshot(sessionId, sinceSeq) {
544
709
  const snapshot = this.registry.getSnapshot(sessionId, sinceSeq);
545
710
  const record = this.registry.getSession(sessionId);
@@ -575,6 +740,23 @@ var SessionHostServer = class extends EventEmitter {
575
740
  this.persistNow(record.sessionId);
576
741
  }
577
742
  }
743
+ async restartRuntime(sessionId) {
744
+ const existing = this.registry.getSession(sessionId);
745
+ if (!existing) {
746
+ throw new Error(`Unknown session: ${sessionId}`);
747
+ }
748
+ if (this.runtimes.has(sessionId)) {
749
+ this.registry.setLifecycle(sessionId, "stopping");
750
+ this.persistNow(sessionId);
751
+ this.recordRuntimeTransition(sessionId, "restart_requested", "stopping", void 0, true);
752
+ this.requireRuntime(sessionId).stop();
753
+ await this.waitForRuntimeExit(sessionId);
754
+ }
755
+ const latest = this.registry.getSession(sessionId) || existing;
756
+ const restarted = this.startRuntime(latest, this.buildPayloadFromRecord(latest), "session_resumed");
757
+ this.recordRuntimeTransition(sessionId, "restart_completed", restarted.lifecycle, void 0, true);
758
+ return restarted;
759
+ }
578
760
  restorePersistedRuntimes() {
579
761
  const states = this.storage.loadAll();
580
762
  const runtimesToResume = [];
@@ -607,7 +789,7 @@ var SessionHostServer = class extends EventEmitter {
607
789
  }
608
790
  }
609
791
  if (skippedOrphanLiveSessions > 0) {
610
- this.emit("log", `session host skipped ${skippedOrphanLiveSessions} orphan live runtime(s) during restore`);
792
+ this.recordHostLog("warn", `session host skipped ${skippedOrphanLiveSessions} orphan live runtime(s) during restore`);
611
793
  }
612
794
  for (const { persisted, recoveredRecord } of runtimesToResume) {
613
795
  try {
@@ -626,6 +808,7 @@ var SessionHostServer = class extends EventEmitter {
626
808
  this.registry.getSnapshot(resumed.sessionId)
627
809
  );
628
810
  this.persistNow(resumed.sessionId);
811
+ this.recordRuntimeTransition(resumed.sessionId, "restore_auto_resumed", resumed.lifecycle, void 0, true);
629
812
  } catch (error) {
630
813
  const interrupted = this.registry.setLifecycle(recoveredRecord.sessionId, "interrupted");
631
814
  this.registry.restoreSession({
@@ -638,6 +821,8 @@ var SessionHostServer = class extends EventEmitter {
638
821
  }
639
822
  }, persisted.snapshot);
640
823
  this.persistNow(recoveredRecord.sessionId);
824
+ this.recordRuntimeTransition(recoveredRecord.sessionId, "restore_resume_failed", "interrupted", void 0, false, error?.message || String(error));
825
+ this.recordHostLog("error", `restore resume failed for ${recoveredRecord.sessionId}: ${error?.message || String(error)}`, recoveredRecord.sessionId);
641
826
  }
642
827
  }
643
828
  }
@@ -667,8 +852,17 @@ var SessionHostServer = class extends EventEmitter {
667
852
  onExit: (exitCode) => {
668
853
  this.registry.markStopped(record.sessionId, exitCode === 0 ? "stopped" : "failed");
669
854
  this.runtimes.delete(record.sessionId);
855
+ this.resolveExitWaiters(record.sessionId, exitCode);
670
856
  this.persistNow(record.sessionId);
671
857
  this.emitEvent({ type: "session_exit", sessionId: record.sessionId, exitCode });
858
+ this.recordRuntimeTransition(
859
+ record.sessionId,
860
+ "session_exit",
861
+ exitCode === 0 ? "stopped" : "failed",
862
+ void 0,
863
+ exitCode === 0,
864
+ exitCode === 0 ? void 0 : `exitCode=${exitCode}`
865
+ );
672
866
  setTimeout(() => this.storage.remove(record.sessionId), 5e3);
673
867
  }
674
868
  });
@@ -678,6 +872,7 @@ var SessionHostServer = class extends EventEmitter {
678
872
  const startedRecord = this.registry.markStarted(record.sessionId, pid);
679
873
  this.persistNow(record.sessionId);
680
874
  this.emitEvent({ type: startEventType, sessionId: record.sessionId, pid });
875
+ this.recordRuntimeTransition(record.sessionId, startEventType, startedRecord.lifecycle, `pid=${pid}`, true);
681
876
  return startedRecord;
682
877
  }
683
878
  };
@@ -939,6 +1134,7 @@ async function attachRuntime(target, readOnly = false, takeover = false) {
939
1134
  return { signal, handler };
940
1135
  });
941
1136
  const unsubscribe = client.onEvent((event) => {
1137
+ if (!("sessionId" in event)) return;
942
1138
  if (event.sessionId !== runtimeId) return;
943
1139
  if (event.type === "session_output") {
944
1140
  if (event.seq <= lastSeq) return;