@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.
@@ -24,13 +24,10 @@ import { EventEmitter } from "events";
24
24
  import * as fs3 from "fs";
25
25
  import * as net from "net";
26
26
  import {
27
- SESSION_HOST_SUPPORTED_REQUEST_TYPES,
28
27
  SessionHostRegistry,
29
28
  createLineParser,
30
29
  createResponseEnvelope,
31
- getDefaultSessionHostEndpoint,
32
- resolveSessionHostCols as resolveSessionHostCols2,
33
- resolveSessionHostRows as resolveSessionHostRows2
30
+ getDefaultSessionHostEndpoint
34
31
  } from "@adhdev/session-host-core";
35
32
 
36
33
  // src/runtime.ts
@@ -74,7 +71,8 @@ function formatXtermViewportPlain(terminal, rows) {
74
71
  const lines = [];
75
72
  for (let i = start; i < end; i++) {
76
73
  const line = buffer.getLine(i);
77
- lines.push(line ? line.translateToString(true) : "");
74
+ const raw = line ? line.translateToString(false) : "";
75
+ lines.push(raw.replace(/\s+$/, ""));
78
76
  }
79
77
  let first = 0;
80
78
  let last = lines.length;
@@ -137,6 +135,9 @@ function createXtermMirror(options) {
137
135
  if (serializer) return serializeXtermViewport(terminal, serializer, currentRows);
138
136
  return formatXtermViewportPlain(terminal, currentRows);
139
137
  },
138
+ formatPlainText() {
139
+ return formatXtermViewportPlain(terminal, currentRows).replace(/\r\n/g, "\n");
140
+ },
140
141
  getCursorPosition() {
141
142
  const buffer = terminal.buffer.active;
142
143
  return {
@@ -169,6 +170,9 @@ function normalizeGhosttyBinding(mod) {
169
170
  formatVT() {
170
171
  return viewportSnapshot.formatVT();
171
172
  },
173
+ formatPlainText() {
174
+ return viewportSnapshot.formatPlainText();
175
+ },
172
176
  getCursorPosition() {
173
177
  if (typeof handle.getCursorPosition === "function") return handle.getCursorPosition();
174
178
  return viewportSnapshot.getCursorPosition();
@@ -217,6 +221,10 @@ var PtySessionRuntime = class {
217
221
  ptyProcess = null;
218
222
  screenMirror = null;
219
223
  pendingQueryScanTail = "";
224
+ terminalModeScanTail = "";
225
+ altScreen = false;
226
+ pasteMode = false;
227
+ scrollRegion;
220
228
  onDataCallback;
221
229
  onExitCallback;
222
230
  constructor(options) {
@@ -224,6 +232,7 @@ var PtySessionRuntime = class {
224
232
  this.payload = options.payload;
225
233
  this.cols = resolveSessionHostCols(options.payload.cols);
226
234
  this.rows = resolveSessionHostRows(options.payload.rows);
235
+ this.scrollRegion = { top: 0, bot: this.rows - 1 };
227
236
  this.onDataCallback = options.onData;
228
237
  this.onExitCallback = options.onExit;
229
238
  }
@@ -255,6 +264,7 @@ var PtySessionRuntime = class {
255
264
  });
256
265
  this.ptyProcess.onData((data) => {
257
266
  this.screenMirror?.write(data);
267
+ this.trackTerminalModes(data);
258
268
  this.respondToTerminalQueries(data);
259
269
  this.onDataCallback(data);
260
270
  });
@@ -263,6 +273,7 @@ var PtySessionRuntime = class {
263
273
  this.screenMirror?.dispose();
264
274
  this.screenMirror = null;
265
275
  this.pendingQueryScanTail = "";
276
+ this.terminalModeScanTail = "";
266
277
  this.onExitCallback(exitCode ?? null);
267
278
  });
268
279
  return this.ptyProcess.pid;
@@ -274,6 +285,12 @@ var PtySessionRuntime = class {
274
285
  resize(cols, rows) {
275
286
  if (!this.ptyProcess) throw new Error(`Session not running: ${this.sessionId}`);
276
287
  this.ptyProcess.resize(cols, rows);
288
+ this.cols = Math.max(1, cols | 0);
289
+ this.rows = Math.max(1, rows | 0);
290
+ this.scrollRegion = {
291
+ top: Math.min(this.scrollRegion.top, this.rows - 1),
292
+ bot: Math.min(Math.max(this.scrollRegion.top, this.scrollRegion.bot), this.rows - 1)
293
+ };
277
294
  this.screenMirror?.resize(cols, rows);
278
295
  }
279
296
  stop() {
@@ -297,6 +314,55 @@ var PtySessionRuntime = class {
297
314
  getSnapshotText() {
298
315
  return this.screenMirror?.formatVT() || "";
299
316
  }
317
+ getTerminalSnapshot() {
318
+ if (!this.ptyProcess || !this.screenMirror) {
319
+ throw new Error(`Session not running: ${this.sessionId}`);
320
+ }
321
+ const cursor = this.screenMirror.getCursorPosition();
322
+ return {
323
+ text: this.screenMirror.formatPlainText(),
324
+ state: {
325
+ cursor: {
326
+ row: Math.max(0, cursor.row | 0),
327
+ col: Math.max(0, cursor.col | 0)
328
+ },
329
+ altScreen: this.altScreen,
330
+ pasteMode: this.pasteMode,
331
+ rawMode: true,
332
+ scrollRegion: { ...this.scrollRegion },
333
+ cols: this.cols,
334
+ rows: this.rows
335
+ }
336
+ };
337
+ }
338
+ trackTerminalModes(data) {
339
+ if (!data) return;
340
+ const combined = this.terminalModeScanTail + data;
341
+ const privateMode = /\x1b\[\?([0-9;]*)([hl])/g;
342
+ let privateMatch;
343
+ while ((privateMatch = privateMode.exec(combined)) !== null) {
344
+ const enabled = privateMatch[2] === "h";
345
+ for (const mode of privateMatch[1].split(";")) {
346
+ if (mode === "47" || mode === "1047" || mode === "1049") this.altScreen = enabled;
347
+ if (mode === "2004") this.pasteMode = enabled;
348
+ }
349
+ }
350
+ const scrollRegion = /\x1b\[(\d*)(?:;(\d*))?r/g;
351
+ let scrollMatch;
352
+ while ((scrollMatch = scrollRegion.exec(combined)) !== null) {
353
+ const top = scrollMatch[1] ? Number.parseInt(scrollMatch[1], 10) - 1 : 0;
354
+ const bot = scrollMatch[2] ? Number.parseInt(scrollMatch[2], 10) - 1 : this.rows - 1;
355
+ this.scrollRegion = {
356
+ top: Math.max(0, Math.min(this.rows - 1, top)),
357
+ bot: Math.max(0, Math.min(this.rows - 1, bot))
358
+ };
359
+ if (this.scrollRegion.bot < this.scrollRegion.top) {
360
+ this.scrollRegion = { top: 0, bot: this.rows - 1 };
361
+ }
362
+ }
363
+ const lastEscape = combined.lastIndexOf("\x1B");
364
+ this.terminalModeScanTail = lastEscape >= 0 && combined.length - lastEscape < 64 ? combined.slice(lastEscape) : "";
365
+ }
300
366
  respondToTerminalQueries(data) {
301
367
  if (!this.ptyProcess || !this.screenMirror || !data) return;
302
368
  const combined = this.pendingQueryScanTail + data;
@@ -361,9 +427,203 @@ var SessionHostStorage = class {
361
427
  }
362
428
  };
363
429
 
430
+ // src/session-diagnostics.ts
431
+ import {
432
+ SESSION_HOST_SUPPORTED_REQUEST_TYPES
433
+ } from "@adhdev/session-host-core";
434
+ var MAX_RECENT_DIAGNOSTICS = 200;
435
+ function pushRecent(bucket, entry, max = MAX_RECENT_DIAGNOSTICS) {
436
+ bucket.push(entry);
437
+ if (bucket.length > max) {
438
+ bucket.splice(0, bucket.length - max);
439
+ }
440
+ }
441
+ function getSessionHostRecoveryLabel(record) {
442
+ const recoveryState = typeof record.meta?.runtimeRecoveryState === "string" ? String(record.meta.runtimeRecoveryState).trim() : "";
443
+ if (!recoveryState) return null;
444
+ if (recoveryState === "auto_resumed") return "restored after restart";
445
+ if (recoveryState === "resume_failed") return "restore failed";
446
+ if (recoveryState === "host_restart_interrupted") return "host restart interrupted";
447
+ if (recoveryState === "orphan_snapshot") return "snapshot recovered";
448
+ return recoveryState.replace(/_/g, " ");
449
+ }
450
+ function getSessionSurfaceKind(record) {
451
+ if (["starting", "running", "stopping", "interrupted"].includes(record.lifecycle)) {
452
+ return "live_runtime";
453
+ }
454
+ if ((record.lifecycle === "stopped" || record.lifecycle === "failed") && (record.meta?.restoredFromStorage === true || getSessionHostRecoveryLabel(record))) {
455
+ return "recovery_snapshot";
456
+ }
457
+ return "inactive_record";
458
+ }
459
+ function annotateSessionSurface(record) {
460
+ return {
461
+ ...record,
462
+ surfaceKind: getSessionSurfaceKind(record)
463
+ };
464
+ }
465
+ function sanitizeDiagnosticsRecord(record) {
466
+ return {
467
+ ...record,
468
+ launchCommand: {
469
+ command: record.launchCommand.command,
470
+ args: Array.isArray(record.launchCommand.args) ? [...record.launchCommand.args] : []
471
+ }
472
+ };
473
+ }
474
+ function buildHostDiagnostics(params) {
475
+ const limit = Math.max(1, Math.min(200, Number(params.payload?.limit) || 50));
476
+ const allSessions = params.payload?.includeSessions === false ? void 0 : params.sessions.map((record) => annotateSessionSurface(record)).map((record) => sanitizeDiagnosticsRecord(record));
477
+ const liveRuntimes = allSessions?.filter((record) => record.surfaceKind === "live_runtime");
478
+ const recoverySnapshots = allSessions?.filter((record) => record.surfaceKind === "recovery_snapshot").slice(0, limit);
479
+ const inactiveRecords = allSessions?.filter((record) => record.surfaceKind === "inactive_record").slice(0, limit);
480
+ const sessions = allSessions ? [
481
+ ...liveRuntimes || [],
482
+ ...recoverySnapshots || [],
483
+ ...inactiveRecords || []
484
+ ] : void 0;
485
+ return {
486
+ hostStartedAt: params.hostStartedAt,
487
+ endpoint: params.endpointPath,
488
+ runtimeCount: params.runtimeCount,
489
+ supportedRequestTypes: [...SESSION_HOST_SUPPORTED_REQUEST_TYPES],
490
+ sessions,
491
+ liveRuntimes,
492
+ recoverySnapshots,
493
+ inactiveRecords,
494
+ recentLogs: params.recentLogs.slice(-limit),
495
+ recentRequests: params.recentRequests.slice(-limit),
496
+ recentTransitions: params.recentTransitions.slice(-limit)
497
+ };
498
+ }
499
+
500
+ // src/session-lifecycle.ts
501
+ import {
502
+ resolveSessionHostCols as resolveSessionHostCols2,
503
+ resolveSessionHostRows as resolveSessionHostRows2
504
+ } from "@adhdev/session-host-core";
505
+ function compareDuplicateCandidates(a, b) {
506
+ const score = (record) => {
507
+ const lifecycleScore = record.lifecycle === "running" ? 4 : record.lifecycle === "starting" ? 3 : record.lifecycle === "stopping" ? 2 : record.lifecycle === "interrupted" ? 1 : 0;
508
+ return [
509
+ lifecycleScore,
510
+ record.writeOwner ? 1 : 0,
511
+ Array.isArray(record.attachedClients) ? record.attachedClients.length : 0,
512
+ record.lastActivityAt || 0,
513
+ record.startedAt || 0,
514
+ record.createdAt || 0
515
+ ];
516
+ };
517
+ const aScore = score(a);
518
+ const bScore = score(b);
519
+ for (let i = 0; i < aScore.length; i += 1) {
520
+ if (aScore[i] === bScore[i]) continue;
521
+ return bScore[i] - aScore[i];
522
+ }
523
+ return 0;
524
+ }
525
+ function planDuplicatePrune(sessions, filters) {
526
+ 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);
527
+ const groups = /* @__PURE__ */ new Map();
528
+ for (const record of candidates) {
529
+ const providerSessionId = typeof record.meta?.providerSessionId === "string" ? String(record.meta.providerSessionId).trim() : "";
530
+ if (!providerSessionId) continue;
531
+ const bindingKey = `${record.providerType}::${record.workspace}::${providerSessionId}`;
532
+ const bucket = groups.get(bindingKey) || [];
533
+ bucket.push(record);
534
+ groups.set(bindingKey, bucket);
535
+ }
536
+ const duplicateGroups = [];
537
+ const keptSessionIds = [];
538
+ const duplicateRecords = [];
539
+ for (const [bindingKey, records] of groups.entries()) {
540
+ if (records.length < 2) continue;
541
+ const sorted = [...records].sort((a, b) => compareDuplicateCandidates(a, b));
542
+ const kept = sorted[0];
543
+ const duplicates = sorted.slice(1);
544
+ const providerSessionId = typeof kept.meta?.providerSessionId === "string" ? String(kept.meta.providerSessionId) : "";
545
+ duplicateGroups.push({
546
+ bindingKey,
547
+ providerType: kept.providerType,
548
+ workspace: kept.workspace,
549
+ providerSessionId,
550
+ keptSessionId: kept.sessionId,
551
+ prunedSessionIds: duplicates.map((record) => record.sessionId)
552
+ });
553
+ keptSessionIds.push(kept.sessionId);
554
+ for (const duplicate of duplicates) {
555
+ duplicateRecords.push(duplicate);
556
+ }
557
+ }
558
+ return { duplicateGroups, keptSessionIds, duplicateRecords };
559
+ }
560
+ function buildPayloadFromRecord(record) {
561
+ return {
562
+ sessionId: record.sessionId,
563
+ runtimeKey: record.runtimeKey,
564
+ displayName: record.displayName,
565
+ providerType: record.providerType,
566
+ category: record.category,
567
+ workspace: record.workspace,
568
+ launchCommand: record.launchCommand,
569
+ cols: resolveSessionHostCols2(typeof record.meta?.sessionHostCols === "number" ? record.meta.sessionHostCols : void 0),
570
+ rows: resolveSessionHostRows2(typeof record.meta?.sessionHostRows === "number" ? record.meta.sessionHostRows : void 0),
571
+ meta: record.meta
572
+ };
573
+ }
574
+ function buildRecoveredRecord(persisted) {
575
+ const wasLiveRuntime = !["stopped", "failed"].includes(persisted.record.lifecycle);
576
+ const hadAttachedClients = Array.isArray(persisted.record.attachedClients) && persisted.record.attachedClients.length > 0;
577
+ const hadWriteOwner = !!persisted.record.writeOwner;
578
+ const hadRecoveryInterest = hadAttachedClients || hadWriteOwner;
579
+ const recoveredRecord = {
580
+ ...persisted.record,
581
+ attachedClients: [],
582
+ writeOwner: null,
583
+ lifecycle: wasLiveRuntime ? "stopped" : persisted.record.lifecycle,
584
+ lastActivityAt: Date.now(),
585
+ meta: {
586
+ ...persisted.record.meta || {},
587
+ restoredFromStorage: true,
588
+ runtimeRecoveryState: wasLiveRuntime ? "orphan_snapshot" : "snapshot",
589
+ runtimeHadAttachedClientsAtCrash: hadAttachedClients,
590
+ runtimeHadWriteOwnerAtCrash: hadWriteOwner,
591
+ runtimeAutoResumeSkipped: wasLiveRuntime && hadRecoveryInterest
592
+ }
593
+ };
594
+ return { recoveredRecord, wasLiveRuntime, hadRecoveryInterest };
595
+ }
596
+
597
+ // src/session-protocol.ts
598
+ function getRequestSessionId(request) {
599
+ const payload = request.payload;
600
+ return typeof payload?.sessionId === "string" ? payload.sessionId : void 0;
601
+ }
602
+ function getRequestClientId(request) {
603
+ const payload = request.payload;
604
+ return typeof payload?.clientId === "string" ? payload.clientId : void 0;
605
+ }
606
+ function mergeRuntimeSnapshot(base, record, opts) {
607
+ const cols = typeof record?.meta?.sessionHostCols === "number" ? record.meta.sessionHostCols : 80;
608
+ const rows = typeof record?.meta?.sessionHostRows === "number" ? record.meta.sessionHostRows : 24;
609
+ if (typeof opts.sinceSeq === "number" || !opts.runtimeText) {
610
+ return {
611
+ ...base,
612
+ cols,
613
+ rows
614
+ };
615
+ }
616
+ return {
617
+ ...base,
618
+ text: opts.runtimeText,
619
+ truncated: false,
620
+ cols,
621
+ rows
622
+ };
623
+ }
624
+
364
625
  // src/server.ts
365
- var SessionHostServer = class _SessionHostServer extends EventEmitter {
366
- static MAX_RECENT_DIAGNOSTICS = 200;
626
+ var SessionHostServer = class extends EventEmitter {
367
627
  endpoint;
368
628
  registry = new SessionHostRegistry();
369
629
  runtimes = /* @__PURE__ */ new Map();
@@ -510,6 +770,8 @@ var SessionHostServer = class _SessionHostServer extends EventEmitter {
510
770
  }
511
771
  case "get_snapshot":
512
772
  return { success: true, result: this.getSnapshot(request.payload.sessionId, request.payload.sinceSeq) };
773
+ case "get_terminal_snapshot":
774
+ return { success: true, result: this.requireRuntime(request.payload.sessionId).getTerminalSnapshot() };
513
775
  case "get_host_diagnostics":
514
776
  return { success: true, result: this.getHostDiagnostics(request.payload) };
515
777
  case "clear_session_buffer": {
@@ -610,7 +872,7 @@ var SessionHostServer = class _SessionHostServer extends EventEmitter {
610
872
  if (this.runtimes.has(request.payload.sessionId)) {
611
873
  return { success: true, result: existing };
612
874
  }
613
- const resumed = this.startRuntime(existing, this.buildPayloadFromRecord(existing), "session_resumed");
875
+ const resumed = this.startRuntime(existing, buildPayloadFromRecord(existing), "session_resumed");
614
876
  this.recordRuntimeTransition(request.payload.sessionId, "resume_session", resumed.lifecycle, void 0, true);
615
877
  return { success: true, result: resumed };
616
878
  }
@@ -692,7 +954,7 @@ var SessionHostServer = class _SessionHostServer extends EventEmitter {
692
954
  sessions.add(sessionId);
693
955
  }
694
956
  async handleIncomingRequest(socket, envelope) {
695
- const sessionId = this.getRequestSessionId(envelope.request);
957
+ const sessionId = getRequestSessionId(envelope.request);
696
958
  if (sessionId && (envelope.request.type === "create_session" || envelope.request.type === "attach_session")) {
697
959
  this.subscribeSocketToSession(socket, sessionId);
698
960
  }
@@ -706,8 +968,8 @@ var SessionHostServer = class _SessionHostServer extends EventEmitter {
706
968
  timestamp: startedAt,
707
969
  requestId: envelope.requestId,
708
970
  type: envelope.request.type,
709
- sessionId: this.getRequestSessionId(envelope.request),
710
- clientId: this.getRequestClientId(envelope.request),
971
+ sessionId: getRequestSessionId(envelope.request),
972
+ clientId: getRequestClientId(envelope.request),
711
973
  success: response.success,
712
974
  durationMs: Math.max(0, Date.now() - startedAt),
713
975
  error: response.success ? void 0 : response.error
@@ -750,79 +1012,24 @@ var SessionHostServer = class _SessionHostServer extends EventEmitter {
750
1012
  const record = this.registry.getSession(sessionId);
751
1013
  if (!record) return;
752
1014
  const snapshot = this.getSnapshot(sessionId);
753
- this.storage.save(record, snapshot);
754
- }
755
- getSessionHostRecoveryLabel(record) {
756
- const recoveryState = typeof record.meta?.runtimeRecoveryState === "string" ? String(record.meta.runtimeRecoveryState).trim() : "";
757
- if (!recoveryState) return null;
758
- if (recoveryState === "auto_resumed") return "restored after restart";
759
- if (recoveryState === "resume_failed") return "restore failed";
760
- if (recoveryState === "host_restart_interrupted") return "host restart interrupted";
761
- if (recoveryState === "orphan_snapshot") return "snapshot recovered";
762
- return recoveryState.replace(/_/g, " ");
763
- }
764
- getSessionSurfaceKind(record) {
765
- if (["starting", "running", "stopping", "interrupted"].includes(record.lifecycle)) {
766
- return "live_runtime";
767
- }
768
- if ((record.lifecycle === "stopped" || record.lifecycle === "failed") && (record.meta?.restoredFromStorage === true || this.getSessionHostRecoveryLabel(record))) {
769
- return "recovery_snapshot";
1015
+ try {
1016
+ this.storage.save(record, snapshot);
1017
+ } catch (error) {
1018
+ const code = typeof error?.code === "string" ? error.code : "persist_failed";
1019
+ console.error(`[session-host] Persist failed for ${sessionId}: ${code}: ${error?.message || error}`);
770
1020
  }
771
- return "inactive_record";
772
- }
773
- annotateSessionSurface(record) {
774
- return {
775
- ...record,
776
- surfaceKind: this.getSessionSurfaceKind(record)
777
- };
778
- }
779
- sanitizeDiagnosticsRecord(record) {
780
- return {
781
- ...record,
782
- launchCommand: {
783
- command: record.launchCommand.command,
784
- args: Array.isArray(record.launchCommand.args) ? [...record.launchCommand.args] : []
785
- }
786
- };
787
1021
  }
788
1022
  getHostDiagnostics(payload) {
789
- const limit = Math.max(1, Math.min(200, Number(payload?.limit) || 50));
790
- const allSessions = payload?.includeSessions === false ? void 0 : this.registry.listSessions().map((record) => this.annotateSessionSurface(record)).map((record) => this.sanitizeDiagnosticsRecord(record));
791
- const liveRuntimes = allSessions?.filter((record) => record.surfaceKind === "live_runtime");
792
- const recoverySnapshots = allSessions?.filter((record) => record.surfaceKind === "recovery_snapshot").slice(0, limit);
793
- const inactiveRecords = allSessions?.filter((record) => record.surfaceKind === "inactive_record").slice(0, limit);
794
- const sessions = allSessions ? [
795
- ...liveRuntimes || [],
796
- ...recoverySnapshots || [],
797
- ...inactiveRecords || []
798
- ] : void 0;
799
- return {
1023
+ return buildHostDiagnostics({
1024
+ payload,
800
1025
  hostStartedAt: this.startedAt,
801
- endpoint: this.endpoint.path,
1026
+ endpointPath: this.endpoint.path,
802
1027
  runtimeCount: this.runtimes.size,
803
- supportedRequestTypes: [...SESSION_HOST_SUPPORTED_REQUEST_TYPES],
804
- sessions,
805
- liveRuntimes,
806
- recoverySnapshots,
807
- inactiveRecords,
808
- recentLogs: this.recentLogs.slice(-limit),
809
- recentRequests: this.recentRequests.slice(-limit),
810
- recentTransitions: this.recentTransitions.slice(-limit)
811
- };
812
- }
813
- getRequestSessionId(request) {
814
- const payload = request.payload;
815
- return typeof payload?.sessionId === "string" ? payload.sessionId : void 0;
816
- }
817
- getRequestClientId(request) {
818
- const payload = request.payload;
819
- return typeof payload?.clientId === "string" ? payload.clientId : void 0;
820
- }
821
- pushRecent(bucket, entry) {
822
- bucket.push(entry);
823
- if (bucket.length > _SessionHostServer.MAX_RECENT_DIAGNOSTICS) {
824
- bucket.splice(0, bucket.length - _SessionHostServer.MAX_RECENT_DIAGNOSTICS);
825
- }
1028
+ sessions: this.registry.listSessions(),
1029
+ recentLogs: this.recentLogs,
1030
+ recentRequests: this.recentRequests,
1031
+ recentTransitions: this.recentTransitions
1032
+ });
826
1033
  }
827
1034
  recordHostLog(level, message, sessionId, data) {
828
1035
  const entry = {
@@ -832,12 +1039,12 @@ var SessionHostServer = class _SessionHostServer extends EventEmitter {
832
1039
  sessionId,
833
1040
  data
834
1041
  };
835
- this.pushRecent(this.recentLogs, entry);
1042
+ pushRecent(this.recentLogs, entry);
836
1043
  this.emitEvent({ type: "host_log", entry });
837
1044
  this.emit("log", `[${level}] ${message}`);
838
1045
  }
839
1046
  recordRequestTrace(trace) {
840
- this.pushRecent(this.recentRequests, trace);
1047
+ pushRecent(this.recentRequests, trace);
841
1048
  this.emitEvent({ type: "request_trace", trace });
842
1049
  if (!trace.success) {
843
1050
  this.recordHostLog(
@@ -907,7 +1114,7 @@ var SessionHostServer = class _SessionHostServer extends EventEmitter {
907
1114
  success,
908
1115
  error
909
1116
  };
910
- this.pushRecent(this.recentTransitions, transition);
1117
+ pushRecent(this.recentTransitions, transition);
911
1118
  this.emitEvent({ type: "runtime_transition", transition });
912
1119
  }
913
1120
  waitForRuntimeExit(sessionId, timeoutMs = 5e3) {
@@ -943,29 +1150,8 @@ var SessionHostServer = class _SessionHostServer extends EventEmitter {
943
1150
  getSnapshot(sessionId, sinceSeq) {
944
1151
  const snapshot = this.registry.getSnapshot(sessionId, sinceSeq);
945
1152
  const record = this.registry.getSession(sessionId);
946
- if (typeof sinceSeq === "number") {
947
- return {
948
- ...snapshot,
949
- cols: typeof record?.meta?.sessionHostCols === "number" ? record.meta.sessionHostCols : 80,
950
- rows: typeof record?.meta?.sessionHostRows === "number" ? record.meta.sessionHostRows : 24
951
- };
952
- }
953
- const runtime = this.runtimes.get(sessionId);
954
- const runtimeText = runtime?.getSnapshotText?.() || "";
955
- if (!runtimeText) {
956
- return {
957
- ...snapshot,
958
- cols: typeof record?.meta?.sessionHostCols === "number" ? record.meta.sessionHostCols : 80,
959
- rows: typeof record?.meta?.sessionHostRows === "number" ? record.meta.sessionHostRows : 24
960
- };
961
- }
962
- return {
963
- ...snapshot,
964
- text: runtimeText,
965
- truncated: false,
966
- cols: typeof record?.meta?.sessionHostCols === "number" ? record.meta.sessionHostCols : 80,
967
- rows: typeof record?.meta?.sessionHostRows === "number" ? record.meta.sessionHostRows : 24
968
- };
1153
+ const runtimeText = typeof sinceSeq === "number" ? "" : this.runtimes.get(sessionId)?.getSnapshotText?.() || "";
1154
+ return mergeRuntimeSnapshot(snapshot, record, { sinceSeq, runtimeText });
969
1155
  }
970
1156
  flushAllPersistence() {
971
1157
  for (const sessionId of this.runtimes.keys()) {
@@ -988,7 +1174,7 @@ var SessionHostServer = class _SessionHostServer extends EventEmitter {
988
1174
  await this.waitForRuntimeExit(sessionId);
989
1175
  }
990
1176
  const latest = this.registry.getSession(sessionId) || existing;
991
- const restarted = this.startRuntime(latest, this.buildPayloadFromRecord(latest), "session_resumed");
1177
+ const restarted = this.startRuntime(latest, buildPayloadFromRecord(latest), "session_resumed");
992
1178
  this.recordRuntimeTransition(sessionId, "restart_completed", restarted.lifecycle, void 0, true);
993
1179
  return restarted;
994
1180
  }
@@ -996,36 +1182,13 @@ var SessionHostServer = class _SessionHostServer extends EventEmitter {
996
1182
  const providerFilter = typeof payload?.providerType === "string" ? payload.providerType.trim() : "";
997
1183
  const workspaceFilter = typeof payload?.workspace === "string" ? payload.workspace.trim() : "";
998
1184
  const dryRun = payload?.dryRun === true;
999
- 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);
1000
- const groups = /* @__PURE__ */ new Map();
1001
- for (const record of sessions) {
1002
- const providerSessionId = typeof record.meta?.providerSessionId === "string" ? String(record.meta.providerSessionId).trim() : "";
1003
- if (!providerSessionId) continue;
1004
- const bindingKey = `${record.providerType}::${record.workspace}::${providerSessionId}`;
1005
- const bucket = groups.get(bindingKey) || [];
1006
- bucket.push(record);
1007
- groups.set(bindingKey, bucket);
1008
- }
1009
- const duplicateGroups = [];
1010
- const keptSessionIds = [];
1185
+ const { duplicateGroups, keptSessionIds, duplicateRecords } = planDuplicatePrune(
1186
+ this.registry.listSessions(),
1187
+ { providerFilter, workspaceFilter }
1188
+ );
1011
1189
  const prunedSessionIds = [];
1012
- for (const [bindingKey, records] of groups.entries()) {
1013
- if (records.length < 2) continue;
1014
- const sorted = [...records].sort((a, b) => this.compareDuplicateCandidates(a, b));
1015
- const kept = sorted[0];
1016
- const duplicates = sorted.slice(1);
1017
- const providerSessionId = typeof kept.meta?.providerSessionId === "string" ? String(kept.meta.providerSessionId) : "";
1018
- duplicateGroups.push({
1019
- bindingKey,
1020
- providerType: kept.providerType,
1021
- workspace: kept.workspace,
1022
- providerSessionId,
1023
- keptSessionId: kept.sessionId,
1024
- prunedSessionIds: duplicates.map((record) => record.sessionId)
1025
- });
1026
- keptSessionIds.push(kept.sessionId);
1027
- if (dryRun) continue;
1028
- for (const duplicate of duplicates) {
1190
+ if (!dryRun) {
1191
+ for (const duplicate of duplicateRecords) {
1029
1192
  await this.pruneDuplicateRuntime(duplicate);
1030
1193
  prunedSessionIds.push(duplicate.sessionId);
1031
1194
  }
@@ -1053,25 +1216,7 @@ var SessionHostServer = class _SessionHostServer extends EventEmitter {
1053
1216
  const states = this.storage.loadAll();
1054
1217
  let skippedAutoResumeSessions = 0;
1055
1218
  for (const persisted of states) {
1056
- const wasLiveRuntime = !["stopped", "failed"].includes(persisted.record.lifecycle);
1057
- const hadAttachedClients = Array.isArray(persisted.record.attachedClients) && persisted.record.attachedClients.length > 0;
1058
- const hadWriteOwner = !!persisted.record.writeOwner;
1059
- const hadRecoveryInterest = hadAttachedClients || hadWriteOwner;
1060
- const recoveredRecord = {
1061
- ...persisted.record,
1062
- attachedClients: [],
1063
- writeOwner: null,
1064
- lifecycle: wasLiveRuntime ? "stopped" : persisted.record.lifecycle,
1065
- lastActivityAt: Date.now(),
1066
- meta: {
1067
- ...persisted.record.meta || {},
1068
- restoredFromStorage: true,
1069
- runtimeRecoveryState: wasLiveRuntime ? "orphan_snapshot" : "snapshot",
1070
- runtimeHadAttachedClientsAtCrash: hadAttachedClients,
1071
- runtimeHadWriteOwnerAtCrash: hadWriteOwner,
1072
- runtimeAutoResumeSkipped: wasLiveRuntime && hadRecoveryInterest
1073
- }
1074
- };
1219
+ const { recoveredRecord, wasLiveRuntime, hadRecoveryInterest } = buildRecoveredRecord(persisted);
1075
1220
  this.registry.restoreSession(recoveredRecord, persisted.snapshot);
1076
1221
  this.storage.save(recoveredRecord, persisted.snapshot);
1077
1222
  if (wasLiveRuntime && hadRecoveryInterest) {
@@ -1082,26 +1227,6 @@ var SessionHostServer = class _SessionHostServer extends EventEmitter {
1082
1227
  this.recordHostLog("warn", `session host restored ${skippedAutoResumeSessions} live runtime snapshot(s) without auto-resume`);
1083
1228
  }
1084
1229
  }
1085
- compareDuplicateCandidates(a, b) {
1086
- const score = (record) => {
1087
- const lifecycleScore = record.lifecycle === "running" ? 4 : record.lifecycle === "starting" ? 3 : record.lifecycle === "stopping" ? 2 : record.lifecycle === "interrupted" ? 1 : 0;
1088
- return [
1089
- lifecycleScore,
1090
- record.writeOwner ? 1 : 0,
1091
- Array.isArray(record.attachedClients) ? record.attachedClients.length : 0,
1092
- record.lastActivityAt || 0,
1093
- record.startedAt || 0,
1094
- record.createdAt || 0
1095
- ];
1096
- };
1097
- const aScore = score(a);
1098
- const bScore = score(b);
1099
- for (let i = 0; i < aScore.length; i += 1) {
1100
- if (aScore[i] === bScore[i]) continue;
1101
- return bScore[i] - aScore[i];
1102
- }
1103
- return 0;
1104
- }
1105
1230
  async pruneDuplicateRuntime(record) {
1106
1231
  const providerSessionId = typeof record.meta?.providerSessionId === "string" ? String(record.meta.providerSessionId) : void 0;
1107
1232
  this.recordRuntimeTransition(
@@ -1122,20 +1247,6 @@ var SessionHostServer = class _SessionHostServer extends EventEmitter {
1122
1247
  this.registry.deleteSession(record.sessionId);
1123
1248
  this.storage.remove(record.sessionId);
1124
1249
  }
1125
- buildPayloadFromRecord(record) {
1126
- return {
1127
- sessionId: record.sessionId,
1128
- runtimeKey: record.runtimeKey,
1129
- displayName: record.displayName,
1130
- providerType: record.providerType,
1131
- category: record.category,
1132
- workspace: record.workspace,
1133
- launchCommand: record.launchCommand,
1134
- cols: resolveSessionHostCols2(typeof record.meta?.sessionHostCols === "number" ? record.meta.sessionHostCols : void 0),
1135
- rows: resolveSessionHostRows2(typeof record.meta?.sessionHostRows === "number" ? record.meta.sessionHostRows : void 0),
1136
- meta: record.meta
1137
- };
1138
- }
1139
1250
  startRuntime(record, payload, startEventType) {
1140
1251
  const runtime = new PtySessionRuntime({
1141
1252
  sessionId: record.sessionId,