@zeph-to/cli 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/listener.d.ts +31 -3
- package/dist/listener.d.ts.map +1 -1
- package/dist/listener.js +127 -18
- package/package.json +1 -1
package/dist/listener.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export declare const sessionsFingerprint: (sessions: AgentSession[]) => string;
|
|
|
56
56
|
* idle heartbeat is due. */
|
|
57
57
|
export declare const sessionsReportDue: (fingerprint: string, lastSentFingerprint: string | null, lastSentAtMs: number, nowMs: number) => boolean;
|
|
58
58
|
export declare const AUTH_FAILURE_CODES: ReadonlySet<number>;
|
|
59
|
-
export declare const checkRateLimit: (session: string, now?: number) => boolean;
|
|
59
|
+
export declare const checkRateLimit: (session: string, now?: number, cost?: number) => boolean;
|
|
60
60
|
/** Read the foreground command in the named tmux session's active pane. */
|
|
61
61
|
export declare const paneCurrentCommand: (session: string) => string | null;
|
|
62
62
|
/**
|
|
@@ -159,6 +159,22 @@ export interface ScreenSnapshot {
|
|
|
159
159
|
* token bucket as command injection.
|
|
160
160
|
*/
|
|
161
161
|
export declare const handleScreenRequest: (req: ScreenRequest) => ScreenSnapshot | null;
|
|
162
|
+
/**
|
|
163
|
+
* Translate the cursor into an index into the lines the frame carries.
|
|
164
|
+
*
|
|
165
|
+
* The visible pane is the tail of the capture, so the cursor's row counts back
|
|
166
|
+
* from the end — which also makes this correct after the byte cap has cut lines
|
|
167
|
+
* off the top. A cursor that falls outside what the frame carries returns null
|
|
168
|
+
* and simply is not drawn.
|
|
169
|
+
*/
|
|
170
|
+
export declare const cursorLineFor: (content: string, cursor: {
|
|
171
|
+
x: number;
|
|
172
|
+
y: number;
|
|
173
|
+
height: number;
|
|
174
|
+
}) => {
|
|
175
|
+
line: number;
|
|
176
|
+
col: number;
|
|
177
|
+
} | null;
|
|
162
178
|
/**
|
|
163
179
|
* After the phone injects keys, push fresh frames instead of waiting for its
|
|
164
180
|
* next screen request — the phone's post-tap re-pull raced the key landing
|
|
@@ -224,6 +240,15 @@ export type StreamFramePayload = {
|
|
|
224
240
|
/** Stream incarnation (stats.startedAt) — lets the receiver reset its
|
|
225
241
|
* seq high-water mark when the daemon restarts the stream. */
|
|
226
242
|
epoch?: number;
|
|
243
|
+
/** Where to draw the cursor: a 0-based index into the lines this frame
|
|
244
|
+
* carries, and a 0-based CELL column (not a string index — a wide glyph
|
|
245
|
+
* spans two cells, which is what the viewer positions by). Absent when the
|
|
246
|
+
* cursor is outside the captured region. Deliberately outside the E2EE
|
|
247
|
+
* envelope, like sessionName and seq: a coordinate without the text it
|
|
248
|
+
* points into says nothing, and keeping it in the clear means the viewer
|
|
249
|
+
* can place the cursor before the pane content finishes decrypting. */
|
|
250
|
+
cursorLine?: number;
|
|
251
|
+
cursorCol?: number;
|
|
227
252
|
/** Plaintext pane content — only on unencrypted streams. */
|
|
228
253
|
content?: string;
|
|
229
254
|
/** E2EE envelope — replaces `content` on encrypted streams. */
|
|
@@ -261,7 +286,10 @@ export declare const isStreamCapReached: (activeCount: number) => boolean;
|
|
|
261
286
|
export declare const buildStreamFrame: (captured: {
|
|
262
287
|
content: string;
|
|
263
288
|
truncated: boolean;
|
|
264
|
-
}, sessionName: string, subscriberPublicKey?: string
|
|
289
|
+
}, sessionName: string, subscriberPublicKey?: string, cursor?: {
|
|
290
|
+
line: number;
|
|
291
|
+
col: number;
|
|
292
|
+
} | null) => Promise<StreamFramePayload | null>;
|
|
265
293
|
/**
|
|
266
294
|
* Handle agent.stream.start / agent.stream.stop. Returns true when the message
|
|
267
295
|
* was a stream-control message (so the caller skips the one-shot screen-peek
|
|
@@ -407,7 +435,7 @@ interface HandlePushDeps {
|
|
|
407
435
|
paneCommand?: (session: string) => string | null;
|
|
408
436
|
inject?: (session: string, text: string) => boolean;
|
|
409
437
|
sendKeys?: (session: string, tokens: string[]) => boolean;
|
|
410
|
-
rateLimit?: (session: string) => boolean;
|
|
438
|
+
rateLimit?: (session: string, now?: number, cost?: number) => boolean;
|
|
411
439
|
now?: () => number;
|
|
412
440
|
/** Injectable for tests; defaults to the REST-backed downloader. */
|
|
413
441
|
downloadAttachments?: (pushId: string, files: PushFileAttachment[]) => Promise<string[]>;
|
package/dist/listener.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAoBH,OAAO,EAA2B,KAAK,SAAS,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AACzG,OAAO,EAAiD,KAAK,UAAU,EAA4C,MAAM,kBAAkB,CAAC;AAE5I,OAAO,EAA4E,KAAK,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACvI,OAAO,EAA6C,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAmCtG,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAElD,UAAU,YAAY;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAAI,UAAU,YAAY,EAAE,KAAG,MAS7C,CAAC;AAEnB;6BAC6B;AAC7B,eAAO,MAAM,iBAAiB,GAC1B,aAAa,MAAM,EACnB,qBAAqB,MAAM,GAAG,IAAI,EAClC,cAAc,MAAM,EACpB,OAAO,MAAM,KACd,OAEoD,CAAC;
|
|
1
|
+
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"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"}
|
package/dist/listener.js
CHANGED
|
@@ -25,7 +25,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
26
|
};
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
-
exports.handleListener = exports.resolveWsUrl = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.setAttachmentContext = exports.writeRemoteMarker = exports.collectSessions = exports.collectSessionsVerbose = exports.handleCommandInput = exports.pendingInputDecrypts = exports.MAX_PENDING_DECRYPTS = exports.MAX_INPUT_LANES = exports.validateInputMessage = exports.MAX_INPUT_BODY_CHARS = exports.MAX_INPUT_KEYS = exports.handleStreamControl = exports.buildStreamFrame = exports.isStreamCapReached = exports.stopAllStreams = exports.stopStream = exports.MAX_CONCURRENT_STREAMS = exports.STREAM_LEASE_MS = exports.claimFrameSend = exports.hasFrameBudget = exports.streamCadence = exports.MAX_FRAMES_PER_SEC = exports.BURST_WINDOW_MS = exports.BURST_INTERVAL_MS = exports.STREAM_INTERVAL_MS = exports.scheduleInjectSnapshots = exports.handleScreenRequest = exports.collectWatchHits = exports.resetPatternWatches = exports.setPatternWatches = exports.deriveSessionState = exports.resetSessionStates = exports.detectRemoteAgent = exports.parseSessionName = exports.invalidateTmuxSocketCache = exports.resolveKeys = exports.paneCurrentCommand = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = exports.sessionsReportDue = exports.sessionsFingerprint = exports.SESSION_REPORT_HEARTBEAT_MS = void 0;
|
|
28
|
+
exports.handleListener = exports.resolveWsUrl = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.setAttachmentContext = exports.writeRemoteMarker = exports.collectSessions = exports.collectSessionsVerbose = exports.handleCommandInput = exports.pendingInputDecrypts = exports.MAX_PENDING_DECRYPTS = exports.MAX_INPUT_LANES = exports.validateInputMessage = exports.MAX_INPUT_BODY_CHARS = exports.MAX_INPUT_KEYS = exports.handleStreamControl = exports.buildStreamFrame = exports.isStreamCapReached = exports.stopAllStreams = exports.stopStream = exports.MAX_CONCURRENT_STREAMS = exports.STREAM_LEASE_MS = exports.claimFrameSend = exports.hasFrameBudget = exports.streamCadence = exports.MAX_FRAMES_PER_SEC = exports.BURST_WINDOW_MS = exports.BURST_INTERVAL_MS = exports.STREAM_INTERVAL_MS = exports.scheduleInjectSnapshots = exports.cursorLineFor = exports.handleScreenRequest = exports.collectWatchHits = exports.resetPatternWatches = exports.setPatternWatches = exports.deriveSessionState = exports.resetSessionStates = exports.detectRemoteAgent = exports.parseSessionName = exports.invalidateTmuxSocketCache = exports.resolveKeys = exports.paneCurrentCommand = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = exports.sessionsReportDue = exports.sessionsFingerprint = exports.SESSION_REPORT_HEARTBEAT_MS = void 0;
|
|
29
29
|
const child_process_1 = require("child_process");
|
|
30
30
|
const crypto_1 = require("crypto");
|
|
31
31
|
const fs_1 = require("fs");
|
|
@@ -92,10 +92,19 @@ exports.sessionsFingerprint = sessionsFingerprint;
|
|
|
92
92
|
const sessionsReportDue = (fingerprint, lastSentFingerprint, lastSentAtMs, nowMs) => fingerprint !== lastSentFingerprint ||
|
|
93
93
|
nowMs - lastSentAtMs >= exports.SESSION_REPORT_HEARTBEAT_MS;
|
|
94
94
|
exports.sessionsReportDue = sessionsReportDue;
|
|
95
|
-
// Per-session token bucket — caps a runaway/compromised sender.
|
|
96
|
-
//
|
|
97
|
-
|
|
95
|
+
// Per-session token bucket — caps a runaway/compromised sender.
|
|
96
|
+
//
|
|
97
|
+
// Weighted rather than flat, because the two things that pass through it are
|
|
98
|
+
// not the same size. A command submit is one whole instruction to the agent;
|
|
99
|
+
// 30 a minute has always been the ceiling for that, and it stays exactly that
|
|
100
|
+
// (120 / SUBMIT_COST). A named key is a keystroke — arrowing through a menu or
|
|
101
|
+
// holding Backspace is ordinary human speed, and charging it as if it were an
|
|
102
|
+
// instruction made the phone's key row refuse a normal burst of taps after a
|
|
103
|
+
// couple of seconds.
|
|
104
|
+
const RATE_LIMIT_TOKENS = 120;
|
|
98
105
|
const RATE_LIMIT_WINDOW_MS = 60_000;
|
|
106
|
+
/** What one command submit costs, keeping its ceiling at the old 30/min. */
|
|
107
|
+
const SUBMIT_COST = 4;
|
|
99
108
|
// Shells are refused: a shell prompt + send-keys = arbitrary command exec.
|
|
100
109
|
const SHELL_COMMANDS = new Set(['bash', 'zsh', 'fish', 'sh', 'dash', 'ksh', 'tcsh', 'csh', 'pwsh']);
|
|
101
110
|
// Auth-failure close codes: retrying with the same bad credentials hammers
|
|
@@ -112,18 +121,18 @@ const pruneStaleBuckets = (now) => {
|
|
|
112
121
|
buckets.delete(key);
|
|
113
122
|
}
|
|
114
123
|
};
|
|
115
|
-
const checkRateLimit = (session, now = Date.now()) => {
|
|
124
|
+
const checkRateLimit = (session, now = Date.now(), cost = 1) => {
|
|
116
125
|
pruneStaleBuckets(now);
|
|
117
126
|
const b = buckets.get(session) ?? { tokens: RATE_LIMIT_TOKENS, lastRefillAt: now };
|
|
118
127
|
const elapsed = Math.max(0, now - b.lastRefillAt);
|
|
119
128
|
// Fractional refill is intentional: smooths the boundary so a session
|
|
120
129
|
// hitting the cap doesn't have to wait a full window for the next slot.
|
|
121
130
|
const refilled = Math.min(RATE_LIMIT_TOKENS, b.tokens + (elapsed / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_TOKENS);
|
|
122
|
-
if (refilled <
|
|
131
|
+
if (refilled < cost) {
|
|
123
132
|
buckets.set(session, { tokens: refilled, lastRefillAt: now });
|
|
124
133
|
return false;
|
|
125
134
|
}
|
|
126
|
-
buckets.set(session, { tokens: refilled -
|
|
135
|
+
buckets.set(session, { tokens: refilled - cost, lastRefillAt: now });
|
|
127
136
|
return true;
|
|
128
137
|
};
|
|
129
138
|
exports.checkRateLimit = checkRateLimit;
|
|
@@ -143,14 +152,53 @@ const isShellPane = (command) => {
|
|
|
143
152
|
return false;
|
|
144
153
|
return SHELL_COMMANDS.has(command);
|
|
145
154
|
};
|
|
155
|
+
/** Where a message is parked between `set-buffer` and `paste-buffer`. Named,
|
|
156
|
+
* so the user's own paste stack is never pushed onto, and `-d` drops it
|
|
157
|
+
* again the moment it has been delivered. */
|
|
158
|
+
const INJECT_BUFFER = 'zeph-inject';
|
|
159
|
+
const sendLiteral = (session, text) => (0, child_process_1.spawnSync)('tmux', tmuxArgs(['send-keys', '-l', '-t', session, text]), { stdio: ['ignore', 'ignore', 'pipe'] })
|
|
160
|
+
.status === 0;
|
|
146
161
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
162
|
+
* Put the message in the pane as a PASTE rather than as typing.
|
|
163
|
+
*
|
|
164
|
+
* `send-keys -l` hands the whole string to the application as if it had been
|
|
165
|
+
* typed at once, and a TUI that re-renders per character has to keep up with a
|
|
166
|
+
* burst it never sees from a human. With Hangul — three bytes and two columns
|
|
167
|
+
* per character — it does not: characters go missing and the wrapped line is
|
|
168
|
+
* painted twice. A phone message is a paste, so send it as one.
|
|
169
|
+
*
|
|
170
|
+
* `paste-buffer -p` brackets it only when the application has actually asked
|
|
171
|
+
* for bracketed paste (DECSET 2004); against one that has not, tmux sends the
|
|
172
|
+
* plain text and nothing changes. That gating is why this is safe to do
|
|
173
|
+
* unconditionally — the markers can never leak into an app that would show
|
|
174
|
+
* them as literal `[200~`.
|
|
175
|
+
*
|
|
176
|
+
* Falls back to the old path if either tmux call fails, because a message
|
|
177
|
+
* delivered imperfectly still beats one not delivered.
|
|
178
|
+
*/
|
|
179
|
+
const pasteText = (session, text) => {
|
|
180
|
+
// `--` so a message that begins with a dash is data, not an option. The
|
|
181
|
+
// buffer contents are data to paste-buffer as well, which preserves the
|
|
182
|
+
// property `send-keys -l` had: no escape sequence inside a message can
|
|
183
|
+
// drive another tmux command.
|
|
184
|
+
const set = (0, child_process_1.spawnSync)('tmux', tmuxArgs(['set-buffer', '-b', INJECT_BUFFER, '--', text]), { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
185
|
+
if (set.status !== 0)
|
|
186
|
+
return sendLiteral(session, text);
|
|
187
|
+
const paste = (0, child_process_1.spawnSync)('tmux', tmuxArgs(['paste-buffer', '-d', '-p', '-b', INJECT_BUFFER, '-t', session]), { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
188
|
+
if (paste.status === 0)
|
|
189
|
+
return true;
|
|
190
|
+
// `-d` never ran, so the buffer is still there holding the message.
|
|
191
|
+
(0, child_process_1.spawnSync)('tmux', tmuxArgs(['delete-buffer', '-b', INJECT_BUFFER]), { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
192
|
+
return sendLiteral(session, text);
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Inject text into a tmux session: the message as a paste, then a separate
|
|
196
|
+
* `Enter` to submit it. The two stay separate because a bracketed paste is
|
|
197
|
+
* text and only text — newlines inside it must not submit early, and the
|
|
198
|
+
* submit has to be a real key press.
|
|
150
199
|
*/
|
|
151
200
|
const injectKeys = (session, text) => {
|
|
152
|
-
|
|
153
|
-
if (a.status !== 0)
|
|
201
|
+
if (!pasteText(session, text))
|
|
154
202
|
return false;
|
|
155
203
|
const b = (0, child_process_1.spawnSync)('tmux', tmuxArgs(['send-keys', '-t', session, 'Enter']), { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
156
204
|
return b.status === 0;
|
|
@@ -728,6 +776,47 @@ const capturePane = (sessionName, escapes = false, lines = SCREEN_PEEK_LINES) =>
|
|
|
728
776
|
}
|
|
729
777
|
return { content, truncated };
|
|
730
778
|
};
|
|
779
|
+
/**
|
|
780
|
+
* Where the cursor sits, in the coordinates of a captured frame.
|
|
781
|
+
*
|
|
782
|
+
* `capture-pane` gives cells and colors but no cursor, so the mirror has never
|
|
783
|
+
* drawn one — which makes an arrow key indistinguishable from a dropped one.
|
|
784
|
+
* tmux reports the position separately; `pane_height` is what turns a
|
|
785
|
+
* pane-relative row into a row of the captured text, since the capture reaches
|
|
786
|
+
* back into scrollback and the visible pane is only its last `pane_height`
|
|
787
|
+
* lines.
|
|
788
|
+
*
|
|
789
|
+
* Read only for frames that are actually about to be sent — one extra tmux
|
|
790
|
+
* spawn per SENT frame rather than per tick.
|
|
791
|
+
*/
|
|
792
|
+
const capturePaneCursor = (sessionName) => {
|
|
793
|
+
const r = (0, child_process_1.spawnSync)('tmux', tmuxArgs(['display-message', '-p', '-t', sessionName, '#{cursor_x},#{cursor_y},#{pane_height}']), { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
794
|
+
if (r.status !== 0)
|
|
795
|
+
return null;
|
|
796
|
+
const [x, y, height] = (r.stdout ?? '').trim().split(',').map(Number);
|
|
797
|
+
if (![x, y, height].every((n) => Number.isSafeInteger(n) && n >= 0))
|
|
798
|
+
return null;
|
|
799
|
+
return { x, y, height };
|
|
800
|
+
};
|
|
801
|
+
/**
|
|
802
|
+
* Translate the cursor into an index into the lines the frame carries.
|
|
803
|
+
*
|
|
804
|
+
* The visible pane is the tail of the capture, so the cursor's row counts back
|
|
805
|
+
* from the end — which also makes this correct after the byte cap has cut lines
|
|
806
|
+
* off the top. A cursor that falls outside what the frame carries returns null
|
|
807
|
+
* and simply is not drawn.
|
|
808
|
+
*/
|
|
809
|
+
const cursorLineFor = (content, cursor) => {
|
|
810
|
+
// capture-pane ends the last row with a newline, which split() turns into a
|
|
811
|
+
// trailing empty element that is not a pane row.
|
|
812
|
+
const lines = content.split('\n');
|
|
813
|
+
const rows = lines.length > 0 && lines[lines.length - 1] === '' ? lines.length - 1 : lines.length;
|
|
814
|
+
const line = rows - cursor.height + cursor.y;
|
|
815
|
+
if (line < 0 || line >= rows)
|
|
816
|
+
return null;
|
|
817
|
+
return { line, col: cursor.x };
|
|
818
|
+
};
|
|
819
|
+
exports.cursorLineFor = cursorLineFor;
|
|
731
820
|
/** Delays after a key injection at which the pane is re-captured and pushed
|
|
732
821
|
* to the phone unsolicited. The first frame lands right after most TUI
|
|
733
822
|
* redraws; the second catches slow animations and is skipped when nothing
|
|
@@ -984,12 +1073,13 @@ echo) => {
|
|
|
984
1073
|
* (bad key, crypto not ready on the first tick) returns null so the caller
|
|
985
1074
|
* drops the frame — an encrypted stream never leaks a plaintext frame.
|
|
986
1075
|
*/
|
|
987
|
-
const buildStreamFrame = async (captured, sessionName, subscriberPublicKey) => {
|
|
1076
|
+
const buildStreamFrame = async (captured, sessionName, subscriberPublicKey, cursor) => {
|
|
988
1077
|
const base = {
|
|
989
1078
|
subtype: 'agent.stream.frame',
|
|
990
1079
|
sessionName,
|
|
991
1080
|
capturedAt: new Date().toISOString(),
|
|
992
1081
|
truncated: captured.truncated,
|
|
1082
|
+
...(cursor ? { cursorLine: cursor.line, cursorCol: cursor.col } : {}),
|
|
993
1083
|
};
|
|
994
1084
|
if (!subscriberPublicKey)
|
|
995
1085
|
return { ...base, content: captured.content };
|
|
@@ -1068,6 +1158,12 @@ const handleStreamControl = (req, send) => {
|
|
|
1068
1158
|
return true;
|
|
1069
1159
|
}
|
|
1070
1160
|
let lastContent = null;
|
|
1161
|
+
// The cursor's last reported place, as `line,col`. Part of the diff-gate
|
|
1162
|
+
// because moving the cursor is a change the CAPTURE cannot show: an arrow
|
|
1163
|
+
// key inside a prompt leaves every character where it was, so gating on the
|
|
1164
|
+
// text alone means the mirror's cursor never moves — which is precisely the
|
|
1165
|
+
// feedback the cursor exists to give.
|
|
1166
|
+
let lastCursor = null;
|
|
1071
1167
|
const stats = {
|
|
1072
1168
|
startedAt: Date.now(),
|
|
1073
1169
|
frames: 0,
|
|
@@ -1128,7 +1224,18 @@ const handleStreamControl = (req, send) => {
|
|
|
1128
1224
|
return true;
|
|
1129
1225
|
}
|
|
1130
1226
|
const captured = capturePane(sessionName, true, STREAM_CAPTURE_LINES);
|
|
1131
|
-
if (!captured
|
|
1227
|
+
if (!captured) {
|
|
1228
|
+
stats.skipped++;
|
|
1229
|
+
return true;
|
|
1230
|
+
}
|
|
1231
|
+
// Read per tick, not per sent frame: the gate below has to know whether
|
|
1232
|
+
// the cursor moved, and by the time a frame is being built the tick
|
|
1233
|
+
// that would have carried the move has already been skipped. One extra
|
|
1234
|
+
// tmux spawn on ticks that go on to send nothing is what that costs.
|
|
1235
|
+
const cursorAt = capturePaneCursor(sessionName);
|
|
1236
|
+
const cursor = cursorAt ? (0, exports.cursorLineFor)(captured.content, cursorAt) : null;
|
|
1237
|
+
const cursorKey = cursor ? `${cursor.line},${cursor.col}` : '';
|
|
1238
|
+
if (captured.content === lastContent && cursorKey === lastCursor) {
|
|
1132
1239
|
stats.skipped++;
|
|
1133
1240
|
return true; // diff-gate
|
|
1134
1241
|
}
|
|
@@ -1147,12 +1254,13 @@ const handleStreamControl = (req, send) => {
|
|
|
1147
1254
|
// fps by one frame per episode in the R2 numbers.
|
|
1148
1255
|
const inBurst = armedDelay === exports.BURST_INTERVAL_MS;
|
|
1149
1256
|
lastContent = captured.content;
|
|
1257
|
+
lastCursor = cursorKey;
|
|
1150
1258
|
// Stamp the sequence in CAPTURE order, synchronously — frame assembly
|
|
1151
1259
|
// is async (encryption) and fire-and-forget, so resolve order is not
|
|
1152
1260
|
// guaranteed under load; the receiver drops any seq it has already
|
|
1153
1261
|
// painted past.
|
|
1154
1262
|
const seq = ++wireSeq;
|
|
1155
|
-
void (0, exports.buildStreamFrame)(captured, sessionName, subscriberPublicKey).then((frame) => {
|
|
1263
|
+
void (0, exports.buildStreamFrame)(captured, sessionName, subscriberPublicKey, cursor).then((frame) => {
|
|
1156
1264
|
// Stream stopped or restarted while this frame was in flight —
|
|
1157
1265
|
// `stats` is unique per start, so it doubles as the identity
|
|
1158
1266
|
// token. Don't send into a dead/replaced stream or skew its stats.
|
|
@@ -1164,6 +1272,7 @@ const handleStreamControl = (req, send) => {
|
|
|
1164
1272
|
// subscriber key) never recovers: fail closed after a few
|
|
1165
1273
|
// strikes instead of retrying every tick for 5 minutes.
|
|
1166
1274
|
lastContent = null;
|
|
1275
|
+
lastCursor = null;
|
|
1167
1276
|
// Init still in flight (or failed — its own path fail-closes):
|
|
1168
1277
|
// a not-yet-ready key is not a malformed key, don't strike.
|
|
1169
1278
|
if ((0, crypto_js_1.getDevicePublicKey)() === null)
|
|
@@ -1655,11 +1764,11 @@ exports.collectSessions = collectSessions;
|
|
|
1655
1764
|
* the structured `agent.command` push type and the legacy `@<session>`
|
|
1656
1765
|
* prefix path route through here so the defense layers can't diverge.
|
|
1657
1766
|
*/
|
|
1658
|
-
const passesInjectGuards = (session, deps) => {
|
|
1767
|
+
const passesInjectGuards = (session, deps, cost = 1) => {
|
|
1659
1768
|
// Rate bucket first: the pane probe below is a blocking tmux spawnSync,
|
|
1660
1769
|
// and the sequencer can flush several held messages back-to-back — an
|
|
1661
1770
|
// empty bucket must refuse before paying that probe N times, not after.
|
|
1662
|
-
if (!(deps.rateLimit ?? exports.checkRateLimit)(session)) {
|
|
1771
|
+
if (!(deps.rateLimit ?? exports.checkRateLimit)(session, Date.now(), cost)) {
|
|
1663
1772
|
log(`! ${session}: rate-limited — drop`);
|
|
1664
1773
|
return false;
|
|
1665
1774
|
}
|
|
@@ -1702,7 +1811,7 @@ const tryInject = (session, text, deps) => {
|
|
|
1702
1811
|
log(`! ${session}: empty text — drop`);
|
|
1703
1812
|
return false;
|
|
1704
1813
|
}
|
|
1705
|
-
if (!passesInjectGuards(session, deps))
|
|
1814
|
+
if (!passesInjectGuards(session, deps, SUBMIT_COST))
|
|
1706
1815
|
return false;
|
|
1707
1816
|
const ok = (deps.inject ?? injectKeys)(session, text);
|
|
1708
1817
|
const preview = text.length > 60 ? text.slice(0, 60) + '…' : text;
|