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.
- package/dist/node_modules/@devchain/codebase-overview/tsconfig.tsbuildinfo +1 -1
- package/dist/node_modules/@devchain/shared/tsconfig.tsbuildinfo +1 -1
- package/dist/server/app.main.module.js +2 -0
- package/dist/server/app.main.module.js.map +1 -1
- package/dist/server/app.normal.module.js +2 -0
- package/dist/server/app.normal.module.js.map +1 -1
- package/dist/server/modules/events/catalog/broadcast-registry.js +19 -12
- package/dist/server/modules/events/catalog/broadcast-registry.js.map +1 -1
- package/dist/server/modules/events/catalog/index.d.ts +23 -3
- package/dist/server/modules/events/catalog/session.transcript.updated.d.ts +23 -3
- package/dist/server/modules/events/catalog/session.transcript.updated.js +41 -19
- package/dist/server/modules/events/catalog/session.transcript.updated.js.map +1 -1
- package/dist/server/modules/metrics/controllers/metrics.controller.d.ts +6 -0
- package/dist/server/modules/metrics/controllers/metrics.controller.js +38 -0
- package/dist/server/modules/metrics/controllers/metrics.controller.js.map +1 -0
- package/dist/server/modules/metrics/helpers/byte-accounting.helper.d.ts +17 -0
- package/dist/server/modules/metrics/helpers/byte-accounting.helper.js +88 -0
- package/dist/server/modules/metrics/helpers/byte-accounting.helper.js.map +1 -0
- package/dist/server/modules/metrics/metrics.module.d.ts +2 -0
- package/dist/server/modules/metrics/metrics.module.js +23 -0
- package/dist/server/modules/metrics/metrics.module.js.map +1 -0
- package/dist/server/modules/metrics/services/metrics.service.d.ts +20 -0
- package/dist/server/modules/metrics/services/metrics.service.js +241 -0
- package/dist/server/modules/metrics/services/metrics.service.js.map +1 -0
- package/dist/server/modules/metrics/types/metrics.types.d.ts +100 -0
- package/dist/server/modules/metrics/types/metrics.types.js +3 -0
- package/dist/server/modules/metrics/types/metrics.types.js.map +1 -0
- package/dist/server/modules/projects/helpers/profile-mapping.helpers.d.ts +5 -3
- package/dist/server/modules/projects/helpers/profile-mapping.helpers.js +25 -13
- package/dist/server/modules/projects/helpers/profile-mapping.helpers.js.map +1 -1
- package/dist/server/modules/projects/helpers/project-import.js +9 -7
- package/dist/server/modules/projects/helpers/project-import.js.map +1 -1
- package/dist/server/modules/projects/helpers/template-loader.js +11 -9
- package/dist/server/modules/projects/helpers/template-loader.js.map +1 -1
- package/dist/server/modules/projects/template-codec/codec-registry.js.map +1 -1
- package/dist/server/modules/projects/template-codec/codecs/agents.codec.d.ts +1 -1
- package/dist/server/modules/projects/template-codec/codecs/agents.codec.js +4 -4
- package/dist/server/modules/projects/template-codec/codecs/agents.codec.js.map +1 -1
- package/dist/server/modules/projects/template-codec/codecs/profiles.codec.d.ts +1 -1
- package/dist/server/modules/projects/template-codec/codecs/profiles.codec.js +27 -8
- package/dist/server/modules/projects/template-codec/codecs/profiles.codec.js.map +1 -1
- package/dist/server/modules/projects/template-codec/codecs/watchers.codec.js +1 -1
- package/dist/server/modules/projects/template-codec/codecs/watchers.codec.js.map +1 -1
- package/dist/server/modules/projects/template-codec/import-context.d.ts +6 -1
- package/dist/server/modules/projects/template-codec/import-context.js.map +1 -1
- package/dist/server/modules/projects/template-codec/template-section-codec.d.ts +2 -3
- package/dist/server/modules/session-reader/adapters/antigravity-session-reader.adapter.d.ts +6 -0
- package/dist/server/modules/session-reader/adapters/antigravity-session-reader.adapter.js +66 -0
- package/dist/server/modules/session-reader/adapters/antigravity-session-reader.adapter.js.map +1 -1
- package/dist/server/modules/session-reader/adapters/claude-session-reader.adapter.d.ts +5 -0
- package/dist/server/modules/session-reader/adapters/claude-session-reader.adapter.js +12 -0
- package/dist/server/modules/session-reader/adapters/claude-session-reader.adapter.js.map +1 -1
- package/dist/server/modules/session-reader/adapters/codex-session-reader.adapter.d.ts +5 -0
- package/dist/server/modules/session-reader/adapters/codex-session-reader.adapter.js +12 -0
- package/dist/server/modules/session-reader/adapters/codex-session-reader.adapter.js.map +1 -1
- package/dist/server/modules/session-reader/adapters/copilot-session-reader.adapter.d.ts +5 -0
- package/dist/server/modules/session-reader/adapters/copilot-session-reader.adapter.js +12 -0
- package/dist/server/modules/session-reader/adapters/copilot-session-reader.adapter.js.map +1 -1
- package/dist/server/modules/session-reader/adapters/opencode-session-reader.adapter.d.ts +5 -0
- package/dist/server/modules/session-reader/adapters/opencode-session-reader.adapter.js +15 -0
- package/dist/server/modules/session-reader/adapters/opencode-session-reader.adapter.js.map +1 -1
- package/dist/server/modules/session-reader/adapters/session-reader-adapter.interface.d.ts +10 -0
- package/dist/server/modules/session-reader/adapters/session-reader-adapter.interface.js +28 -1
- package/dist/server/modules/session-reader/adapters/session-reader-adapter.interface.js.map +1 -1
- package/dist/server/modules/session-reader/controllers/session-reader.controller.d.ts +10 -4
- package/dist/server/modules/session-reader/controllers/session-reader.controller.js +18 -19
- package/dist/server/modules/session-reader/controllers/session-reader.controller.js.map +1 -1
- package/dist/server/modules/session-reader/data/pricing.json +206 -0
- package/dist/server/modules/session-reader/parsers/claude-jsonl.parser.d.ts +1 -0
- package/dist/server/modules/session-reader/parsers/claude-jsonl.parser.js +7 -3
- package/dist/server/modules/session-reader/parsers/claude-jsonl.parser.js.map +1 -1
- package/dist/server/modules/session-reader/parsers/codex-jsonl.parser.d.ts +1 -0
- package/dist/server/modules/session-reader/parsers/codex-jsonl.parser.js +22 -11
- package/dist/server/modules/session-reader/parsers/codex-jsonl.parser.js.map +1 -1
- package/dist/server/modules/session-reader/parsers/copilot-jsonl.parser.d.ts +1 -0
- package/dist/server/modules/session-reader/parsers/copilot-jsonl.parser.js +21 -9
- package/dist/server/modules/session-reader/parsers/copilot-jsonl.parser.js.map +1 -1
- package/dist/server/modules/session-reader/readers/opencode-sqlite.reader.d.ts +5 -1
- package/dist/server/modules/session-reader/readers/opencode-sqlite.reader.js +102 -0
- package/dist/server/modules/session-reader/readers/opencode-sqlite.reader.js.map +1 -1
- package/dist/server/modules/session-reader/services/session-cache.service.d.ts +71 -4
- package/dist/server/modules/session-reader/services/session-cache.service.js +427 -68
- package/dist/server/modules/session-reader/services/session-cache.service.js.map +1 -1
- package/dist/server/modules/session-reader/services/session-reader.service.d.ts +21 -4
- package/dist/server/modules/session-reader/services/session-reader.service.js +126 -40
- package/dist/server/modules/session-reader/services/session-reader.service.js.map +1 -1
- package/dist/server/modules/session-reader/services/transcript-cursor.d.ts +1 -1
- package/dist/server/modules/session-reader/services/transcript-cursor.js +2 -2
- package/dist/server/modules/session-reader/services/transcript-cursor.js.map +1 -1
- package/dist/server/modules/session-reader/services/transcript-watcher.service.d.ts +2 -0
- package/dist/server/modules/session-reader/services/transcript-watcher.service.js +61 -11
- package/dist/server/modules/session-reader/services/transcript-watcher.service.js.map +1 -1
- package/dist/server/modules/session-reader/session-reader.module.js +3 -1
- package/dist/server/modules/session-reader/session-reader.module.js.map +1 -1
- package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.d.ts +4 -4
- package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.js +11 -5
- package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.js.map +1 -1
- package/dist/server/modules/sessions/services/session-runtime/session-restore-pipeline.service.d.ts +3 -1
- package/dist/server/modules/sessions/services/session-runtime/session-restore-pipeline.service.js +11 -2
- package/dist/server/modules/sessions/services/session-runtime/session-restore-pipeline.service.js.map +1 -1
- package/dist/server/modules/terminal/dtos/ws-envelope.dto.d.ts +140 -1
- package/dist/server/modules/terminal/dtos/ws-envelope.dto.js +59 -2
- package/dist/server/modules/terminal/dtos/ws-envelope.dto.js.map +1 -1
- package/dist/server/modules/terminal/gateways/terminal.gateway.d.ts +33 -1
- package/dist/server/modules/terminal/gateways/terminal.gateway.js +471 -61
- package/dist/server/modules/terminal/gateways/terminal.gateway.js.map +1 -1
- package/dist/server/modules/terminal/services/process-executor/fake-process-executor.d.ts +1 -0
- package/dist/server/modules/terminal/services/process-executor/fake-process-executor.js +1 -0
- package/dist/server/modules/terminal/services/process-executor/fake-process-executor.js.map +1 -1
- package/dist/server/modules/terminal/services/pty.service.d.ts +8 -3
- package/dist/server/modules/terminal/services/pty.service.js +11 -2
- package/dist/server/modules/terminal/services/pty.service.js.map +1 -1
- package/dist/server/modules/terminal/services/terminal-io/capture.js +2 -0
- package/dist/server/modules/terminal/services/terminal-io/capture.js.map +1 -1
- package/dist/server/modules/terminal/services/terminal-io/delivery.d.ts +1 -0
- package/dist/server/modules/terminal/services/terminal-io/delivery.js.map +1 -1
- package/dist/server/modules/terminal/services/terminal-io/terminal-io.service.js +28 -1
- package/dist/server/modules/terminal/services/terminal-io/terminal-io.service.js.map +1 -1
- package/dist/server/modules/terminal/services/terminal-key-input/terminal-key-input.facade.d.ts +6 -0
- package/dist/server/modules/terminal/services/terminal-key-input/terminal-key-input.facade.js +26 -1
- package/dist/server/modules/terminal/services/terminal-key-input/terminal-key-input.facade.js.map +1 -1
- package/dist/server/modules/terminal/services/terminal-seed.service.d.ts +27 -3
- package/dist/server/modules/terminal/services/terminal-seed.service.js +75 -19
- package/dist/server/modules/terminal/services/terminal-seed.service.js.map +1 -1
- package/dist/server/modules/terminal/services/terminal-send-scheduler.service.d.ts +73 -0
- package/dist/server/modules/terminal/services/terminal-send-scheduler.service.js +387 -0
- package/dist/server/modules/terminal/services/terminal-send-scheduler.service.js.map +1 -0
- package/dist/server/modules/terminal/services/terminal-session/terminal-session.d.ts +4 -10
- package/dist/server/modules/terminal/services/terminal-session/terminal-session.js +20 -48
- package/dist/server/modules/terminal/services/terminal-session/terminal-session.js.map +1 -1
- package/dist/server/modules/terminal/services/terminal-socket-drain.adapter.d.ts +10 -0
- package/dist/server/modules/terminal/services/terminal-socket-drain.adapter.js +75 -0
- package/dist/server/modules/terminal/services/terminal-socket-drain.adapter.js.map +1 -0
- package/dist/server/modules/terminal/services/terminal-stream.service.d.ts +53 -6
- package/dist/server/modules/terminal/services/terminal-stream.service.js +190 -35
- package/dist/server/modules/terminal/services/terminal-stream.service.js.map +1 -1
- package/dist/server/modules/terminal/terminal.module.js +7 -0
- package/dist/server/modules/terminal/terminal.module.js.map +1 -1
- package/dist/server/templates/3-agents-dev.json +97 -93
- package/dist/server/templates/teams-dev.json +174 -160
- package/dist/server/tsconfig.tsbuildinfo +1 -1
- package/dist/server/ui/assets/{ReviewDetailPage-Dd40lXMW.js → ReviewDetailPage-C5BI5FSx.js} +1 -1
- package/dist/server/ui/assets/{ReviewsPage-CBSbsimJ.js → ReviewsPage-C4V7MJQc.js} +1 -1
- package/dist/server/ui/assets/index-BSx7QxmM.css +32 -0
- package/dist/server/ui/assets/index-nwIXVn_B.js +1142 -0
- package/dist/server/ui/assets/{useReviewSubscription-CpDyx3vJ.js → useReviewSubscription-Bd1R13V5.js} +1 -1
- package/dist/server/ui/index.html +2 -2
- package/dist/templates/3-agents-dev.json +97 -93
- package/dist/templates/teams-dev.json +174 -160
- package/package.json +16 -5
- package/dist/server/ui/assets/index-COvyD_66.css +0 -32
- 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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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)
|
|
363
|
-
|
|
364
|
-
|
|
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.
|
|
369
|
-
|
|
370
|
-
|
|
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.
|
|
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.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|