adhdev 0.8.25 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adhdev",
3
- "version": "0.8.25",
3
+ "version": "0.8.27",
4
4
  "description": "ADHDev — Agent Dashboard Hub for Dev. Remote-control AI coding agents from anywhere.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -7,6 +7,7 @@ interface SessionHostServerOptions {
7
7
  appName?: string;
8
8
  }
9
9
  declare class SessionHostServer extends EventEmitter {
10
+ private static readonly MAX_RECENT_DIAGNOSTICS;
10
11
  readonly endpoint: SessionHostEndpoint;
11
12
  readonly registry: SessionHostRegistry;
12
13
  private runtimes;
@@ -14,6 +15,11 @@ declare class SessionHostServer extends EventEmitter {
14
15
  private ipcServer;
15
16
  private sockets;
16
17
  private persistTimers;
18
+ private readonly startedAt;
19
+ private recentLogs;
20
+ private recentRequests;
21
+ private recentTransitions;
22
+ private exitWaiters;
17
23
  constructor(options?: SessionHostServerOptions);
18
24
  start(): Promise<void>;
19
25
  stop(): Promise<void>;
@@ -24,8 +30,18 @@ declare class SessionHostServer extends EventEmitter {
24
30
  private handleIncomingRequest;
25
31
  private schedulePersist;
26
32
  private persistNow;
33
+ private getHostDiagnostics;
34
+ private getRequestSessionId;
35
+ private getRequestClientId;
36
+ private pushRecent;
37
+ private recordHostLog;
38
+ private recordRequestTrace;
39
+ private recordRuntimeTransition;
40
+ private waitForRuntimeExit;
41
+ private resolveExitWaiters;
27
42
  private getSnapshot;
28
43
  flushAllPersistence(): void;
44
+ private restartRuntime;
29
45
  private restorePersistedRuntimes;
30
46
  private buildPayloadFromRecord;
31
47
  private startRuntime;
@@ -7,6 +7,7 @@ interface SessionHostServerOptions {
7
7
  appName?: string;
8
8
  }
9
9
  declare class SessionHostServer extends EventEmitter {
10
+ private static readonly MAX_RECENT_DIAGNOSTICS;
10
11
  readonly endpoint: SessionHostEndpoint;
11
12
  readonly registry: SessionHostRegistry;
12
13
  private runtimes;
@@ -14,6 +15,11 @@ declare class SessionHostServer extends EventEmitter {
14
15
  private ipcServer;
15
16
  private sockets;
16
17
  private persistTimers;
18
+ private readonly startedAt;
19
+ private recentLogs;
20
+ private recentRequests;
21
+ private recentTransitions;
22
+ private exitWaiters;
17
23
  constructor(options?: SessionHostServerOptions);
18
24
  start(): Promise<void>;
19
25
  stop(): Promise<void>;
@@ -24,8 +30,18 @@ declare class SessionHostServer extends EventEmitter {
24
30
  private handleIncomingRequest;
25
31
  private schedulePersist;
26
32
  private persistNow;
33
+ private getHostDiagnostics;
34
+ private getRequestSessionId;
35
+ private getRequestClientId;
36
+ private pushRecent;
37
+ private recordHostLog;
38
+ private recordRequestTrace;
39
+ private recordRuntimeTransition;
40
+ private waitForRuntimeExit;
41
+ private resolveExitWaiters;
27
42
  private getSnapshot;
28
43
  flushAllPersistence(): void;
44
+ private restartRuntime;
29
45
  private restorePersistedRuntimes;
30
46
  private buildPayloadFromRecord;
31
47
  private startRuntime;
@@ -254,6 +254,20 @@ var PtySessionRuntime = class {
254
254
  if (!this.ptyProcess) return;
255
255
  this.ptyProcess.kill();
256
256
  }
257
+ sendSignal(signal) {
258
+ if (!this.ptyProcess) throw new Error(`Session not running: ${this.sessionId}`);
259
+ const normalized = String(signal || "").trim().toUpperCase();
260
+ if (!normalized) throw new Error("signal is required");
261
+ try {
262
+ process.kill(this.ptyProcess.pid, normalized);
263
+ } catch {
264
+ if (normalized === "SIGTERM" || normalized === "SIGKILL") {
265
+ this.ptyProcess.kill();
266
+ return;
267
+ }
268
+ throw new Error(`Unsupported signal for runtime ${this.sessionId}: ${normalized}`);
269
+ }
270
+ }
257
271
  getSnapshotText() {
258
272
  return this.screenMirror?.formatVT() || "";
259
273
  }
@@ -322,7 +336,8 @@ var SessionHostStorage = class {
322
336
  };
323
337
 
324
338
  // src/server.ts
325
- var SessionHostServer = class extends import_events.EventEmitter {
339
+ var SessionHostServer = class _SessionHostServer extends import_events.EventEmitter {
340
+ static MAX_RECENT_DIAGNOSTICS = 200;
326
341
  endpoint;
327
342
  registry = new import_session_host_core2.SessionHostRegistry();
328
343
  runtimes = /* @__PURE__ */ new Map();
@@ -330,6 +345,11 @@ var SessionHostServer = class extends import_events.EventEmitter {
330
345
  ipcServer = null;
331
346
  sockets = /* @__PURE__ */ new Set();
332
347
  persistTimers = /* @__PURE__ */ new Map();
348
+ startedAt = Date.now();
349
+ recentLogs = [];
350
+ recentRequests = [];
351
+ recentTransitions = [];
352
+ exitWaiters = /* @__PURE__ */ new Map();
333
353
  constructor(options = {}) {
334
354
  super();
335
355
  this.endpoint = options.endpoint || (0, import_session_host_core2.getDefaultSessionHostEndpoint)(options.appName || "adhdev");
@@ -357,12 +377,12 @@ var SessionHostServer = class extends import_events.EventEmitter {
357
377
  this.ipcServer?.once("error", reject);
358
378
  this.ipcServer?.listen(this.endpoint.path);
359
379
  });
360
- this.emit("log", `session host endpoint ready: ${this.endpoint.path}`);
380
+ this.recordHostLog("info", `session host endpoint ready: ${this.endpoint.path}`);
361
381
  setTimeout(() => {
362
382
  try {
363
383
  this.restorePersistedRuntimes();
364
384
  } catch (error) {
365
- this.emit("log", `session host restore failed: ${error?.message || String(error)}`);
385
+ this.recordHostLog("error", `session host restore failed: ${error?.message || String(error)}`);
366
386
  }
367
387
  }, 0);
368
388
  }
@@ -403,12 +423,14 @@ var SessionHostServer = class extends import_events.EventEmitter {
403
423
  const record = this.registry.createSession(request.payload);
404
424
  this.schedulePersist(record.sessionId);
405
425
  this.emitEvent({ type: "session_created", sessionId: record.sessionId, record });
426
+ this.recordRuntimeTransition(record.sessionId, "create_session", "starting", `provider=${record.providerType}`, true);
406
427
  try {
407
428
  const startedRecord = this.startRuntime(record, request.payload, "session_started");
408
429
  return { success: true, result: startedRecord };
409
430
  } catch (error) {
410
431
  this.registry.markStopped(record.sessionId, "failed");
411
432
  this.persistNow(record.sessionId);
433
+ this.recordRuntimeTransition(record.sessionId, "create_session_failed", "failed", void 0, false, error?.message || String(error));
412
434
  return { success: false, error: error?.message || String(error) };
413
435
  }
414
436
  }
@@ -421,32 +443,39 @@ var SessionHostServer = class extends import_events.EventEmitter {
421
443
  if (client) {
422
444
  this.emitEvent({ type: "client_attached", sessionId: record.sessionId, client });
423
445
  }
446
+ this.recordRuntimeTransition(record.sessionId, "attach_client", record.lifecycle, request.payload.clientId, true);
424
447
  return { success: true, result: record };
425
448
  }
426
449
  case "detach_session": {
427
450
  const record = this.registry.detachClient(request.payload);
428
451
  this.schedulePersist(record.sessionId);
429
452
  this.emitEvent({ type: "client_detached", sessionId: record.sessionId, clientId: request.payload.clientId });
453
+ this.recordRuntimeTransition(record.sessionId, "detach_client", record.lifecycle, request.payload.clientId, true);
430
454
  return { success: true, result: record };
431
455
  }
432
456
  case "acquire_write": {
433
457
  const record = this.registry.acquireWrite(request.payload);
434
458
  this.persistNow(record.sessionId);
435
459
  this.emitEvent({ type: "write_owner_changed", sessionId: record.sessionId, owner: record.writeOwner });
460
+ this.recordRuntimeTransition(record.sessionId, "acquire_write", record.lifecycle, request.payload.clientId, true);
436
461
  return { success: true, result: record };
437
462
  }
438
463
  case "release_write": {
439
464
  const record = this.registry.releaseWrite(request.payload);
440
465
  this.persistNow(record.sessionId);
441
466
  this.emitEvent({ type: "write_owner_changed", sessionId: record.sessionId, owner: record.writeOwner });
467
+ this.recordRuntimeTransition(record.sessionId, "release_write", record.lifecycle, request.payload.clientId, true);
442
468
  return { success: true, result: record };
443
469
  }
444
470
  case "get_snapshot":
445
471
  return { success: true, result: this.getSnapshot(request.payload.sessionId, request.payload.sinceSeq) };
472
+ case "get_host_diagnostics":
473
+ return { success: true, result: this.getHostDiagnostics(request.payload) };
446
474
  case "clear_session_buffer": {
447
475
  const record = this.registry.clearBuffer(request.payload.sessionId);
448
476
  this.persistNow(record.sessionId);
449
477
  this.emitEvent({ type: "session_cleared", sessionId: record.sessionId });
478
+ this.recordRuntimeTransition(record.sessionId, "clear_buffer", record.lifecycle, void 0, true);
450
479
  return { success: true, result: record };
451
480
  }
452
481
  case "update_session_meta": {
@@ -456,6 +485,7 @@ var SessionHostServer = class extends import_events.EventEmitter {
456
485
  request.payload.replace === true
457
486
  );
458
487
  this.persistNow(record.sessionId);
488
+ this.recordRuntimeTransition(record.sessionId, "update_meta", record.lifecycle, void 0, true);
459
489
  return { success: true, result: record };
460
490
  }
461
491
  case "send_input": {
@@ -500,6 +530,7 @@ var SessionHostServer = class extends import_events.EventEmitter {
500
530
  this.persistNow(request.payload.sessionId);
501
531
  this.requireRuntime(request.payload.sessionId).stop();
502
532
  this.emitEvent({ type: "session_stopped", sessionId: request.payload.sessionId });
533
+ this.recordRuntimeTransition(request.payload.sessionId, "stop_session", "stopping", void 0, true);
503
534
  return { success: true, result: this.registry.getSession(request.payload.sessionId) };
504
535
  }
505
536
  case "resume_session": {
@@ -511,8 +542,38 @@ var SessionHostServer = class extends import_events.EventEmitter {
511
542
  return { success: true, result: existing };
512
543
  }
513
544
  const resumed = this.startRuntime(existing, this.buildPayloadFromRecord(existing), "session_resumed");
545
+ this.recordRuntimeTransition(request.payload.sessionId, "resume_session", resumed.lifecycle, void 0, true);
514
546
  return { success: true, result: resumed };
515
547
  }
548
+ case "restart_session": {
549
+ const restarted = await this.restartRuntime(request.payload.sessionId);
550
+ return { success: true, result: restarted };
551
+ }
552
+ case "send_signal": {
553
+ const runtime = this.requireRuntime(request.payload.sessionId);
554
+ runtime.sendSignal(request.payload.signal);
555
+ const record = this.registry.getSession(request.payload.sessionId);
556
+ this.recordRuntimeTransition(request.payload.sessionId, "send_signal", record?.lifecycle, request.payload.signal, true);
557
+ return { success: true, result: record };
558
+ }
559
+ case "force_detach_client": {
560
+ const session = this.registry.getSession(request.payload.sessionId);
561
+ if (session?.writeOwner?.clientId === request.payload.clientId) {
562
+ const released = this.registry.releaseWrite({
563
+ sessionId: request.payload.sessionId,
564
+ clientId: request.payload.clientId
565
+ });
566
+ this.emitEvent({ type: "write_owner_changed", sessionId: released.sessionId, owner: released.writeOwner });
567
+ }
568
+ const record = this.registry.detachClient({
569
+ sessionId: request.payload.sessionId,
570
+ clientId: request.payload.clientId
571
+ });
572
+ this.schedulePersist(record.sessionId);
573
+ this.emitEvent({ type: "client_detached", sessionId: record.sessionId, clientId: request.payload.clientId });
574
+ this.recordRuntimeTransition(record.sessionId, "force_detach_client", record.lifecycle, request.payload.clientId, true);
575
+ return { success: true, result: record };
576
+ }
516
577
  default:
517
578
  return { success: false, error: `Unsupported session host request: ${request?.type || "unknown"}` };
518
579
  }
@@ -539,7 +600,18 @@ var SessionHostServer = class extends import_events.EventEmitter {
539
600
  this.emit("event", event);
540
601
  }
541
602
  async handleIncomingRequest(socket, envelope) {
603
+ const startedAt = Date.now();
542
604
  const response = await this.handleRequest(envelope.request);
605
+ this.recordRequestTrace({
606
+ timestamp: startedAt,
607
+ requestId: envelope.requestId,
608
+ type: envelope.request.type,
609
+ sessionId: this.getRequestSessionId(envelope.request),
610
+ clientId: this.getRequestClientId(envelope.request),
611
+ success: response.success,
612
+ durationMs: Math.max(0, Date.now() - startedAt),
613
+ error: response.success ? void 0 : response.error
614
+ });
543
615
  (0, import_session_host_core2.writeEnvelope)(socket, (0, import_session_host_core2.createResponseEnvelope)(envelope.requestId, response));
544
616
  }
545
617
  schedulePersist(sessionId) {
@@ -556,6 +628,99 @@ var SessionHostServer = class extends import_events.EventEmitter {
556
628
  const snapshot = this.getSnapshot(sessionId);
557
629
  this.storage.save(record, snapshot);
558
630
  }
631
+ getHostDiagnostics(payload) {
632
+ const limit = Math.max(1, Math.min(200, Number(payload?.limit) || 50));
633
+ return {
634
+ hostStartedAt: this.startedAt,
635
+ endpoint: this.endpoint.path,
636
+ runtimeCount: this.runtimes.size,
637
+ sessions: payload?.includeSessions === false ? void 0 : this.registry.listSessions(),
638
+ recentLogs: this.recentLogs.slice(-limit),
639
+ recentRequests: this.recentRequests.slice(-limit),
640
+ recentTransitions: this.recentTransitions.slice(-limit)
641
+ };
642
+ }
643
+ getRequestSessionId(request) {
644
+ const payload = request.payload;
645
+ return typeof payload?.sessionId === "string" ? payload.sessionId : void 0;
646
+ }
647
+ getRequestClientId(request) {
648
+ const payload = request.payload;
649
+ return typeof payload?.clientId === "string" ? payload.clientId : void 0;
650
+ }
651
+ pushRecent(bucket, entry) {
652
+ bucket.push(entry);
653
+ if (bucket.length > _SessionHostServer.MAX_RECENT_DIAGNOSTICS) {
654
+ bucket.splice(0, bucket.length - _SessionHostServer.MAX_RECENT_DIAGNOSTICS);
655
+ }
656
+ }
657
+ recordHostLog(level, message, sessionId, data) {
658
+ const entry = {
659
+ timestamp: Date.now(),
660
+ level,
661
+ message,
662
+ sessionId,
663
+ data
664
+ };
665
+ this.pushRecent(this.recentLogs, entry);
666
+ this.emitEvent({ type: "host_log", entry });
667
+ this.emit("log", `[${level}] ${message}`);
668
+ }
669
+ recordRequestTrace(trace) {
670
+ this.pushRecent(this.recentRequests, trace);
671
+ this.emitEvent({ type: "request_trace", trace });
672
+ if (!trace.success) {
673
+ this.recordHostLog(
674
+ "warn",
675
+ `request ${trace.type} failed after ${trace.durationMs}ms${trace.error ? `: ${trace.error}` : ""}`,
676
+ trace.sessionId,
677
+ { requestId: trace.requestId, clientId: trace.clientId }
678
+ );
679
+ }
680
+ }
681
+ recordRuntimeTransition(sessionId, action, lifecycle, detail, success = true, error) {
682
+ const transition = {
683
+ timestamp: Date.now(),
684
+ sessionId,
685
+ action,
686
+ lifecycle,
687
+ detail,
688
+ success,
689
+ error
690
+ };
691
+ this.pushRecent(this.recentTransitions, transition);
692
+ this.emitEvent({ type: "runtime_transition", transition });
693
+ }
694
+ waitForRuntimeExit(sessionId, timeoutMs = 5e3) {
695
+ if (!this.runtimes.has(sessionId)) {
696
+ return Promise.resolve(this.registry.getSession(sessionId)?.lifecycle === "failed" ? 1 : 0);
697
+ }
698
+ return new Promise((resolve, reject) => {
699
+ const timeout = setTimeout(() => {
700
+ const waiters2 = this.exitWaiters.get(sessionId) || [];
701
+ this.exitWaiters.set(sessionId, waiters2.filter((waiter) => waiter !== onExit));
702
+ reject(new Error(`Timed out waiting for runtime ${sessionId} to exit`));
703
+ }, timeoutMs);
704
+ const onExit = (exitCode) => {
705
+ clearTimeout(timeout);
706
+ resolve(exitCode);
707
+ };
708
+ const waiters = this.exitWaiters.get(sessionId) || [];
709
+ waiters.push(onExit);
710
+ this.exitWaiters.set(sessionId, waiters);
711
+ });
712
+ }
713
+ resolveExitWaiters(sessionId, exitCode) {
714
+ const waiters = this.exitWaiters.get(sessionId);
715
+ if (!waiters?.length) return;
716
+ this.exitWaiters.delete(sessionId);
717
+ for (const waiter of waiters) {
718
+ try {
719
+ waiter(exitCode);
720
+ } catch {
721
+ }
722
+ }
723
+ }
559
724
  getSnapshot(sessionId, sinceSeq) {
560
725
  const snapshot = this.registry.getSnapshot(sessionId, sinceSeq);
561
726
  const record = this.registry.getSession(sessionId);
@@ -591,6 +756,23 @@ var SessionHostServer = class extends import_events.EventEmitter {
591
756
  this.persistNow(record.sessionId);
592
757
  }
593
758
  }
759
+ async restartRuntime(sessionId) {
760
+ const existing = this.registry.getSession(sessionId);
761
+ if (!existing) {
762
+ throw new Error(`Unknown session: ${sessionId}`);
763
+ }
764
+ if (this.runtimes.has(sessionId)) {
765
+ this.registry.setLifecycle(sessionId, "stopping");
766
+ this.persistNow(sessionId);
767
+ this.recordRuntimeTransition(sessionId, "restart_requested", "stopping", void 0, true);
768
+ this.requireRuntime(sessionId).stop();
769
+ await this.waitForRuntimeExit(sessionId);
770
+ }
771
+ const latest = this.registry.getSession(sessionId) || existing;
772
+ const restarted = this.startRuntime(latest, this.buildPayloadFromRecord(latest), "session_resumed");
773
+ this.recordRuntimeTransition(sessionId, "restart_completed", restarted.lifecycle, void 0, true);
774
+ return restarted;
775
+ }
594
776
  restorePersistedRuntimes() {
595
777
  const states = this.storage.loadAll();
596
778
  const runtimesToResume = [];
@@ -623,7 +805,7 @@ var SessionHostServer = class extends import_events.EventEmitter {
623
805
  }
624
806
  }
625
807
  if (skippedOrphanLiveSessions > 0) {
626
- this.emit("log", `session host skipped ${skippedOrphanLiveSessions} orphan live runtime(s) during restore`);
808
+ this.recordHostLog("warn", `session host skipped ${skippedOrphanLiveSessions} orphan live runtime(s) during restore`);
627
809
  }
628
810
  for (const { persisted, recoveredRecord } of runtimesToResume) {
629
811
  try {
@@ -642,6 +824,7 @@ var SessionHostServer = class extends import_events.EventEmitter {
642
824
  this.registry.getSnapshot(resumed.sessionId)
643
825
  );
644
826
  this.persistNow(resumed.sessionId);
827
+ this.recordRuntimeTransition(resumed.sessionId, "restore_auto_resumed", resumed.lifecycle, void 0, true);
645
828
  } catch (error) {
646
829
  const interrupted = this.registry.setLifecycle(recoveredRecord.sessionId, "interrupted");
647
830
  this.registry.restoreSession({
@@ -654,6 +837,8 @@ var SessionHostServer = class extends import_events.EventEmitter {
654
837
  }
655
838
  }, persisted.snapshot);
656
839
  this.persistNow(recoveredRecord.sessionId);
840
+ this.recordRuntimeTransition(recoveredRecord.sessionId, "restore_resume_failed", "interrupted", void 0, false, error?.message || String(error));
841
+ this.recordHostLog("error", `restore resume failed for ${recoveredRecord.sessionId}: ${error?.message || String(error)}`, recoveredRecord.sessionId);
657
842
  }
658
843
  }
659
844
  }
@@ -683,8 +868,17 @@ var SessionHostServer = class extends import_events.EventEmitter {
683
868
  onExit: (exitCode) => {
684
869
  this.registry.markStopped(record.sessionId, exitCode === 0 ? "stopped" : "failed");
685
870
  this.runtimes.delete(record.sessionId);
871
+ this.resolveExitWaiters(record.sessionId, exitCode);
686
872
  this.persistNow(record.sessionId);
687
873
  this.emitEvent({ type: "session_exit", sessionId: record.sessionId, exitCode });
874
+ this.recordRuntimeTransition(
875
+ record.sessionId,
876
+ "session_exit",
877
+ exitCode === 0 ? "stopped" : "failed",
878
+ void 0,
879
+ exitCode === 0,
880
+ exitCode === 0 ? void 0 : `exitCode=${exitCode}`
881
+ );
688
882
  setTimeout(() => this.storage.remove(record.sessionId), 5e3);
689
883
  }
690
884
  });
@@ -694,6 +888,7 @@ var SessionHostServer = class extends import_events.EventEmitter {
694
888
  const startedRecord = this.registry.markStarted(record.sessionId, pid);
695
889
  this.persistNow(record.sessionId);
696
890
  this.emitEvent({ type: startEventType, sessionId: record.sessionId, pid });
891
+ this.recordRuntimeTransition(record.sessionId, startEventType, startedRecord.lifecycle, `pid=${pid}`, true);
697
892
  return startedRecord;
698
893
  }
699
894
  };
@@ -955,6 +1150,7 @@ async function attachRuntime(target, readOnly = false, takeover = false) {
955
1150
  return { signal, handler };
956
1151
  });
957
1152
  const unsubscribe = client.onEvent((event) => {
1153
+ if (!("sessionId" in event)) return;
958
1154
  if (event.sessionId !== runtimeId) return;
959
1155
  if (event.type === "session_output") {
960
1156
  if (event.seq <= lastSeq) return;