devchain-cli 0.17.0 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/node_modules/@devchain/codebase-overview/tsconfig.tsbuildinfo +1 -1
  2. package/dist/node_modules/@devchain/shared/tsconfig.tsbuildinfo +1 -1
  3. package/dist/server/app.main.module.js +2 -0
  4. package/dist/server/app.main.module.js.map +1 -1
  5. package/dist/server/app.normal.module.js +2 -0
  6. package/dist/server/app.normal.module.js.map +1 -1
  7. package/dist/server/modules/events/catalog/broadcast-registry.js +19 -12
  8. package/dist/server/modules/events/catalog/broadcast-registry.js.map +1 -1
  9. package/dist/server/modules/events/catalog/index.d.ts +23 -3
  10. package/dist/server/modules/events/catalog/session.transcript.updated.d.ts +23 -3
  11. package/dist/server/modules/events/catalog/session.transcript.updated.js +41 -19
  12. package/dist/server/modules/events/catalog/session.transcript.updated.js.map +1 -1
  13. package/dist/server/modules/metrics/controllers/metrics.controller.d.ts +6 -0
  14. package/dist/server/modules/metrics/controllers/metrics.controller.js +38 -0
  15. package/dist/server/modules/metrics/controllers/metrics.controller.js.map +1 -0
  16. package/dist/server/modules/metrics/helpers/byte-accounting.helper.d.ts +17 -0
  17. package/dist/server/modules/metrics/helpers/byte-accounting.helper.js +88 -0
  18. package/dist/server/modules/metrics/helpers/byte-accounting.helper.js.map +1 -0
  19. package/dist/server/modules/metrics/metrics.module.d.ts +2 -0
  20. package/dist/server/modules/metrics/metrics.module.js +23 -0
  21. package/dist/server/modules/metrics/metrics.module.js.map +1 -0
  22. package/dist/server/modules/metrics/services/metrics.service.d.ts +20 -0
  23. package/dist/server/modules/metrics/services/metrics.service.js +241 -0
  24. package/dist/server/modules/metrics/services/metrics.service.js.map +1 -0
  25. package/dist/server/modules/metrics/types/metrics.types.d.ts +100 -0
  26. package/dist/server/modules/metrics/types/metrics.types.js +3 -0
  27. package/dist/server/modules/metrics/types/metrics.types.js.map +1 -0
  28. package/dist/server/modules/projects/helpers/profile-mapping.helpers.d.ts +5 -3
  29. package/dist/server/modules/projects/helpers/profile-mapping.helpers.js +25 -13
  30. package/dist/server/modules/projects/helpers/profile-mapping.helpers.js.map +1 -1
  31. package/dist/server/modules/projects/helpers/project-import.js +9 -7
  32. package/dist/server/modules/projects/helpers/project-import.js.map +1 -1
  33. package/dist/server/modules/projects/helpers/template-loader.js +11 -9
  34. package/dist/server/modules/projects/helpers/template-loader.js.map +1 -1
  35. package/dist/server/modules/projects/template-codec/codec-registry.js.map +1 -1
  36. package/dist/server/modules/projects/template-codec/codecs/agents.codec.d.ts +1 -1
  37. package/dist/server/modules/projects/template-codec/codecs/agents.codec.js +4 -4
  38. package/dist/server/modules/projects/template-codec/codecs/agents.codec.js.map +1 -1
  39. package/dist/server/modules/projects/template-codec/codecs/profiles.codec.d.ts +1 -1
  40. package/dist/server/modules/projects/template-codec/codecs/profiles.codec.js +27 -8
  41. package/dist/server/modules/projects/template-codec/codecs/profiles.codec.js.map +1 -1
  42. package/dist/server/modules/projects/template-codec/codecs/watchers.codec.js +1 -1
  43. package/dist/server/modules/projects/template-codec/codecs/watchers.codec.js.map +1 -1
  44. package/dist/server/modules/projects/template-codec/import-context.d.ts +6 -1
  45. package/dist/server/modules/projects/template-codec/import-context.js.map +1 -1
  46. package/dist/server/modules/projects/template-codec/template-section-codec.d.ts +2 -3
  47. package/dist/server/modules/session-reader/adapters/antigravity-session-reader.adapter.d.ts +6 -0
  48. package/dist/server/modules/session-reader/adapters/antigravity-session-reader.adapter.js +66 -0
  49. package/dist/server/modules/session-reader/adapters/antigravity-session-reader.adapter.js.map +1 -1
  50. package/dist/server/modules/session-reader/adapters/claude-session-reader.adapter.d.ts +5 -0
  51. package/dist/server/modules/session-reader/adapters/claude-session-reader.adapter.js +12 -0
  52. package/dist/server/modules/session-reader/adapters/claude-session-reader.adapter.js.map +1 -1
  53. package/dist/server/modules/session-reader/adapters/codex-session-reader.adapter.d.ts +5 -0
  54. package/dist/server/modules/session-reader/adapters/codex-session-reader.adapter.js +12 -0
  55. package/dist/server/modules/session-reader/adapters/codex-session-reader.adapter.js.map +1 -1
  56. package/dist/server/modules/session-reader/adapters/copilot-session-reader.adapter.d.ts +5 -0
  57. package/dist/server/modules/session-reader/adapters/copilot-session-reader.adapter.js +12 -0
  58. package/dist/server/modules/session-reader/adapters/copilot-session-reader.adapter.js.map +1 -1
  59. package/dist/server/modules/session-reader/adapters/opencode-session-reader.adapter.d.ts +5 -0
  60. package/dist/server/modules/session-reader/adapters/opencode-session-reader.adapter.js +15 -0
  61. package/dist/server/modules/session-reader/adapters/opencode-session-reader.adapter.js.map +1 -1
  62. package/dist/server/modules/session-reader/adapters/session-reader-adapter.interface.d.ts +10 -0
  63. package/dist/server/modules/session-reader/adapters/session-reader-adapter.interface.js +28 -1
  64. package/dist/server/modules/session-reader/adapters/session-reader-adapter.interface.js.map +1 -1
  65. package/dist/server/modules/session-reader/controllers/session-reader.controller.d.ts +10 -4
  66. package/dist/server/modules/session-reader/controllers/session-reader.controller.js +18 -19
  67. package/dist/server/modules/session-reader/controllers/session-reader.controller.js.map +1 -1
  68. package/dist/server/modules/session-reader/data/pricing.json +206 -0
  69. package/dist/server/modules/session-reader/parsers/claude-jsonl.parser.d.ts +1 -0
  70. package/dist/server/modules/session-reader/parsers/claude-jsonl.parser.js +7 -3
  71. package/dist/server/modules/session-reader/parsers/claude-jsonl.parser.js.map +1 -1
  72. package/dist/server/modules/session-reader/parsers/codex-jsonl.parser.d.ts +1 -0
  73. package/dist/server/modules/session-reader/parsers/codex-jsonl.parser.js +22 -11
  74. package/dist/server/modules/session-reader/parsers/codex-jsonl.parser.js.map +1 -1
  75. package/dist/server/modules/session-reader/parsers/copilot-jsonl.parser.d.ts +1 -0
  76. package/dist/server/modules/session-reader/parsers/copilot-jsonl.parser.js +21 -9
  77. package/dist/server/modules/session-reader/parsers/copilot-jsonl.parser.js.map +1 -1
  78. package/dist/server/modules/session-reader/readers/opencode-sqlite.reader.d.ts +5 -1
  79. package/dist/server/modules/session-reader/readers/opencode-sqlite.reader.js +102 -0
  80. package/dist/server/modules/session-reader/readers/opencode-sqlite.reader.js.map +1 -1
  81. package/dist/server/modules/session-reader/services/session-cache.service.d.ts +71 -4
  82. package/dist/server/modules/session-reader/services/session-cache.service.js +427 -68
  83. package/dist/server/modules/session-reader/services/session-cache.service.js.map +1 -1
  84. package/dist/server/modules/session-reader/services/session-reader.service.d.ts +21 -4
  85. package/dist/server/modules/session-reader/services/session-reader.service.js +126 -40
  86. package/dist/server/modules/session-reader/services/session-reader.service.js.map +1 -1
  87. package/dist/server/modules/session-reader/services/transcript-cursor.d.ts +1 -1
  88. package/dist/server/modules/session-reader/services/transcript-cursor.js +2 -2
  89. package/dist/server/modules/session-reader/services/transcript-cursor.js.map +1 -1
  90. package/dist/server/modules/session-reader/services/transcript-watcher.service.d.ts +2 -0
  91. package/dist/server/modules/session-reader/services/transcript-watcher.service.js +61 -11
  92. package/dist/server/modules/session-reader/services/transcript-watcher.service.js.map +1 -1
  93. package/dist/server/modules/session-reader/session-reader.module.js +3 -1
  94. package/dist/server/modules/session-reader/session-reader.module.js.map +1 -1
  95. package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.d.ts +4 -4
  96. package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.js +11 -5
  97. package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.js.map +1 -1
  98. package/dist/server/modules/sessions/services/session-runtime/session-restore-pipeline.service.d.ts +3 -1
  99. package/dist/server/modules/sessions/services/session-runtime/session-restore-pipeline.service.js +11 -2
  100. package/dist/server/modules/sessions/services/session-runtime/session-restore-pipeline.service.js.map +1 -1
  101. package/dist/server/modules/terminal/dtos/ws-envelope.dto.d.ts +140 -1
  102. package/dist/server/modules/terminal/dtos/ws-envelope.dto.js +59 -2
  103. package/dist/server/modules/terminal/dtos/ws-envelope.dto.js.map +1 -1
  104. package/dist/server/modules/terminal/gateways/terminal.gateway.d.ts +33 -1
  105. package/dist/server/modules/terminal/gateways/terminal.gateway.js +471 -61
  106. package/dist/server/modules/terminal/gateways/terminal.gateway.js.map +1 -1
  107. package/dist/server/modules/terminal/services/process-executor/fake-process-executor.d.ts +1 -0
  108. package/dist/server/modules/terminal/services/process-executor/fake-process-executor.js +1 -0
  109. package/dist/server/modules/terminal/services/process-executor/fake-process-executor.js.map +1 -1
  110. package/dist/server/modules/terminal/services/pty.service.d.ts +8 -3
  111. package/dist/server/modules/terminal/services/pty.service.js +11 -2
  112. package/dist/server/modules/terminal/services/pty.service.js.map +1 -1
  113. package/dist/server/modules/terminal/services/terminal-io/capture.js +2 -0
  114. package/dist/server/modules/terminal/services/terminal-io/capture.js.map +1 -1
  115. package/dist/server/modules/terminal/services/terminal-io/delivery.d.ts +1 -0
  116. package/dist/server/modules/terminal/services/terminal-io/delivery.js.map +1 -1
  117. package/dist/server/modules/terminal/services/terminal-io/terminal-io.service.js +28 -1
  118. package/dist/server/modules/terminal/services/terminal-io/terminal-io.service.js.map +1 -1
  119. package/dist/server/modules/terminal/services/terminal-key-input/terminal-key-input.facade.d.ts +6 -0
  120. package/dist/server/modules/terminal/services/terminal-key-input/terminal-key-input.facade.js +26 -1
  121. package/dist/server/modules/terminal/services/terminal-key-input/terminal-key-input.facade.js.map +1 -1
  122. package/dist/server/modules/terminal/services/terminal-seed.service.d.ts +27 -3
  123. package/dist/server/modules/terminal/services/terminal-seed.service.js +75 -19
  124. package/dist/server/modules/terminal/services/terminal-seed.service.js.map +1 -1
  125. package/dist/server/modules/terminal/services/terminal-send-scheduler.service.d.ts +73 -0
  126. package/dist/server/modules/terminal/services/terminal-send-scheduler.service.js +387 -0
  127. package/dist/server/modules/terminal/services/terminal-send-scheduler.service.js.map +1 -0
  128. package/dist/server/modules/terminal/services/terminal-session/terminal-session.d.ts +4 -10
  129. package/dist/server/modules/terminal/services/terminal-session/terminal-session.js +20 -48
  130. package/dist/server/modules/terminal/services/terminal-session/terminal-session.js.map +1 -1
  131. package/dist/server/modules/terminal/services/terminal-socket-drain.adapter.d.ts +10 -0
  132. package/dist/server/modules/terminal/services/terminal-socket-drain.adapter.js +75 -0
  133. package/dist/server/modules/terminal/services/terminal-socket-drain.adapter.js.map +1 -0
  134. package/dist/server/modules/terminal/services/terminal-stream.service.d.ts +53 -6
  135. package/dist/server/modules/terminal/services/terminal-stream.service.js +190 -35
  136. package/dist/server/modules/terminal/services/terminal-stream.service.js.map +1 -1
  137. package/dist/server/modules/terminal/terminal.module.js +7 -0
  138. package/dist/server/modules/terminal/terminal.module.js.map +1 -1
  139. package/dist/server/templates/3-agents-dev.json +97 -93
  140. package/dist/server/templates/teams-dev.json +174 -160
  141. package/dist/server/tsconfig.tsbuildinfo +1 -1
  142. package/dist/server/ui/assets/{ReviewDetailPage-Dd40lXMW.js → ReviewDetailPage-C5BI5FSx.js} +1 -1
  143. package/dist/server/ui/assets/{ReviewsPage-CBSbsimJ.js → ReviewsPage-C4V7MJQc.js} +1 -1
  144. package/dist/server/ui/assets/index-BSx7QxmM.css +32 -0
  145. package/dist/server/ui/assets/index-nwIXVn_B.js +1142 -0
  146. package/dist/server/ui/assets/{useReviewSubscription-CpDyx3vJ.js → useReviewSubscription-Bd1R13V5.js} +1 -1
  147. package/dist/server/ui/index.html +2 -2
  148. package/dist/templates/3-agents-dev.json +97 -93
  149. package/dist/templates/teams-dev.json +174 -160
  150. package/package.json +16 -5
  151. package/dist/server/ui/assets/index-COvyD_66.css +0 -32
  152. package/dist/server/ui/assets/index-DtIjl-Lt.js +0 -1113
