@adhdev/daemon-standalone 0.9.82-rc.99 → 1.0.0

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.
@@ -38,13 +38,13 @@ var import_crypto = require("crypto");
38
38
  var fs4 = __toESM(require("fs"));
39
39
  var os3 = __toESM(require("os"));
40
40
  var path2 = __toESM(require("path"));
41
- var import_session_host_core3 = require("@adhdev/session-host-core");
41
+ var import_session_host_core5 = require("@adhdev/session-host-core");
42
42
 
43
43
  // src/server.ts
44
44
  var import_events = require("events");
45
45
  var fs3 = __toESM(require("fs"));
46
46
  var net = __toESM(require("net"));
47
- var import_session_host_core2 = require("@adhdev/session-host-core");
47
+ var import_session_host_core4 = require("@adhdev/session-host-core");
48
48
 
49
49
  // src/runtime.ts
50
50
  var fs = __toESM(require("fs"));
@@ -82,7 +82,8 @@ function formatXtermViewportPlain(terminal, rows) {
82
82
  const lines = [];
83
83
  for (let i = start; i < end; i++) {
84
84
  const line = buffer.getLine(i);
85
- lines.push(line ? line.translateToString(true) : "");
85
+ const raw = line ? line.translateToString(false) : "";
86
+ lines.push(raw.replace(/\s+$/, ""));
86
87
  }
87
88
  let first = 0;
88
89
  let last = lines.length;
@@ -145,6 +146,9 @@ function createXtermMirror(options) {
145
146
  if (serializer) return serializeXtermViewport(terminal, serializer, currentRows);
146
147
  return formatXtermViewportPlain(terminal, currentRows);
147
148
  },
149
+ formatPlainText() {
150
+ return formatXtermViewportPlain(terminal, currentRows).replace(/\r\n/g, "\n");
151
+ },
148
152
  getCursorPosition() {
149
153
  const buffer = terminal.buffer.active;
150
154
  return {
@@ -177,6 +181,9 @@ function normalizeGhosttyBinding(mod) {
177
181
  formatVT() {
178
182
  return viewportSnapshot.formatVT();
179
183
  },
184
+ formatPlainText() {
185
+ return viewportSnapshot.formatPlainText();
186
+ },
180
187
  getCursorPosition() {
181
188
  if (typeof handle.getCursorPosition === "function") return handle.getCursorPosition();
182
189
  return viewportSnapshot.getCursorPosition();
@@ -225,6 +232,10 @@ var PtySessionRuntime = class {
225
232
  ptyProcess = null;
226
233
  screenMirror = null;
227
234
  pendingQueryScanTail = "";
235
+ terminalModeScanTail = "";
236
+ altScreen = false;
237
+ pasteMode = false;
238
+ scrollRegion;
228
239
  onDataCallback;
229
240
  onExitCallback;
230
241
  constructor(options) {
@@ -232,6 +243,7 @@ var PtySessionRuntime = class {
232
243
  this.payload = options.payload;
233
244
  this.cols = (0, import_session_host_core.resolveSessionHostCols)(options.payload.cols);
234
245
  this.rows = (0, import_session_host_core.resolveSessionHostRows)(options.payload.rows);
246
+ this.scrollRegion = { top: 0, bot: this.rows - 1 };
235
247
  this.onDataCallback = options.onData;
236
248
  this.onExitCallback = options.onExit;
237
249
  }
@@ -263,6 +275,7 @@ var PtySessionRuntime = class {
263
275
  });
264
276
  this.ptyProcess.onData((data) => {
265
277
  this.screenMirror?.write(data);
278
+ this.trackTerminalModes(data);
266
279
  this.respondToTerminalQueries(data);
267
280
  this.onDataCallback(data);
268
281
  });
@@ -271,6 +284,7 @@ var PtySessionRuntime = class {
271
284
  this.screenMirror?.dispose();
272
285
  this.screenMirror = null;
273
286
  this.pendingQueryScanTail = "";
287
+ this.terminalModeScanTail = "";
274
288
  this.onExitCallback(exitCode ?? null);
275
289
  });
276
290
  return this.ptyProcess.pid;
@@ -282,6 +296,12 @@ var PtySessionRuntime = class {
282
296
  resize(cols, rows) {
283
297
  if (!this.ptyProcess) throw new Error(`Session not running: ${this.sessionId}`);
284
298
  this.ptyProcess.resize(cols, rows);
299
+ this.cols = Math.max(1, cols | 0);
300
+ this.rows = Math.max(1, rows | 0);
301
+ this.scrollRegion = {
302
+ top: Math.min(this.scrollRegion.top, this.rows - 1),
303
+ bot: Math.min(Math.max(this.scrollRegion.top, this.scrollRegion.bot), this.rows - 1)
304
+ };
285
305
  this.screenMirror?.resize(cols, rows);
286
306
  }
287
307
  stop() {
@@ -305,6 +325,55 @@ var PtySessionRuntime = class {
305
325
  getSnapshotText() {
306
326
  return this.screenMirror?.formatVT() || "";
307
327
  }
328
+ getTerminalSnapshot() {
329
+ if (!this.ptyProcess || !this.screenMirror) {
330
+ throw new Error(`Session not running: ${this.sessionId}`);
331
+ }
332
+ const cursor = this.screenMirror.getCursorPosition();
333
+ return {
334
+ text: this.screenMirror.formatPlainText(),
335
+ state: {
336
+ cursor: {
337
+ row: Math.max(0, cursor.row | 0),
338
+ col: Math.max(0, cursor.col | 0)
339
+ },
340
+ altScreen: this.altScreen,
341
+ pasteMode: this.pasteMode,
342
+ rawMode: true,
343
+ scrollRegion: { ...this.scrollRegion },
344
+ cols: this.cols,
345
+ rows: this.rows
346
+ }
347
+ };
348
+ }
349
+ trackTerminalModes(data) {
350
+ if (!data) return;
351
+ const combined = this.terminalModeScanTail + data;
352
+ const privateMode = /\x1b\[\?([0-9;]*)([hl])/g;
353
+ let privateMatch;
354
+ while ((privateMatch = privateMode.exec(combined)) !== null) {
355
+ const enabled = privateMatch[2] === "h";
356
+ for (const mode of privateMatch[1].split(";")) {
357
+ if (mode === "47" || mode === "1047" || mode === "1049") this.altScreen = enabled;
358
+ if (mode === "2004") this.pasteMode = enabled;
359
+ }
360
+ }
361
+ const scrollRegion = /\x1b\[(\d*)(?:;(\d*))?r/g;
362
+ let scrollMatch;
363
+ while ((scrollMatch = scrollRegion.exec(combined)) !== null) {
364
+ const top = scrollMatch[1] ? Number.parseInt(scrollMatch[1], 10) - 1 : 0;
365
+ const bot = scrollMatch[2] ? Number.parseInt(scrollMatch[2], 10) - 1 : this.rows - 1;
366
+ this.scrollRegion = {
367
+ top: Math.max(0, Math.min(this.rows - 1, top)),
368
+ bot: Math.max(0, Math.min(this.rows - 1, bot))
369
+ };
370
+ if (this.scrollRegion.bot < this.scrollRegion.top) {
371
+ this.scrollRegion = { top: 0, bot: this.rows - 1 };
372
+ }
373
+ }
374
+ const lastEscape = combined.lastIndexOf("\x1B");
375
+ this.terminalModeScanTail = lastEscape >= 0 && combined.length - lastEscape < 64 ? combined.slice(lastEscape) : "";
376
+ }
308
377
  respondToTerminalQueries(data) {
309
378
  if (!this.ptyProcess || !this.screenMirror || !data) return;
310
379
  const combined = this.pendingQueryScanTail + data;
@@ -369,11 +438,200 @@ var SessionHostStorage = class {
369
438
  }
370
439
  };
371
440
 
441
+ // src/session-diagnostics.ts
442
+ var import_session_host_core2 = require("@adhdev/session-host-core");
443
+ var MAX_RECENT_DIAGNOSTICS = 200;
444
+ function pushRecent(bucket, entry, max = MAX_RECENT_DIAGNOSTICS) {
445
+ bucket.push(entry);
446
+ if (bucket.length > max) {
447
+ bucket.splice(0, bucket.length - max);
448
+ }
449
+ }
450
+ function getSessionHostRecoveryLabel(record) {
451
+ const recoveryState = typeof record.meta?.runtimeRecoveryState === "string" ? String(record.meta.runtimeRecoveryState).trim() : "";
452
+ if (!recoveryState) return null;
453
+ if (recoveryState === "auto_resumed") return "restored after restart";
454
+ if (recoveryState === "resume_failed") return "restore failed";
455
+ if (recoveryState === "host_restart_interrupted") return "host restart interrupted";
456
+ if (recoveryState === "orphan_snapshot") return "snapshot recovered";
457
+ return recoveryState.replace(/_/g, " ");
458
+ }
459
+ function getSessionSurfaceKind(record) {
460
+ if (["starting", "running", "stopping", "interrupted"].includes(record.lifecycle)) {
461
+ return "live_runtime";
462
+ }
463
+ if ((record.lifecycle === "stopped" || record.lifecycle === "failed") && (record.meta?.restoredFromStorage === true || getSessionHostRecoveryLabel(record))) {
464
+ return "recovery_snapshot";
465
+ }
466
+ return "inactive_record";
467
+ }
468
+ function annotateSessionSurface(record) {
469
+ return {
470
+ ...record,
471
+ surfaceKind: getSessionSurfaceKind(record)
472
+ };
473
+ }
474
+ function sanitizeDiagnosticsRecord(record) {
475
+ return {
476
+ ...record,
477
+ launchCommand: {
478
+ command: record.launchCommand.command,
479
+ args: Array.isArray(record.launchCommand.args) ? [...record.launchCommand.args] : []
480
+ }
481
+ };
482
+ }
483
+ function buildHostDiagnostics(params) {
484
+ const limit = Math.max(1, Math.min(200, Number(params.payload?.limit) || 50));
485
+ const allSessions = params.payload?.includeSessions === false ? void 0 : params.sessions.map((record) => annotateSessionSurface(record)).map((record) => sanitizeDiagnosticsRecord(record));
486
+ const liveRuntimes = allSessions?.filter((record) => record.surfaceKind === "live_runtime");
487
+ const recoverySnapshots = allSessions?.filter((record) => record.surfaceKind === "recovery_snapshot").slice(0, limit);
488
+ const inactiveRecords = allSessions?.filter((record) => record.surfaceKind === "inactive_record").slice(0, limit);
489
+ const sessions = allSessions ? [
490
+ ...liveRuntimes || [],
491
+ ...recoverySnapshots || [],
492
+ ...inactiveRecords || []
493
+ ] : void 0;
494
+ return {
495
+ hostStartedAt: params.hostStartedAt,
496
+ endpoint: params.endpointPath,
497
+ runtimeCount: params.runtimeCount,
498
+ supportedRequestTypes: [...import_session_host_core2.SESSION_HOST_SUPPORTED_REQUEST_TYPES],
499
+ sessions,
500
+ liveRuntimes,
501
+ recoverySnapshots,
502
+ inactiveRecords,
503
+ recentLogs: params.recentLogs.slice(-limit),
504
+ recentRequests: params.recentRequests.slice(-limit),
505
+ recentTransitions: params.recentTransitions.slice(-limit)
506
+ };
507
+ }
508
+
509
+ // src/session-lifecycle.ts
510
+ var import_session_host_core3 = require("@adhdev/session-host-core");
511
+ function compareDuplicateCandidates(a, b) {
512
+ const score = (record) => {
513
+ const lifecycleScore = record.lifecycle === "running" ? 4 : record.lifecycle === "starting" ? 3 : record.lifecycle === "stopping" ? 2 : record.lifecycle === "interrupted" ? 1 : 0;
514
+ return [
515
+ lifecycleScore,
516
+ record.writeOwner ? 1 : 0,
517
+ Array.isArray(record.attachedClients) ? record.attachedClients.length : 0,
518
+ record.lastActivityAt || 0,
519
+ record.startedAt || 0,
520
+ record.createdAt || 0
521
+ ];
522
+ };
523
+ const aScore = score(a);
524
+ const bScore = score(b);
525
+ for (let i = 0; i < aScore.length; i += 1) {
526
+ if (aScore[i] === bScore[i]) continue;
527
+ return bScore[i] - aScore[i];
528
+ }
529
+ return 0;
530
+ }
531
+ function planDuplicatePrune(sessions, filters) {
532
+ const candidates = sessions.filter((record) => ["starting", "running", "stopping", "interrupted"].includes(record.lifecycle)).filter((record) => !filters.providerFilter || record.providerType === filters.providerFilter).filter((record) => !filters.workspaceFilter || record.workspace === filters.workspaceFilter);
533
+ const groups = /* @__PURE__ */ new Map();
534
+ for (const record of candidates) {
535
+ const providerSessionId = typeof record.meta?.providerSessionId === "string" ? String(record.meta.providerSessionId).trim() : "";
536
+ if (!providerSessionId) continue;
537
+ const bindingKey = `${record.providerType}::${record.workspace}::${providerSessionId}`;
538
+ const bucket = groups.get(bindingKey) || [];
539
+ bucket.push(record);
540
+ groups.set(bindingKey, bucket);
541
+ }
542
+ const duplicateGroups = [];
543
+ const keptSessionIds = [];
544
+ const duplicateRecords = [];
545
+ for (const [bindingKey, records] of groups.entries()) {
546
+ if (records.length < 2) continue;
547
+ const sorted = [...records].sort((a, b) => compareDuplicateCandidates(a, b));
548
+ const kept = sorted[0];
549
+ const duplicates = sorted.slice(1);
550
+ const providerSessionId = typeof kept.meta?.providerSessionId === "string" ? String(kept.meta.providerSessionId) : "";
551
+ duplicateGroups.push({
552
+ bindingKey,
553
+ providerType: kept.providerType,
554
+ workspace: kept.workspace,
555
+ providerSessionId,
556
+ keptSessionId: kept.sessionId,
557
+ prunedSessionIds: duplicates.map((record) => record.sessionId)
558
+ });
559
+ keptSessionIds.push(kept.sessionId);
560
+ for (const duplicate of duplicates) {
561
+ duplicateRecords.push(duplicate);
562
+ }
563
+ }
564
+ return { duplicateGroups, keptSessionIds, duplicateRecords };
565
+ }
566
+ function buildPayloadFromRecord(record) {
567
+ return {
568
+ sessionId: record.sessionId,
569
+ runtimeKey: record.runtimeKey,
570
+ displayName: record.displayName,
571
+ providerType: record.providerType,
572
+ category: record.category,
573
+ workspace: record.workspace,
574
+ launchCommand: record.launchCommand,
575
+ cols: (0, import_session_host_core3.resolveSessionHostCols)(typeof record.meta?.sessionHostCols === "number" ? record.meta.sessionHostCols : void 0),
576
+ rows: (0, import_session_host_core3.resolveSessionHostRows)(typeof record.meta?.sessionHostRows === "number" ? record.meta.sessionHostRows : void 0),
577
+ meta: record.meta
578
+ };
579
+ }
580
+ function buildRecoveredRecord(persisted) {
581
+ const wasLiveRuntime = !["stopped", "failed"].includes(persisted.record.lifecycle);
582
+ const hadAttachedClients = Array.isArray(persisted.record.attachedClients) && persisted.record.attachedClients.length > 0;
583
+ const hadWriteOwner = !!persisted.record.writeOwner;
584
+ const hadRecoveryInterest = hadAttachedClients || hadWriteOwner;
585
+ const recoveredRecord = {
586
+ ...persisted.record,
587
+ attachedClients: [],
588
+ writeOwner: null,
589
+ lifecycle: wasLiveRuntime ? "stopped" : persisted.record.lifecycle,
590
+ lastActivityAt: Date.now(),
591
+ meta: {
592
+ ...persisted.record.meta || {},
593
+ restoredFromStorage: true,
594
+ runtimeRecoveryState: wasLiveRuntime ? "orphan_snapshot" : "snapshot",
595
+ runtimeHadAttachedClientsAtCrash: hadAttachedClients,
596
+ runtimeHadWriteOwnerAtCrash: hadWriteOwner,
597
+ runtimeAutoResumeSkipped: wasLiveRuntime && hadRecoveryInterest
598
+ }
599
+ };
600
+ return { recoveredRecord, wasLiveRuntime, hadRecoveryInterest };
601
+ }
602
+
603
+ // src/session-protocol.ts
604
+ function getRequestSessionId(request) {
605
+ const payload = request.payload;
606
+ return typeof payload?.sessionId === "string" ? payload.sessionId : void 0;
607
+ }
608
+ function getRequestClientId(request) {
609
+ const payload = request.payload;
610
+ return typeof payload?.clientId === "string" ? payload.clientId : void 0;
611
+ }
612
+ function mergeRuntimeSnapshot(base, record, opts) {
613
+ const cols = typeof record?.meta?.sessionHostCols === "number" ? record.meta.sessionHostCols : 80;
614
+ const rows = typeof record?.meta?.sessionHostRows === "number" ? record.meta.sessionHostRows : 24;
615
+ if (typeof opts.sinceSeq === "number" || !opts.runtimeText) {
616
+ return {
617
+ ...base,
618
+ cols,
619
+ rows
620
+ };
621
+ }
622
+ return {
623
+ ...base,
624
+ text: opts.runtimeText,
625
+ truncated: false,
626
+ cols,
627
+ rows
628
+ };
629
+ }
630
+
372
631
  // src/server.ts
373
- var SessionHostServer = class _SessionHostServer extends import_events.EventEmitter {
374
- static MAX_RECENT_DIAGNOSTICS = 200;
632
+ var SessionHostServer = class extends import_events.EventEmitter {
375
633
  endpoint;
376
- registry = new import_session_host_core2.SessionHostRegistry();
634
+ registry = new import_session_host_core4.SessionHostRegistry();
377
635
  runtimes = /* @__PURE__ */ new Map();
378
636
  storage;
379
637
  ipcServer = null;
@@ -390,7 +648,7 @@ var SessionHostServer = class _SessionHostServer extends import_events.EventEmit
390
648
  lastNoOutputInputWarnAt = /* @__PURE__ */ new Map();
391
649
  constructor(options = {}) {
392
650
  super();
393
- this.endpoint = options.endpoint || (0, import_session_host_core2.getDefaultSessionHostEndpoint)(options.appName || "adhdev");
651
+ this.endpoint = options.endpoint || (0, import_session_host_core4.getDefaultSessionHostEndpoint)(options.appName || "adhdev");
394
652
  this.storage = new SessionHostStorage({ appName: options.appName || "adhdev" });
395
653
  }
396
654
  async start() {
@@ -415,7 +673,7 @@ var SessionHostServer = class _SessionHostServer extends import_events.EventEmit
415
673
  } catch {
416
674
  }
417
675
  });
418
- socket.on("data", (0, import_session_host_core2.createLineParser)((envelope) => {
676
+ socket.on("data", (0, import_session_host_core4.createLineParser)((envelope) => {
419
677
  if (envelope.kind !== "request") return;
420
678
  void this.handleIncomingRequest(socket, envelope);
421
679
  }));
@@ -518,6 +776,8 @@ var SessionHostServer = class _SessionHostServer extends import_events.EventEmit
518
776
  }
519
777
  case "get_snapshot":
520
778
  return { success: true, result: this.getSnapshot(request.payload.sessionId, request.payload.sinceSeq) };
779
+ case "get_terminal_snapshot":
780
+ return { success: true, result: this.requireRuntime(request.payload.sessionId).getTerminalSnapshot() };
521
781
  case "get_host_diagnostics":
522
782
  return { success: true, result: this.getHostDiagnostics(request.payload) };
523
783
  case "clear_session_buffer": {
@@ -618,7 +878,7 @@ var SessionHostServer = class _SessionHostServer extends import_events.EventEmit
618
878
  if (this.runtimes.has(request.payload.sessionId)) {
619
879
  return { success: true, result: existing };
620
880
  }
621
- const resumed = this.startRuntime(existing, this.buildPayloadFromRecord(existing), "session_resumed");
881
+ const resumed = this.startRuntime(existing, buildPayloadFromRecord(existing), "session_resumed");
622
882
  this.recordRuntimeTransition(request.payload.sessionId, "resume_session", resumed.lifecycle, void 0, true);
623
883
  return { success: true, result: resumed };
624
884
  }
@@ -700,7 +960,7 @@ var SessionHostServer = class _SessionHostServer extends import_events.EventEmit
700
960
  sessions.add(sessionId);
701
961
  }
702
962
  async handleIncomingRequest(socket, envelope) {
703
- const sessionId = this.getRequestSessionId(envelope.request);
963
+ const sessionId = getRequestSessionId(envelope.request);
704
964
  if (sessionId && (envelope.request.type === "create_session" || envelope.request.type === "attach_session")) {
705
965
  this.subscribeSocketToSession(socket, sessionId);
706
966
  }
@@ -714,13 +974,13 @@ var SessionHostServer = class _SessionHostServer extends import_events.EventEmit
714
974
  timestamp: startedAt,
715
975
  requestId: envelope.requestId,
716
976
  type: envelope.request.type,
717
- sessionId: this.getRequestSessionId(envelope.request),
718
- clientId: this.getRequestClientId(envelope.request),
977
+ sessionId: getRequestSessionId(envelope.request),
978
+ clientId: getRequestClientId(envelope.request),
719
979
  success: response.success,
720
980
  durationMs: Math.max(0, Date.now() - startedAt),
721
981
  error: response.success ? void 0 : response.error
722
982
  });
723
- this.writeEnvelopeSafely(socket, (0, import_session_host_core2.createResponseEnvelope)(envelope.requestId, response));
983
+ this.writeEnvelopeSafely(socket, (0, import_session_host_core4.createResponseEnvelope)(envelope.requestId, response));
724
984
  }
725
985
  writeEnvelopeSafely(socket, envelope) {
726
986
  if (socket.destroyed || !socket.writable || socket.writableEnded) {
@@ -758,79 +1018,24 @@ var SessionHostServer = class _SessionHostServer extends import_events.EventEmit
758
1018
  const record = this.registry.getSession(sessionId);
759
1019
  if (!record) return;
760
1020
  const snapshot = this.getSnapshot(sessionId);
761
- this.storage.save(record, snapshot);
762
- }
763
- getSessionHostRecoveryLabel(record) {
764
- const recoveryState = typeof record.meta?.runtimeRecoveryState === "string" ? String(record.meta.runtimeRecoveryState).trim() : "";
765
- if (!recoveryState) return null;
766
- if (recoveryState === "auto_resumed") return "restored after restart";
767
- if (recoveryState === "resume_failed") return "restore failed";
768
- if (recoveryState === "host_restart_interrupted") return "host restart interrupted";
769
- if (recoveryState === "orphan_snapshot") return "snapshot recovered";
770
- return recoveryState.replace(/_/g, " ");
771
- }
772
- getSessionSurfaceKind(record) {
773
- if (["starting", "running", "stopping", "interrupted"].includes(record.lifecycle)) {
774
- return "live_runtime";
775
- }
776
- if ((record.lifecycle === "stopped" || record.lifecycle === "failed") && (record.meta?.restoredFromStorage === true || this.getSessionHostRecoveryLabel(record))) {
777
- return "recovery_snapshot";
1021
+ try {
1022
+ this.storage.save(record, snapshot);
1023
+ } catch (error) {
1024
+ const code = typeof error?.code === "string" ? error.code : "persist_failed";
1025
+ console.error(`[session-host] Persist failed for ${sessionId}: ${code}: ${error?.message || error}`);
778
1026
  }
779
- return "inactive_record";
780
- }
781
- annotateSessionSurface(record) {
782
- return {
783
- ...record,
784
- surfaceKind: this.getSessionSurfaceKind(record)
785
- };
786
- }
787
- sanitizeDiagnosticsRecord(record) {
788
- return {
789
- ...record,
790
- launchCommand: {
791
- command: record.launchCommand.command,
792
- args: Array.isArray(record.launchCommand.args) ? [...record.launchCommand.args] : []
793
- }
794
- };
795
1027
  }
796
1028
  getHostDiagnostics(payload) {
797
- const limit = Math.max(1, Math.min(200, Number(payload?.limit) || 50));
798
- const allSessions = payload?.includeSessions === false ? void 0 : this.registry.listSessions().map((record) => this.annotateSessionSurface(record)).map((record) => this.sanitizeDiagnosticsRecord(record));
799
- const liveRuntimes = allSessions?.filter((record) => record.surfaceKind === "live_runtime");
800
- const recoverySnapshots = allSessions?.filter((record) => record.surfaceKind === "recovery_snapshot").slice(0, limit);
801
- const inactiveRecords = allSessions?.filter((record) => record.surfaceKind === "inactive_record").slice(0, limit);
802
- const sessions = allSessions ? [
803
- ...liveRuntimes || [],
804
- ...recoverySnapshots || [],
805
- ...inactiveRecords || []
806
- ] : void 0;
807
- return {
1029
+ return buildHostDiagnostics({
1030
+ payload,
808
1031
  hostStartedAt: this.startedAt,
809
- endpoint: this.endpoint.path,
1032
+ endpointPath: this.endpoint.path,
810
1033
  runtimeCount: this.runtimes.size,
811
- supportedRequestTypes: [...import_session_host_core2.SESSION_HOST_SUPPORTED_REQUEST_TYPES],
812
- sessions,
813
- liveRuntimes,
814
- recoverySnapshots,
815
- inactiveRecords,
816
- recentLogs: this.recentLogs.slice(-limit),
817
- recentRequests: this.recentRequests.slice(-limit),
818
- recentTransitions: this.recentTransitions.slice(-limit)
819
- };
820
- }
821
- getRequestSessionId(request) {
822
- const payload = request.payload;
823
- return typeof payload?.sessionId === "string" ? payload.sessionId : void 0;
824
- }
825
- getRequestClientId(request) {
826
- const payload = request.payload;
827
- return typeof payload?.clientId === "string" ? payload.clientId : void 0;
828
- }
829
- pushRecent(bucket, entry) {
830
- bucket.push(entry);
831
- if (bucket.length > _SessionHostServer.MAX_RECENT_DIAGNOSTICS) {
832
- bucket.splice(0, bucket.length - _SessionHostServer.MAX_RECENT_DIAGNOSTICS);
833
- }
1034
+ sessions: this.registry.listSessions(),
1035
+ recentLogs: this.recentLogs,
1036
+ recentRequests: this.recentRequests,
1037
+ recentTransitions: this.recentTransitions
1038
+ });
834
1039
  }
835
1040
  recordHostLog(level, message, sessionId, data) {
836
1041
  const entry = {
@@ -840,12 +1045,12 @@ var SessionHostServer = class _SessionHostServer extends import_events.EventEmit
840
1045
  sessionId,
841
1046
  data
842
1047
  };
843
- this.pushRecent(this.recentLogs, entry);
1048
+ pushRecent(this.recentLogs, entry);
844
1049
  this.emitEvent({ type: "host_log", entry });
845
1050
  this.emit("log", `[${level}] ${message}`);
846
1051
  }
847
1052
  recordRequestTrace(trace) {
848
- this.pushRecent(this.recentRequests, trace);
1053
+ pushRecent(this.recentRequests, trace);
849
1054
  this.emitEvent({ type: "request_trace", trace });
850
1055
  if (!trace.success) {
851
1056
  this.recordHostLog(
@@ -915,7 +1120,7 @@ var SessionHostServer = class _SessionHostServer extends import_events.EventEmit
915
1120
  success,
916
1121
  error
917
1122
  };
918
- this.pushRecent(this.recentTransitions, transition);
1123
+ pushRecent(this.recentTransitions, transition);
919
1124
  this.emitEvent({ type: "runtime_transition", transition });
920
1125
  }
921
1126
  waitForRuntimeExit(sessionId, timeoutMs = 5e3) {
@@ -951,29 +1156,8 @@ var SessionHostServer = class _SessionHostServer extends import_events.EventEmit
951
1156
  getSnapshot(sessionId, sinceSeq) {
952
1157
  const snapshot = this.registry.getSnapshot(sessionId, sinceSeq);
953
1158
  const record = this.registry.getSession(sessionId);
954
- if (typeof sinceSeq === "number") {
955
- return {
956
- ...snapshot,
957
- cols: typeof record?.meta?.sessionHostCols === "number" ? record.meta.sessionHostCols : 80,
958
- rows: typeof record?.meta?.sessionHostRows === "number" ? record.meta.sessionHostRows : 24
959
- };
960
- }
961
- const runtime = this.runtimes.get(sessionId);
962
- const runtimeText = runtime?.getSnapshotText?.() || "";
963
- if (!runtimeText) {
964
- return {
965
- ...snapshot,
966
- cols: typeof record?.meta?.sessionHostCols === "number" ? record.meta.sessionHostCols : 80,
967
- rows: typeof record?.meta?.sessionHostRows === "number" ? record.meta.sessionHostRows : 24
968
- };
969
- }
970
- return {
971
- ...snapshot,
972
- text: runtimeText,
973
- truncated: false,
974
- cols: typeof record?.meta?.sessionHostCols === "number" ? record.meta.sessionHostCols : 80,
975
- rows: typeof record?.meta?.sessionHostRows === "number" ? record.meta.sessionHostRows : 24
976
- };
1159
+ const runtimeText = typeof sinceSeq === "number" ? "" : this.runtimes.get(sessionId)?.getSnapshotText?.() || "";
1160
+ return mergeRuntimeSnapshot(snapshot, record, { sinceSeq, runtimeText });
977
1161
  }
978
1162
  flushAllPersistence() {
979
1163
  for (const sessionId of this.runtimes.keys()) {
@@ -996,7 +1180,7 @@ var SessionHostServer = class _SessionHostServer extends import_events.EventEmit
996
1180
  await this.waitForRuntimeExit(sessionId);
997
1181
  }
998
1182
  const latest = this.registry.getSession(sessionId) || existing;
999
- const restarted = this.startRuntime(latest, this.buildPayloadFromRecord(latest), "session_resumed");
1183
+ const restarted = this.startRuntime(latest, buildPayloadFromRecord(latest), "session_resumed");
1000
1184
  this.recordRuntimeTransition(sessionId, "restart_completed", restarted.lifecycle, void 0, true);
1001
1185
  return restarted;
1002
1186
  }
@@ -1004,36 +1188,13 @@ var SessionHostServer = class _SessionHostServer extends import_events.EventEmit
1004
1188
  const providerFilter = typeof payload?.providerType === "string" ? payload.providerType.trim() : "";
1005
1189
  const workspaceFilter = typeof payload?.workspace === "string" ? payload.workspace.trim() : "";
1006
1190
  const dryRun = payload?.dryRun === true;
1007
- const sessions = this.registry.listSessions().filter((record) => ["starting", "running", "stopping", "interrupted"].includes(record.lifecycle)).filter((record) => !providerFilter || record.providerType === providerFilter).filter((record) => !workspaceFilter || record.workspace === workspaceFilter);
1008
- const groups = /* @__PURE__ */ new Map();
1009
- for (const record of sessions) {
1010
- const providerSessionId = typeof record.meta?.providerSessionId === "string" ? String(record.meta.providerSessionId).trim() : "";
1011
- if (!providerSessionId) continue;
1012
- const bindingKey = `${record.providerType}::${record.workspace}::${providerSessionId}`;
1013
- const bucket = groups.get(bindingKey) || [];
1014
- bucket.push(record);
1015
- groups.set(bindingKey, bucket);
1016
- }
1017
- const duplicateGroups = [];
1018
- const keptSessionIds = [];
1191
+ const { duplicateGroups, keptSessionIds, duplicateRecords } = planDuplicatePrune(
1192
+ this.registry.listSessions(),
1193
+ { providerFilter, workspaceFilter }
1194
+ );
1019
1195
  const prunedSessionIds = [];
1020
- for (const [bindingKey, records] of groups.entries()) {
1021
- if (records.length < 2) continue;
1022
- const sorted = [...records].sort((a, b) => this.compareDuplicateCandidates(a, b));
1023
- const kept = sorted[0];
1024
- const duplicates = sorted.slice(1);
1025
- const providerSessionId = typeof kept.meta?.providerSessionId === "string" ? String(kept.meta.providerSessionId) : "";
1026
- duplicateGroups.push({
1027
- bindingKey,
1028
- providerType: kept.providerType,
1029
- workspace: kept.workspace,
1030
- providerSessionId,
1031
- keptSessionId: kept.sessionId,
1032
- prunedSessionIds: duplicates.map((record) => record.sessionId)
1033
- });
1034
- keptSessionIds.push(kept.sessionId);
1035
- if (dryRun) continue;
1036
- for (const duplicate of duplicates) {
1196
+ if (!dryRun) {
1197
+ for (const duplicate of duplicateRecords) {
1037
1198
  await this.pruneDuplicateRuntime(duplicate);
1038
1199
  prunedSessionIds.push(duplicate.sessionId);
1039
1200
  }
@@ -1061,25 +1222,7 @@ var SessionHostServer = class _SessionHostServer extends import_events.EventEmit
1061
1222
  const states = this.storage.loadAll();
1062
1223
  let skippedAutoResumeSessions = 0;
1063
1224
  for (const persisted of states) {
1064
- const wasLiveRuntime = !["stopped", "failed"].includes(persisted.record.lifecycle);
1065
- const hadAttachedClients = Array.isArray(persisted.record.attachedClients) && persisted.record.attachedClients.length > 0;
1066
- const hadWriteOwner = !!persisted.record.writeOwner;
1067
- const hadRecoveryInterest = hadAttachedClients || hadWriteOwner;
1068
- const recoveredRecord = {
1069
- ...persisted.record,
1070
- attachedClients: [],
1071
- writeOwner: null,
1072
- lifecycle: wasLiveRuntime ? "stopped" : persisted.record.lifecycle,
1073
- lastActivityAt: Date.now(),
1074
- meta: {
1075
- ...persisted.record.meta || {},
1076
- restoredFromStorage: true,
1077
- runtimeRecoveryState: wasLiveRuntime ? "orphan_snapshot" : "snapshot",
1078
- runtimeHadAttachedClientsAtCrash: hadAttachedClients,
1079
- runtimeHadWriteOwnerAtCrash: hadWriteOwner,
1080
- runtimeAutoResumeSkipped: wasLiveRuntime && hadRecoveryInterest
1081
- }
1082
- };
1225
+ const { recoveredRecord, wasLiveRuntime, hadRecoveryInterest } = buildRecoveredRecord(persisted);
1083
1226
  this.registry.restoreSession(recoveredRecord, persisted.snapshot);
1084
1227
  this.storage.save(recoveredRecord, persisted.snapshot);
1085
1228
  if (wasLiveRuntime && hadRecoveryInterest) {
@@ -1090,26 +1233,6 @@ var SessionHostServer = class _SessionHostServer extends import_events.EventEmit
1090
1233
  this.recordHostLog("warn", `session host restored ${skippedAutoResumeSessions} live runtime snapshot(s) without auto-resume`);
1091
1234
  }
1092
1235
  }
1093
- compareDuplicateCandidates(a, b) {
1094
- const score = (record) => {
1095
- const lifecycleScore = record.lifecycle === "running" ? 4 : record.lifecycle === "starting" ? 3 : record.lifecycle === "stopping" ? 2 : record.lifecycle === "interrupted" ? 1 : 0;
1096
- return [
1097
- lifecycleScore,
1098
- record.writeOwner ? 1 : 0,
1099
- Array.isArray(record.attachedClients) ? record.attachedClients.length : 0,
1100
- record.lastActivityAt || 0,
1101
- record.startedAt || 0,
1102
- record.createdAt || 0
1103
- ];
1104
- };
1105
- const aScore = score(a);
1106
- const bScore = score(b);
1107
- for (let i = 0; i < aScore.length; i += 1) {
1108
- if (aScore[i] === bScore[i]) continue;
1109
- return bScore[i] - aScore[i];
1110
- }
1111
- return 0;
1112
- }
1113
1236
  async pruneDuplicateRuntime(record) {
1114
1237
  const providerSessionId = typeof record.meta?.providerSessionId === "string" ? String(record.meta.providerSessionId) : void 0;
1115
1238
  this.recordRuntimeTransition(
@@ -1130,20 +1253,6 @@ var SessionHostServer = class _SessionHostServer extends import_events.EventEmit
1130
1253
  this.registry.deleteSession(record.sessionId);
1131
1254
  this.storage.remove(record.sessionId);
1132
1255
  }
1133
- buildPayloadFromRecord(record) {
1134
- return {
1135
- sessionId: record.sessionId,
1136
- runtimeKey: record.runtimeKey,
1137
- displayName: record.displayName,
1138
- providerType: record.providerType,
1139
- category: record.category,
1140
- workspace: record.workspace,
1141
- launchCommand: record.launchCommand,
1142
- cols: (0, import_session_host_core2.resolveSessionHostCols)(typeof record.meta?.sessionHostCols === "number" ? record.meta.sessionHostCols : void 0),
1143
- rows: (0, import_session_host_core2.resolveSessionHostRows)(typeof record.meta?.sessionHostRows === "number" ? record.meta.sessionHostRows : void 0),
1144
- meta: record.meta
1145
- };
1146
- }
1147
1256
  startRuntime(record, payload, startEventType) {
1148
1257
  const runtime = new PtySessionRuntime({
1149
1258
  sessionId: record.sessionId,
@@ -1233,7 +1342,7 @@ async function runServer() {
1233
1342
  });
1234
1343
  }
1235
1344
  async function listRuntimes(showAll = false) {
1236
- const client = new import_session_host_core3.SessionHostClient({ endpoint: (0, import_session_host_core3.getDefaultSessionHostEndpoint)(SESSION_HOST_APP_NAME) });
1345
+ const client = new import_session_host_core5.SessionHostClient({ endpoint: (0, import_session_host_core5.getDefaultSessionHostEndpoint)(SESSION_HOST_APP_NAME) });
1237
1346
  try {
1238
1347
  const response = await client.request({
1239
1348
  type: "list_sessions",
@@ -1252,7 +1361,7 @@ async function listRuntimes(showAll = false) {
1252
1361
  console.log([
1253
1362
  runtime.runtimeKey,
1254
1363
  runtime.lifecycle,
1255
- (0, import_session_host_core3.formatRuntimeOwner)(runtime),
1364
+ (0, import_session_host_core5.formatRuntimeOwner)(runtime),
1256
1365
  runtime.workspaceLabel,
1257
1366
  runtime.sessionId,
1258
1367
  runtime.displayName
@@ -1264,7 +1373,7 @@ async function listRuntimes(showAll = false) {
1264
1373
  }
1265
1374
  }
1266
1375
  async function attachRuntime(target, readOnly = false, takeover = false) {
1267
- const client = new import_session_host_core3.SessionHostClient({ endpoint: (0, import_session_host_core3.getDefaultSessionHostEndpoint)(SESSION_HOST_APP_NAME) });
1376
+ const client = new import_session_host_core5.SessionHostClient({ endpoint: (0, import_session_host_core5.getDefaultSessionHostEndpoint)(SESSION_HOST_APP_NAME) });
1268
1377
  const clientId = `local-terminal-${process.pid}-${(0, import_crypto.randomUUID)().slice(0, 8)}`;
1269
1378
  let lastSeq = 0;
1270
1379
  let restoredRawMode = false;
@@ -1359,7 +1468,7 @@ async function attachRuntime(target, readOnly = false, takeover = false) {
1359
1468
  if (!listResponse.success || !listResponse.result) {
1360
1469
  throw new Error(listResponse.error || "Failed to list runtimes");
1361
1470
  }
1362
- let runtimeRecord = (0, import_session_host_core3.resolveAttachableRuntimeRecord)(listResponse.result, target);
1471
+ let runtimeRecord = (0, import_session_host_core5.resolveAttachableRuntimeRecord)(listResponse.result, target);
1363
1472
  runtimeId = runtimeRecord.sessionId;
1364
1473
  if (runtimeRecord.lifecycle === "interrupted" && !readOnly) {
1365
1474
  const resumeResponse = await client.request({
@@ -1502,13 +1611,13 @@ async function main() {
1502
1611
  if (!target) {
1503
1612
  throw new Error("runtime target is required: adhdev-sessiond resume <runtimeId|runtimeKey>");
1504
1613
  }
1505
- const client = new import_session_host_core3.SessionHostClient({ endpoint: (0, import_session_host_core3.getDefaultSessionHostEndpoint)(SESSION_HOST_APP_NAME) });
1614
+ const client = new import_session_host_core5.SessionHostClient({ endpoint: (0, import_session_host_core5.getDefaultSessionHostEndpoint)(SESSION_HOST_APP_NAME) });
1506
1615
  try {
1507
1616
  const listResponse = await client.request({ type: "list_sessions", payload: {} });
1508
1617
  if (!listResponse.success || !listResponse.result) {
1509
1618
  throw new Error(listResponse.error || "Failed to list runtimes");
1510
1619
  }
1511
- const runtimeRecord = (0, import_session_host_core3.resolveRuntimeRecord)(listResponse.result, target);
1620
+ const runtimeRecord = (0, import_session_host_core5.resolveRuntimeRecord)(listResponse.result, target);
1512
1621
  const resumeResponse = await client.request({
1513
1622
  type: "resume_session",
1514
1623
  payload: {