@@ -29,16 +29,31 @@ const ws_envelope_dto_1 = require("../dtos/ws-envelope.dto");
29
29
  const sessions_service_1 = require("../../sessions/services/sessions.service");
30
30
  const normalize_line_endings_1 = require("../utils/normalize-line-endings");
31
31
  const realtime_broadcast_service_1 = require("../../realtime/services/realtime-broadcast.service");
32
+ const metrics_service_1 = require("../../metrics/services/metrics.service");
33
+ const terminal_send_scheduler_service_1 = require("../services/terminal-send-scheduler.service");
32
34
  const logger = (0, logger_1.createLogger)('TerminalGateway');
33
35
  const THEME_HEX_RE = /^#[0-9a-fA-F]{6}$/;
34
36
  const HEARTBEAT_INTERVAL = 30000;
35
37
  const HEARTBEAT_TIMEOUT = 45000;
36
38
  const VIEWPORT_RESTORE_COALESCE_MS = 500;
39
+ const STOPPED_REPLAY_RETENTION_MS = 60000;
37
40
  const INPUT_RATE_WINDOW_MS = 5000;
38
41
  const INPUT_RATE_MSG_THRESHOLD = 500;
39
42
  const INPUT_RATE_BYTES_THRESHOLD = 512000;
43
+ function classifySubscribeCursor(lastSequence, sequenceEpoch) {
44
+ const sequence = typeof lastSequence === 'number' && Number.isFinite(lastSequence) ? lastSequence : undefined;
45
+ const epoch = typeof sequenceEpoch === 'string' && sequenceEpoch.length > 0 ? sequenceEpoch : undefined;
46
+ if (sequence !== undefined && epoch !== undefined) {
47
+ return { kind: 'cursor', cursor: { sequenceEpoch: epoch, sequence } };
48
+ }
49
+ if (sequence !== undefined)
50
+ return { kind: 'legacy-gap', sequence };
51
+ if (epoch !== undefined)
52
+ return { kind: 'invalid' };
53
+ return { kind: 'first-attach' };
54
+ }
40
55
  let TerminalGateway = class TerminalGateway {
41
- constructor(streamService, settingsService, ptyService, seedService, terminalIO, registry, sessionsService, realtimeBroadcast) {
56
+ constructor(streamService, settingsService, ptyService, seedService, terminalIO, registry, sessionsService, realtimeBroadcast, sendScheduler, metricsService) {
42
57
  this.streamService = streamService;
43
58
  this.settingsService = settingsService;
44
59
  this.ptyService = ptyService;
@@ -47,24 +62,39 @@ let TerminalGateway = class TerminalGateway {
47
62
  this.registry = registry;
48
63
  this.sessionsService = sessionsService;
49
64
  this.realtimeBroadcast = realtimeBroadcast;
65
+ this.sendScheduler = sendScheduler;
66
+ this.metricsService = metricsService;
50
67
  this.clientSessions = new Map();
51
68
  this.frameListeners = new Map();
52
69
  this.inputRateTracker = new Map();
53
70
  this.themeCache = new Map();
54
71
  this.viewportRestoreAt = new Map();
72
+ this.recoveries = new Map();
73
+ this.targetedSeedAttempts = new Map();
74
+ this.streamService.setClearExpiryHandler((sessionId) => this.retireSessionRecoveries(sessionId));
55
75
  }
56
76
  afterInit() {
57
77
  this.realtimeBroadcast.setServer(this.server);
78
+ this.metricsService.registerStatsProvider('sockets', () => this.getSocketStats());
58
79
  logger.info('WebSocket gateway initialized');
59
80
  this.startHeartbeat();
60
81
  }
82
+ getSocketStats() {
83
+ return {
84
+ connectedClients: this.clientSessions.size,
85
+ ...this.sendScheduler.getStats(),
86
+ ...(this.seedService.getCaptureStats?.() ?? {}),
87
+ };
88
+ }
61
89
  handleConnection(client) {
62
90
  logger.info({ clientId: client.id, transport: client.conn?.transport?.name }, 'Client connected');
63
91
  this.clientSessions.set(client.id, {
92
+ socket: client,
64
93
  sessionId: '',
65
94
  lastHeartbeat: new Date(),
66
95
  subscriptions: new Set(),
67
96
  });
97
+ this.sendScheduler.registerSocket(client);
68
98
  this.sendHeartbeat(client);
69
99
  }
70
100
  handleDisconnect(client) {
@@ -87,41 +117,39 @@ let TerminalGateway = class TerminalGateway {
87
117
  this.registry.get(sessionId)?.unsubscribe(clientId);
88
118
  }
89
119
  }
120
+ this.pruneInputRateEntries((key) => key.startsWith(`${clientId}:`));
121
+ for (const [key, recovery] of this.recoveries) {
122
+ if (recovery.socketId === clientId)
123
+ this.recoveries.delete(key);
124
+ }
125
+ for (const key of this.targetedSeedAttempts.keys()) {
126
+ if (key.startsWith(`${clientId}:`))
127
+ this.targetedSeedAttempts.delete(key);
128
+ }
129
+ this.sendScheduler.removeSocket(clientId);
90
130
  this.clientSessions.delete(clientId);
91
131
  }
92
132
  async handleSubscribe(client, payload) {
93
- const { sessionId, lastSequence, rows, cols } = payload;
94
- logger.info({ clientId: client.id, sessionId, lastSequence, rows, cols }, 'Client subscribing');
133
+ const { sessionId, lastSequence, sequenceEpoch, rows, cols } = payload;
134
+ logger.info({ clientId: client.id, sessionId, lastSequence, sequenceEpoch, rows, cols }, 'Client subscribing');
95
135
  const clientSession = this.clientSessions.get(client.id);
96
136
  if (!clientSession)
97
137
  return;
138
+ this.cancelReplayCleanup(sessionId);
98
139
  clientSession.sessionId = sessionId;
99
140
  clientSession.subscriptions.add(`session/${sessionId}`);
100
141
  client.join(`session:${sessionId}`);
101
142
  this.streamService.initializeBuffer(sessionId);
143
+ const cursor = classifySubscribeCursor(lastSequence, sequenceEpoch);
102
144
  const session = this.registry.get(sessionId);
103
145
  if (!session) {
104
146
  logger.warn({ sessionId }, 'No registry entry — using fallback seed path');
105
- client.emit('message', (0, ws_envelope_dto_1.createEnvelope)(`terminal/${sessionId}`, 'subscribed', {
106
- sessionId,
107
- currentSequence: 0,
108
- }));
147
+ const decision = this.resolveSubscribeReplay(sessionId, cursor);
148
+ this.emitSubscribed(client, sessionId, decision.cursorSnapshot.currentSequence, decision.replayStatus, decision.cursorSnapshot.sequenceEpoch);
109
149
  clientSession.subscriptions.add(`terminal/${sessionId}`);
110
150
  client.join(`terminal:${sessionId}`);
111
- if (typeof lastSequence !== 'number') {
112
- const { maxBytes: seedMaxBytes } = this.seedService.resolveSeedingConfig();
113
- this.seedService
114
- .emitSeedToClient({
115
- client,
116
- sessionId,
117
- maxBytes: seedMaxBytes,
118
- cols,
119
- rows,
120
- })
121
- .catch((error) => {
122
- logger.error({ sessionId, clientId: client.id, error }, 'Fallback seed failed');
123
- });
124
- }
151
+ this.streamService.resumeRetention(sessionId);
152
+ await this.applySubscribeReplay(client, sessionId, cursor, decision, cols, rows, false);
125
153
  return;
126
154
  }
127
155
  const tmuxAlive = await this.terminalIO.sessionExists({ name: session.tmuxSessionName });
@@ -130,13 +158,13 @@ let TerminalGateway = class TerminalGateway {
130
158
  return;
131
159
  }
132
160
  await this.ensurePtyStreaming(sessionId, session.tmuxSessionName, { cols, rows });
133
- const isFirstAttach = typeof lastSequence !== 'number';
161
+ const isSeedAttach = cursor.kind === 'first-attach' || cursor.kind === 'invalid';
134
162
  const validDims = typeof rows === 'number' && rows > 0 && typeof cols === 'number' && cols > 0;
135
163
  let wonInitialAuthority = false;
136
164
  if (validDims && session.claimInitialAuthority(client.id)) {
137
165
  wonInitialAuthority = true;
138
166
  this.ptyService.resize(sessionId, cols, rows);
139
- if (isFirstAttach) {
167
+ if (isSeedAttach) {
140
168
  this.seedService.invalidateCache(sessionId);
141
169
  await new Promise((r) => setTimeout(r, 50));
142
170
  }
@@ -145,26 +173,309 @@ let TerminalGateway = class TerminalGateway {
145
173
  logger.info({ sessionId, clientId: client.id }, 'Client gone mid-subscribe — skipping');
146
174
  return;
147
175
  }
148
- client.emit('message', (0, ws_envelope_dto_1.createEnvelope)(`terminal/${sessionId}`, 'subscribed', {
149
- sessionId,
150
- currentSequence: this.streamService.getCurrentSequence(sessionId),
151
- }));
176
+ const decision = this.resolveSubscribeReplay(sessionId, cursor);
177
+ this.emitSubscribed(client, sessionId, decision.cursorSnapshot.currentSequence, decision.replayStatus, decision.cursorSnapshot.sequenceEpoch);
152
178
  clientSession.subscriptions.add(`terminal/${sessionId}`);
153
179
  client.join(`terminal:${sessionId}`);
154
180
  this.wireFrameListener(sessionId);
155
- session.setUsesAlternateScreen(this.sessionsService.usesAlternateScreenFor(sessionId));
156
181
  if (wonInitialAuthority) {
157
182
  session.notifyInitialAuthority(client.id);
158
183
  }
159
184
  session.subscribe(client.id);
160
- if (!isFirstAttach) {
161
- const bufferedFrames = this.streamService.getFramesSince(sessionId, lastSequence);
162
- for (const frame of bufferedFrames) {
163
- client.emit('message', frame);
185
+ this.streamService.resumeRetention(sessionId);
186
+ await this.applySubscribeReplay(client, sessionId, cursor, decision, cols, rows, true);
187
+ }
188
+ resolveSubscribeReplay(sessionId, cursor) {
189
+ switch (cursor.kind) {
190
+ case 'cursor': {
191
+ const replay = this.streamService.getReconnectReplay(sessionId, cursor.cursor);
192
+ return {
193
+ replayStatus: replay.status,
194
+ cursorSnapshot: {
195
+ sequenceEpoch: replay.sequenceEpoch,
196
+ currentSequence: replay.currentSequence,
197
+ },
198
+ replay,
199
+ };
164
200
  }
165
- this.maybeRestoreViewportModes(sessionId);
201
+ case 'legacy-gap':
202
+ return { replayStatus: 'gap', cursorSnapshot: this.streamService.sampleCursor(sessionId) };
203
+ case 'first-attach':
204
+ case 'invalid':
205
+ return { replayStatus: 'seed', cursorSnapshot: this.streamService.sampleCursor(sessionId) };
166
206
  }
167
207
  }
208
+ async applySubscribeReplay(client, sessionId, cursor, decision, cols, rows, restoreViewportOnCovered) {
209
+ switch (cursor.kind) {
210
+ case 'first-attach':
211
+ case 'invalid':
212
+ if (cursor.kind === 'invalid') {
213
+ logger.warn({ sessionId, clientId: client.id }, 'Invalid subscribe cursor — reseeding');
214
+ }
215
+ await this.emitTargetedSeed(client, sessionId, cols, rows);
216
+ return;
217
+ case 'legacy-gap':
218
+ await this.emitReplayResync(client, sessionId, cursor.sequence, { status: 'gap', currentSequence: decision.cursorSnapshot.currentSequence }, cols, rows);
219
+ return;
220
+ case 'cursor': {
221
+ const replay = decision.replay;
222
+ if (!replay)
223
+ return;
224
+ if (replay.status === 'covered') {
225
+ for (const frame of replay.frames)
226
+ this.enqueueLiveOrRecover(client, sessionId, frame);
227
+ if (restoreViewportOnCovered)
228
+ this.maybeRestoreViewportModes(sessionId);
229
+ }
230
+ else {
231
+ await this.emitReplayResync(client, sessionId, cursor.cursor.sequence, replay, cols, rows);
232
+ }
233
+ return;
234
+ }
235
+ }
236
+ }
237
+ emitSubscribed(client, sessionId, currentSequence, replayStatus, sequenceEpoch) {
238
+ const payload = ws_envelope_dto_1.SubscribedPayloadSchema.parse({
239
+ sessionId,
240
+ currentSequence,
241
+ sequenceEpoch,
242
+ replayStatus,
243
+ historyRefreshable: !this.sessionsService.usesAlternateScreenFor(sessionId),
244
+ });
245
+ client.emit('message', (0, ws_envelope_dto_1.createEnvelope)(`terminal/${sessionId}`, 'subscribed', payload));
246
+ }
247
+ async emitReplayResync(client, sessionId, requestedSequence, replay, cols, rows) {
248
+ client.emit('message', (0, ws_envelope_dto_1.createEnvelope)(`terminal/${sessionId}`, 'resync_required', {
249
+ sessionId,
250
+ requestedSequence,
251
+ currentSequence: replay.currentSequence,
252
+ earliestAvailableSequence: replay.earliestAvailableSequence,
253
+ }));
254
+ await this.startRecovery(client, sessionId, cols, rows);
255
+ }
256
+ async emitTargetedSeed(client, sessionId, cols, rows) {
257
+ const { maxBytes } = this.seedService.resolveSeedingConfig();
258
+ const recoveryKey = this.recoveryKey(client.id, sessionId);
259
+ const seedAttempt = Symbol();
260
+ this.targetedSeedAttempts.set(recoveryKey, seedAttempt);
261
+ let failedAdmission;
262
+ try {
263
+ await this.seedService.emitSeedToClient({
264
+ deliver: (envelope) => {
265
+ const clientSession = this.clientSessions.get(client.id);
266
+ if (this.targetedSeedAttempts.get(recoveryKey) !== seedAttempt ||
267
+ !clientSession?.subscriptions.has(`terminal/${sessionId}`) ||
268
+ this.recoveries.has(recoveryKey)) {
269
+ return terminal_seed_service_1.TerminalSeedDelivery.Abort;
270
+ }
271
+ const admission = this.sendScheduler.enqueueRecovery(client, envelope);
272
+ if (admission === terminal_send_scheduler_service_1.TerminalSendAdmission.Accepted) {
273
+ return terminal_seed_service_1.TerminalSeedDelivery.Continue;
274
+ }
275
+ failedAdmission ??= admission;
276
+ return terminal_seed_service_1.TerminalSeedDelivery.Abort;
277
+ },
278
+ sessionId,
279
+ maxBytes,
280
+ cols,
281
+ rows,
282
+ allowEmpty: true,
283
+ getCurrentSequence: () => this.streamService.getCurrentSequence(sessionId),
284
+ });
285
+ if (failedAdmission &&
286
+ this.targetedSeedAttempts.get(recoveryKey) === seedAttempt &&
287
+ this.clientSessions.get(client.id)?.subscriptions.has(`terminal/${sessionId}`)) {
288
+ logger.warn({ sessionId, clientId: client.id, admission: failedAdmission }, 'Initial targeted seed admission failed; starting bounded recovery');
289
+ await this.startRecovery(client, sessionId, cols, rows);
290
+ }
291
+ }
292
+ catch (error) {
293
+ logger.error({ sessionId, clientId: client.id, error }, 'Targeted terminal seed failed');
294
+ }
295
+ finally {
296
+ if (this.targetedSeedAttempts.get(recoveryKey) === seedAttempt) {
297
+ this.targetedSeedAttempts.delete(recoveryKey);
298
+ }
299
+ }
300
+ }
301
+ async handleResyncRequest(client, payload) {
302
+ const parsed = ws_envelope_dto_1.TerminalResyncRequestPayloadSchema.safeParse(payload);
303
+ if (!parsed.success)
304
+ return;
305
+ const { sessionId } = parsed.data;
306
+ const clientSession = this.clientSessions.get(client.id);
307
+ if (!clientSession?.subscriptions.has(`terminal/${sessionId}`))
308
+ return;
309
+ await this.startRecovery(client, sessionId);
310
+ }
311
+ handleResyncAbort(client, payload) {
312
+ const parsed = ws_envelope_dto_1.TerminalResyncAbortPayloadSchema.safeParse(payload);
313
+ if (!parsed.success)
314
+ return false;
315
+ const { sessionId, sequenceEpoch, recoveryEpoch } = parsed.data;
316
+ const key = this.recoveryKey(client.id, sessionId);
317
+ const state = this.recoveries.get(key);
318
+ if (!state ||
319
+ !this.isCurrentRecovery(state) ||
320
+ state.phase !== 'seeding' ||
321
+ state.sequenceEpoch !== sequenceEpoch ||
322
+ state.recoveryEpoch !== recoveryEpoch) {
323
+ return false;
324
+ }
325
+ this.recoveries.delete(key);
326
+ this.sendScheduler.removeLane(client.id, sessionId);
327
+ return true;
328
+ }
329
+ handleResyncComplete(client, payload) {
330
+ const parsed = ws_envelope_dto_1.TerminalResyncCompletePayloadSchema.safeParse(payload);
331
+ if (!parsed.success)
332
+ return;
333
+ const completion = parsed.data;
334
+ const state = this.recoveries.get(this.recoveryKey(client.id, completion.sessionId));
335
+ if (!state ||
336
+ !this.isCurrentRecovery(state) ||
337
+ state.phase !== 'seeding' ||
338
+ state.sequenceEpoch !== completion.sequenceEpoch ||
339
+ state.recoveryEpoch !== completion.recoveryEpoch ||
340
+ state.capturedSequence !== completion.capturedSequence) {
341
+ return;
342
+ }
343
+ state.phase = 'tail-catch-up';
344
+ this.deliverCoveredTail(client, state, completion.capturedSequence);
345
+ }
346
+ async startRecovery(client, sessionId, cols, rows) {
347
+ const key = this.recoveryKey(client.id, sessionId);
348
+ this.targetedSeedAttempts.delete(key);
349
+ if (this.recoveries.has(key))
350
+ return;
351
+ await this.launchRecovery(client, sessionId, cols, rows);
352
+ }
353
+ enqueueLiveOrRecover(client, sessionId, envelope) {
354
+ const admission = this.sendScheduler.enqueueLive(client, envelope);
355
+ if (admission === terminal_send_scheduler_service_1.TerminalSendAdmission.NewlyDesynchronized) {
356
+ void this.startRecovery(client, sessionId);
357
+ }
358
+ }
359
+ async launchRecovery(client, sessionId, cols, rows, replacementAttempt = 0) {
360
+ const sequenceEpoch = this.streamService.sampleCursor(sessionId).sequenceEpoch;
361
+ const recoveryEpoch = this.streamService.nextRecoveryCounter(sessionId);
362
+ const state = {
363
+ socketId: client.id,
364
+ sessionId,
365
+ sequenceEpoch,
366
+ recoveryEpoch,
367
+ phase: 'seeding',
368
+ replacementAttempt,
369
+ };
370
+ this.recoveries.set(this.recoveryKey(client.id, sessionId), state);
371
+ if (!this.sendScheduler.beginRecovery(client, sessionId, recoveryEpoch)) {
372
+ this.disconnectUnrecoverableRecovery(client, state, 'recovery_begin_rejected');
373
+ return;
374
+ }
375
+ this.seedService.invalidateCache(sessionId);
376
+ const { maxBytes } = this.seedService.resolveSeedingConfig();
377
+ let admissionFailed = false;
378
+ try {
379
+ const watermark = await this.seedService.emitSeedToClient({
380
+ deliver: (envelope) => {
381
+ if (!this.isCurrentRecovery(state))
382
+ return terminal_seed_service_1.TerminalSeedDelivery.Abort;
383
+ const admission = this.sendScheduler.enqueueRecovery(client, envelope);
384
+ if (admission !== terminal_send_scheduler_service_1.TerminalSendAdmission.Accepted) {
385
+ admissionFailed = true;
386
+ return terminal_seed_service_1.TerminalSeedDelivery.Abort;
387
+ }
388
+ return terminal_seed_service_1.TerminalSeedDelivery.Continue;
389
+ },
390
+ sessionId,
391
+ maxBytes,
392
+ cols,
393
+ rows,
394
+ allowEmpty: true,
395
+ recovery: {
396
+ sequenceEpoch,
397
+ recoveryEpoch,
398
+ getCurrentSequence: () => this.streamService.getCurrentSequence(sessionId),
399
+ onCapturedSequence: (capturedSequence) => {
400
+ if (this.isCurrentRecovery(state))
401
+ state.capturedSequence = capturedSequence;
402
+ },
403
+ },
404
+ });
405
+ if (!this.isCurrentRecovery(state))
406
+ return;
407
+ if (admissionFailed) {
408
+ this.disconnectUnrecoverableRecovery(client, state, 'recovery_seed_admission_failed');
409
+ return;
410
+ }
411
+ if (!watermark) {
412
+ this.disconnectUnrecoverableRecovery(client, state, 'recovery_seed_capture_failed');
413
+ return;
414
+ }
415
+ state.capturedSequence = watermark.capturedSequence;
416
+ }
417
+ catch (error) {
418
+ logger.error({ sessionId, clientId: client.id, recoveryEpoch, error }, 'Targeted recovery seed failed');
419
+ this.disconnectUnrecoverableRecovery(client, state, 'recovery_seed_failed');
420
+ }
421
+ }
422
+ deliverCoveredTail(client, state, afterSequence) {
423
+ if (!this.isCurrentRecovery(state))
424
+ return;
425
+ const replay = this.streamService.getFramesSince(state.sessionId, afterSequence);
426
+ if (replay.status === 'gap') {
427
+ this.scheduleReplacementSeed(client, state);
428
+ return;
429
+ }
430
+ if (replay.frames.length === 0) {
431
+ this.finishRecovery(state);
432
+ return;
433
+ }
434
+ for (let index = 0; index < replay.frames.length; index += 1) {
435
+ const isLast = index === replay.frames.length - 1;
436
+ const admission = this.sendScheduler.enqueueRecovery(client, replay.frames[index], isLast ? () => this.deliverCoveredTail(client, state, replay.currentSequence) : undefined);
437
+ if (admission !== terminal_send_scheduler_service_1.TerminalSendAdmission.Accepted) {
438
+ this.disconnectUnrecoverableRecovery(client, state, 'recovery_tail_admission_failed');
439
+ return;
440
+ }
441
+ }
442
+ }
443
+ scheduleReplacementSeed(client, state) {
444
+ if (!this.isCurrentRecovery(state) || state.phase === 'replacing')
445
+ return;
446
+ if (state.replacementAttempt >= 1) {
447
+ this.disconnectUnrecoverableRecovery(client, state, 'recovery_tail_gap_repeated');
448
+ return;
449
+ }
450
+ state.phase = 'replacing';
451
+ void this.launchRecovery(client, state.sessionId, undefined, undefined, 1);
452
+ }
453
+ disconnectUnrecoverableRecovery(client, state, reason) {
454
+ if (!this.isCurrentRecovery(state))
455
+ return;
456
+ logger.warn({
457
+ clientId: client.id,
458
+ sessionId: state.sessionId,
459
+ recoveryEpoch: state.recoveryEpoch,
460
+ reason,
461
+ }, 'Terminal recovery failed closed; disconnecting affected socket');
462
+ this.recoveries.delete(this.recoveryKey(state.socketId, state.sessionId));
463
+ client.conn.close();
464
+ this.releaseClientSession(client.id);
465
+ }
466
+ finishRecovery(state) {
467
+ if (!this.isCurrentRecovery(state))
468
+ return;
469
+ this.sendScheduler.markSynchronized(state.socketId, state.sessionId, state.recoveryEpoch);
470
+ this.recoveries.delete(this.recoveryKey(state.socketId, state.sessionId));
471
+ }
472
+ isCurrentRecovery(state) {
473
+ return (this.recoveries.get(this.recoveryKey(state.socketId, state.sessionId)) === state &&
474
+ this.streamService.getSequenceEpoch(state.sessionId) === state.sequenceEpoch);
475
+ }
476
+ recoveryKey(socketId, sessionId) {
477
+ return `${socketId}:${sessionId}`;
478
+ }
168
479
  handleEventsSubscribe(client) {
169
480
  const cs = this.clientSessions.get(client.id);
170
481
  if (!cs)
@@ -201,6 +512,10 @@ let TerminalGateway = class TerminalGateway {
201
512
  const session = this.registry.get(sessionId);
202
513
  if (session)
203
514
  session.unsubscribe(client.id);
515
+ const recoveryKey = this.recoveryKey(client.id, sessionId);
516
+ this.recoveries.delete(recoveryKey);
517
+ this.targetedSeedAttempts.delete(recoveryKey);
518
+ this.sendScheduler.removeLane(client.id, sessionId);
204
519
  }
205
520
  async handleTheme(client, payload) {
206
521
  const cs = this.clientSessions.get(client.id);
@@ -307,6 +622,11 @@ let TerminalGateway = class TerminalGateway {
307
622
  }
308
623
  async handleRequestFullHistory(client, payload) {
309
624
  const { sessionId } = payload;
625
+ const correlationParse = ws_envelope_dto_1.FullHistoryRequestPayloadSchema.shape.correlationId.safeParse(payload.correlationId);
626
+ if (!correlationParse.success) {
627
+ throw new websockets_1.WsException('correlationId must be a string');
628
+ }
629
+ const correlationId = correlationParse.data;
310
630
  let maxLines = 10000;
311
631
  if (payload.maxLines !== undefined && payload.maxLines !== null) {
312
632
  const parsed = Math.floor(Number(payload.maxLines));
@@ -323,10 +643,11 @@ let TerminalGateway = class TerminalGateway {
323
643
  if (!session)
324
644
  return;
325
645
  const target = { name: session.tmuxSessionName };
646
+ const beforeEpoch = this.streamService.sampleCursor(sessionId).sequenceEpoch;
326
647
  const captureResult = await this.terminalIO.captureHistory(target, maxLines, true);
327
648
  const capturedSequence = this.streamService.getCurrentSequence(sessionId);
328
649
  let history = captureResult.ok ? captureResult.output : '';
329
- history = (0, normalize_line_endings_1.stripFinalLineEnding)(history);
650
+ history = (0, normalize_line_endings_1.normalizeLineEndings)((0, normalize_line_endings_1.stripFinalLineEnding)(history));
330
651
  const { maxBytes } = this.seedService.resolveSeedingConfig();
331
652
  let hasHistory = false;
332
653
  if (Buffer.byteLength(history, 'utf-8') > maxBytes) {
@@ -334,14 +655,20 @@ let TerminalGateway = class TerminalGateway {
334
655
  history = truncated;
335
656
  hasHistory = wasTruncated;
336
657
  }
337
- history = (0, normalize_line_endings_1.normalizeLineEndings)(history);
338
658
  const cursorPos = await this.terminalIO.getCursorPosition(target);
659
+ const afterEpoch = this.streamService.sampleCursor(sessionId).sequenceEpoch;
660
+ if (afterEpoch !== beforeEpoch) {
661
+ logger.warn({ sessionId, beforeEpoch, afterEpoch }, 'Sequence domain changed during full_history capture; dropping response');
662
+ return;
663
+ }
339
664
  client.emit('message', (0, ws_envelope_dto_1.createEnvelope)(`terminal/${sessionId}`, 'full_history', {
340
665
  history,
341
666
  cursorX: cursorPos?.x,
342
667
  cursorY: cursorPos?.y,
343
668
  hasHistory,
344
669
  capturedSequence,
670
+ sequenceEpoch: beforeEpoch,
671
+ ...(correlationId !== undefined && { correlationId }),
345
672
  }));
346
673
  }
347
674
  handleRestoreViewportModes(client, payload) {
@@ -359,15 +686,40 @@ let TerminalGateway = class TerminalGateway {
359
686
  cs.lastHeartbeat = new Date();
360
687
  }
361
688
  broadcastTerminalData(sessionId, data) {
362
- this.registry.get(sessionId)?.pushFrame(data);
363
- const envelope = this.streamService.addFrame(sessionId, data);
364
- this.server.to(`terminal:${sessionId}`).emit('message', envelope);
689
+ const session = this.registry.get(sessionId);
690
+ if (!this.hasWebSubscribers(sessionId)) {
691
+ session?.pushFrame(data);
692
+ this.streamService.markDiscontinuous(sessionId);
693
+ return;
694
+ }
695
+ this.streamService.resumeRetention(sessionId);
696
+ const envelopes = this.streamService.addFrame(sessionId, data);
697
+ for (const envelope of envelopes) {
698
+ const chunk = envelope.payload.data;
699
+ session?.pushFrame(chunk);
700
+ for (const clientSession of this.clientSessions.values()) {
701
+ if (clientSession.subscriptions.has(`terminal/${sessionId}`)) {
702
+ this.enqueueLiveOrRecover(clientSession.socket, sessionId, envelope);
703
+ }
704
+ }
705
+ }
706
+ }
707
+ hasWebSubscribers(sessionId) {
708
+ const session = this.registry.get(sessionId);
709
+ if (session)
710
+ return session.getSubscriberCount() > 0;
711
+ for (const clientSession of this.clientSessions.values()) {
712
+ if (clientSession.subscriptions.has(`terminal/${sessionId}`))
713
+ return true;
714
+ }
715
+ return false;
365
716
  }
366
717
  handleSessionCrashed(payload) {
367
718
  this.sessionsService.markSessionFailed(payload.sessionId, 'tmux session lost (health check)');
368
- this.ptyService.stopStreaming(payload.sessionId);
369
- this.unwireFrameListener(payload.sessionId);
370
- this.registry.dispose(payload.sessionId);
719
+ this.cleanupSessionLifecycle(payload.sessionId, {
720
+ replayRetentionMs: 0,
721
+ disposeTerminalState: true,
722
+ });
371
723
  const ep = {
372
724
  sessionId: payload.sessionId,
373
725
  status: 'crashed',
@@ -376,9 +728,6 @@ let TerminalGateway = class TerminalGateway {
376
728
  this.server
377
729
  .to(`session:${payload.sessionId}`)
378
730
  .emit('message', (0, ws_envelope_dto_1.createEnvelope)(`session/${payload.sessionId}`, 'state_change', ep));
379
- setTimeout(() => this.streamService.clearBuffer(payload.sessionId), 60000);
380
- this.themeCache.delete(payload.sessionId);
381
- this.viewportRestoreAt.delete(payload.sessionId);
382
731
  }
383
732
  handleSessionStarted(payload) {
384
733
  const ep = {
@@ -389,6 +738,7 @@ let TerminalGateway = class TerminalGateway {
389
738
  this.server.emit('message', (0, ws_envelope_dto_1.createEnvelope)('sessions', 'started', ep));
390
739
  }
391
740
  handleSessionRestored(payload) {
741
+ this.cancelReplayCleanup(payload.sessionId);
392
742
  if (!this.registry.get(payload.sessionId) && payload.tmuxSessionName) {
393
743
  try {
394
744
  this.registry.create(payload.sessionId, payload.tmuxSessionName, {
@@ -407,17 +757,16 @@ let TerminalGateway = class TerminalGateway {
407
757
  this.server.emit('message', (0, ws_envelope_dto_1.createEnvelope)('sessions', 'started', ep));
408
758
  }
409
759
  handleSessionStopped(payload) {
410
- this.unwireFrameListener(payload.sessionId);
760
+ this.cleanupSessionLifecycle(payload.sessionId, {
761
+ replayRetentionMs: STOPPED_REPLAY_RETENTION_MS,
762
+ disposeTerminalState: false,
763
+ });
411
764
  const ep = {
412
765
  sessionId: payload.sessionId,
413
766
  status: 'ended',
414
767
  message: 'Session terminated',
415
768
  };
416
769
  this.server.emit('message', (0, ws_envelope_dto_1.createEnvelope)('sessions', 'stopped', ep));
417
- setTimeout(() => this.streamService.clearBuffer(payload.sessionId), 60000);
418
- this.seedService.invalidateCache(payload.sessionId);
419
- this.themeCache.delete(payload.sessionId);
420
- this.viewportRestoreAt.delete(payload.sessionId);
421
770
  }
422
771
  startHeartbeat() {
423
772
  this.heartbeatInterval = setInterval(() => {
@@ -452,12 +801,7 @@ let TerminalGateway = class TerminalGateway {
452
801
  logger.info({ sessionId }, 'Rewiring stale frame listener for restored session');
453
802
  this.frameListeners.delete(sessionId);
454
803
  }
455
- const FORWARDED_FRAME_TYPES = new Set([
456
- 'focus_changed',
457
- 'seed_ansi',
458
- 'resize_jiggle',
459
- 'full_history',
460
- ]);
804
+ const FORWARDED_FRAME_TYPES = new Set(['focus_changed', 'resize_jiggle', 'full_history']);
461
805
  const listener = (frame) => {
462
806
  if (FORWARDED_FRAME_TYPES.has(frame.type)) {
463
807
  this.server
@@ -478,11 +822,10 @@ let TerminalGateway = class TerminalGateway {
478
822
  async handleDeadTmuxSession(sessionId, client) {
479
823
  logger.warn({ sessionId }, 'Dead tmux detected — marking session failed');
480
824
  this.sessionsService.markSessionFailed(sessionId, 'tmux session no longer exists');
481
- this.ptyService.stopStreaming(sessionId);
482
- this.unwireFrameListener(sessionId);
483
- this.registry.dispose(sessionId);
484
- this.themeCache.delete(sessionId);
485
- this.viewportRestoreAt.delete(sessionId);
825
+ this.cleanupSessionLifecycle(sessionId, {
826
+ replayRetentionMs: 0,
827
+ disposeTerminalState: true,
828
+ });
486
829
  const ep = {
487
830
  sessionId,
488
831
  status: 'crashed',
@@ -492,6 +835,37 @@ let TerminalGateway = class TerminalGateway {
492
835
  client.emit('message', envelope);
493
836
  this.server.to(`session:${sessionId}`).emit('message', envelope);
494
837
  }
838
+ cancelReplayCleanup(sessionId) {
839
+ this.streamService.cancelScheduledClear(sessionId);
840
+ }
841
+ cleanupSessionLifecycle(sessionId, policy) {
842
+ this.cancelReplayCleanup(sessionId);
843
+ this.unwireFrameListener(sessionId);
844
+ this.seedService.invalidateCache(sessionId);
845
+ this.themeCache.delete(sessionId);
846
+ this.viewportRestoreAt.delete(sessionId);
847
+ this.pruneInputRateEntries((key) => key.endsWith(`:${sessionId}`));
848
+ this.retireSessionRecoveries(sessionId);
849
+ if (policy.disposeTerminalState) {
850
+ this.ptyService.stopStreaming(sessionId);
851
+ this.registry.dispose(sessionId);
852
+ }
853
+ if (policy.replayRetentionMs === 0) {
854
+ this.streamService.clearBuffer(sessionId);
855
+ return;
856
+ }
857
+ this.streamService.scheduleClear(sessionId, policy.replayRetentionMs);
858
+ }
859
+ retireSessionRecoveries(sessionId) {
860
+ for (const [key, recovery] of this.recoveries) {
861
+ if (recovery.sessionId === sessionId)
862
+ this.recoveries.delete(key);
863
+ }
864
+ for (const clientId of this.clientSessions.keys()) {
865
+ this.targetedSeedAttempts.delete(this.recoveryKey(clientId, sessionId));
866
+ }
867
+ this.sendScheduler.removeSession(sessionId);
868
+ }
495
869
  async ensurePtyStreaming(sessionId, tmuxSessionName, options) {
496
870
  if (this.ptyService.isStreaming(sessionId))
497
871
  return;
@@ -544,6 +918,12 @@ let TerminalGateway = class TerminalGateway {
544
918
  entry.messages++;
545
919
  entry.bytes += dataBytes;
546
920
  }
921
+ pruneInputRateEntries(matches) {
922
+ for (const key of this.inputRateTracker.keys()) {
923
+ if (matches(key))
924
+ this.inputRateTracker.delete(key);
925
+ }
926
+ }
547
927
  onModuleDestroy() {
548
928
  if (this.heartbeatInterval)
549
929
  clearInterval(this.heartbeatInterval);
@@ -552,6 +932,10 @@ let TerminalGateway = class TerminalGateway {
552
932
  }
553
933
  this.themeCache.clear();
554
934
  this.viewportRestoreAt.clear();
935
+ this.inputRateTracker.clear();
936
+ this.recoveries.clear();
937
+ this.sendScheduler.dispose();
938
+ this.clientSessions.clear();
555
939
  }
556
940
  };
557
941
  exports.TerminalGateway = TerminalGateway;
@@ -567,6 +951,30 @@ __decorate([
567
951
  __metadata("design:paramtypes", [socket_io_1.Socket, Object]),
568
952
  __metadata("design:returntype", Promise)
569
953
  ], TerminalGateway.prototype, "handleSubscribe", null);
954
+ __decorate([
955
+ (0, websockets_1.SubscribeMessage)('terminal:resync_request'),
956
+ __param(0, (0, websockets_1.ConnectedSocket)()),
957
+ __param(1, (0, websockets_1.MessageBody)()),
958
+ __metadata("design:type", Function),
959
+ __metadata("design:paramtypes", [socket_io_1.Socket, Object]),
960
+ __metadata("design:returntype", Promise)
961
+ ], TerminalGateway.prototype, "handleResyncRequest", null);
962
+ __decorate([
963
+ (0, websockets_1.SubscribeMessage)('terminal:resync_abort'),
964
+ __param(0, (0, websockets_1.ConnectedSocket)()),
965
+ __param(1, (0, websockets_1.MessageBody)()),
966
+ __metadata("design:type", Function),
967
+ __metadata("design:paramtypes", [socket_io_1.Socket, Object]),
968
+ __metadata("design:returntype", Boolean)
969
+ ], TerminalGateway.prototype, "handleResyncAbort", null);
970
+ __decorate([
971
+ (0, websockets_1.SubscribeMessage)('terminal:resync_complete'),
972
+ __param(0, (0, websockets_1.ConnectedSocket)()),
973
+ __param(1, (0, websockets_1.MessageBody)()),
974
+ __metadata("design:type", Function),
975
+ __metadata("design:paramtypes", [socket_io_1.Socket, Object]),
976
+ __metadata("design:returntype", void 0)
977
+ ], TerminalGateway.prototype, "handleResyncComplete", null);
570
978
  __decorate([
571
979
  (0, websockets_1.SubscribeMessage)('events:subscribe'),
572
980
  __param(0, (0, websockets_1.ConnectedSocket)()),
@@ -690,6 +1098,8 @@ exports.TerminalGateway = TerminalGateway = __decorate([
690
1098
  terminal_io_service_1.TerminalIOService,
691
1099
  terminal_session_registry_1.TerminalSessionRegistry,
692
1100
  sessions_service_1.SessionsService,
693
- realtime_broadcast_service_1.RealtimeBroadcastService])
1101
+ realtime_broadcast_service_1.RealtimeBroadcastService,
1102
+ terminal_send_scheduler_service_1.TerminalSendSchedulerService,
1103
+ metrics_service_1.MetricsService])
694
1104
  ], TerminalGateway);
695
1105
  //# sourceMappingURL=terminal.gateway.js.map