cursorconnect 0.1.7 → 0.1.8

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.
@@ -13,7 +13,7 @@ WEBAPP_PASSWORD=
13
13
  # CURSOR_PROJECTS_DIR=
14
14
  LOG_LEVEL=info
15
15
 
16
- # Chat lenta: JSONL → agent:messages (liveMessages always []). DOM = state:patch (working, approve).
16
+ # Chat lenta: JSONL → agent:messages; liveMessages = DOM overlay until turn is in .jsonl.
17
17
  # CHAT_HISTORY_JSONL=1 — extra socket agent:history on app (legacy).
18
18
  # CHAT_HISTORY_JSONL=1
19
19
  # Follow active Cursor window in CDP (1.5s poll). Set FOCUS_SYNC_ENABLED=0 to disable.
@@ -25,3 +25,7 @@ LOG_LEVEL=info
25
25
  # RELAY_URL=https://your-domain.example
26
26
  # RELAY_TOKEN=same-as-relay-.env
27
27
  # RELAY_ROOM_ID=default
28
+ # macOS: не давать Mac уснуть, пока bridge запущен (cursorconnect start). Выключить: KEEP_AWAKE=0
29
+ # KEEP_AWAKE=0
30
+ # Периодический ping relay upstream (NAT/idle). 0 = выкл.
31
+ # RELAY_KEEPALIVE_MS=20000
@@ -1 +1 @@
1
- {"cliVersion":"0.1.7","bundledAt":"2026-05-25T18:20:35Z"}
1
+ {"cliVersion":"0.1.8","bundledAt":"2026-05-26T06:15:47Z"}
@@ -1,4 +1,4 @@
1
- import type { AgentsIndex, CursorState, HistoryMessage } from './types.js';
1
+ import type { AgentsIndex, CursorState } from './types.js';
2
2
  export declare const AGENT_COMPLETION_PUSH_BODY = "\u0410\u0433\u0435\u043D\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B \u0440\u0430\u0431\u043E\u0442\u0443";
3
3
  export declare function suppressAgentCompletionPush(ms?: number): void;
4
4
  export interface AgentCompletionPushPayload {
@@ -8,35 +8,28 @@ export interface AgentCompletionPushPayload {
8
8
  }
9
9
  export type AgentCompletionPushEmit = (payload: AgentCompletionPushPayload) => void;
10
10
  /**
11
- * Push when a new assistant row lands in JSONL for the current/recent task.
12
- * If JSONL flushes while DOM still shows "working", retry when DOM goes idle.
11
+ * Push once per real generation session: agent must be stably working (≥3 DOM polls),
12
+ * then stably idle (≥2 polls). Opening a finished chat may flicker isWorking for 1 poll — ignored.
13
13
  */
14
14
  export declare class AgentCompletionPush {
15
15
  private emit;
16
16
  private agentsIndex;
17
- private getState;
18
- /** Last seen assistant line id in JSONL (line number / ts). */
19
- private lastJsonlAssistantKey;
20
- /** Agents that were generating on previous DOM poll. */
21
17
  private prevWorking;
18
+ private workingPolls;
19
+ /** Agent had a real generation episode this bridge session (not open-chat flicker). */
20
+ private confirmedWorking;
21
+ private idlePolls;
22
22
  private lastEmitAt;
23
- /** JSONL had a new assistant line while DOM was still working — emit after idle. */
24
- private pendingAfterIdle;
25
- private pendingDeferSince;
26
- private deferTimers;
27
- private lastJsonlMessages;
23
+ private emittedThisSession;
24
+ /** ~3 × 400ms — ignore brief spinner when opening a finished chat. */
25
+ private static readonly WORKING_POLLS_REQUIRED;
26
+ /** ~2 × 400ms idle before push. */
27
+ private static readonly IDLE_POLLS_REQUIRED;
28
28
  private static readonly MIN_EMIT_GAP_MS;
29
- /** DOM can lie "working" after JSONL finished — emit anyway after this. */
30
- private static readonly MAX_DEFER_MS;
31
- constructor(emit: AgentCompletionPushEmit, agentsIndex: () => AgentsIndex | null, getState: () => CursorState);
32
- /** Sync working snapshot; flush pending push when a task leaves DOM "working". */
29
+ constructor(emit: AgentCompletionPushEmit, agentsIndex: () => AgentsIndex | null);
33
30
  observe(state: CursorState): void;
34
- /**
35
- * JSONL file updated — push if a new assistant message was appended for this task
36
- * and DOM no longer shows generation for that composer.
37
- */
38
- onJsonlUpdated(agentId: string, messages: HistoryMessage[], state: CursorState): void;
39
- private scheduleDeferTimeout;
40
- private clearDeferTimer;
31
+ private trackWorkingSessions;
32
+ private detectIdleCompletions;
33
+ private syncPrevWorking;
41
34
  private tryEmit;
42
35
  }
@@ -9,7 +9,6 @@ function isSuppressed() {
9
9
  function normalizeTitle(title) {
10
10
  return title.trim().replace(/\s+/g, ' ');
11
11
  }
12
- /** Ephemeral sidebar ids (tab-0) must not drive push — composerId can appear next poll. */
13
12
  function isStableComposerId(id) {
14
13
  if (!id?.trim())
15
14
  return false;
@@ -47,16 +46,14 @@ function resolveAgentChatTitle(agentId, state, agentsIndex) {
47
46
  }
48
47
  return 'Чат';
49
48
  }
50
- /**
51
- * Working agents for correlating JSONL flush with the task that was running.
52
- * Sidebar: stable composerId only (no tab-N flicker).
53
- */
49
+ /** Active + background sidebar tabs that show generation. */
54
50
  function snapshotWorkingAgents(state, agentsIndex) {
55
51
  const map = new Map();
56
52
  const activeId = state.activeComposerId;
57
- const activeGenerating = state.agentStatus === 'working' &&
53
+ const activeGenerating = !!state.agentWorking &&
58
54
  !!activeId &&
59
- isStableComposerId(activeId);
55
+ isStableComposerId(activeId) &&
56
+ !agentPausedForInput(state, activeId);
60
57
  if (activeGenerating && activeId) {
61
58
  map.set(activeId, resolveAgentChatTitle(activeId, state, agentsIndex));
62
59
  }
@@ -66,155 +63,133 @@ function snapshotWorkingAgents(state, agentsIndex) {
66
63
  const id = tab.composerId?.trim();
67
64
  if (!id || !isStableComposerId(id))
68
65
  continue;
66
+ if (agentPausedForInput(state, id))
67
+ continue;
69
68
  map.set(id, tab.title?.trim() || resolveAgentChatTitle(id, state, agentsIndex));
70
69
  }
71
70
  return map;
72
71
  }
73
- function agentPausedForInput(state) {
72
+ function agentPausedForInput(state, agentId) {
73
+ const isActive = state.activeComposerId === agentId;
74
+ if (!isActive)
75
+ return false;
74
76
  return ((state.pendingApprovals?.length ?? 0) > 0 ||
75
77
  !!state.pendingQuestionnaire ||
76
78
  state.agentStatus === 'waiting_approval' ||
77
79
  state.agentStatus === 'waiting_questionnaire');
78
80
  }
79
- function lastAssistantLineKey(messages) {
80
- for (let i = messages.length - 1; i >= 0; i--) {
81
- const m = messages[i];
82
- if (m.role === 'assistant')
83
- return m.ts ?? i;
81
+ function isStillWorkingWithTitle(title, now) {
82
+ const norm = normalizeTitle(title);
83
+ if (!norm)
84
+ return false;
85
+ for (const t of now.values()) {
86
+ if (normalizeTitle(t) === norm)
87
+ return true;
84
88
  }
85
- return 0;
89
+ return false;
86
90
  }
87
91
  /**
88
- * Push when a new assistant row lands in JSONL for the current/recent task.
89
- * If JSONL flushes while DOM still shows "working", retry when DOM goes idle.
92
+ * Push once per real generation session: agent must be stably working (≥3 DOM polls),
93
+ * then stably idle (≥2 polls). Opening a finished chat may flicker isWorking for 1 poll — ignored.
90
94
  */
91
95
  export class AgentCompletionPush {
92
96
  emit;
93
97
  agentsIndex;
94
- getState;
95
- /** Last seen assistant line id in JSONL (line number / ts). */
96
- lastJsonlAssistantKey = new Map();
97
- /** Agents that were generating on previous DOM poll. */
98
- prevWorking = new Map();
98
+ prevWorking = null;
99
+ workingPolls = new Map();
100
+ /** Agent had a real generation episode this bridge session (not open-chat flicker). */
101
+ confirmedWorking = new Set();
102
+ idlePolls = new Map();
99
103
  lastEmitAt = new Map();
100
- /** JSONL had a new assistant line while DOM was still working — emit after idle. */
101
- pendingAfterIdle = new Set();
102
- pendingDeferSince = new Map();
103
- deferTimers = new Map();
104
- lastJsonlMessages = new Map();
105
- static MIN_EMIT_GAP_MS = 45_000;
106
- /** DOM can lie "working" after JSONL finished — emit anyway after this. */
107
- static MAX_DEFER_MS = 12_000;
108
- constructor(emit, agentsIndex, getState) {
104
+ emittedThisSession = new Set();
105
+ /** ~3 × 400ms — ignore brief spinner when opening a finished chat. */
106
+ static WORKING_POLLS_REQUIRED = 3;
107
+ /** ~2 × 400ms idle before push. */
108
+ static IDLE_POLLS_REQUIRED = 2;
109
+ static MIN_EMIT_GAP_MS = 8_000;
110
+ constructor(emit, agentsIndex) {
109
111
  this.emit = emit;
110
112
  this.agentsIndex = agentsIndex;
111
- this.getState = getState;
112
113
  }
113
- /** Sync working snapshot; flush pending push when a task leaves DOM "working". */
114
114
  observe(state) {
115
- const index = this.agentsIndex();
116
- if (isSuppressed() || agentPausedForInput(state)) {
117
- this.prevWorking = snapshotWorkingAgents(state, index);
115
+ if (isSuppressed()) {
116
+ this.syncPrevWorking(state);
118
117
  return;
119
118
  }
120
- const nowWorking = snapshotWorkingAgents(state, index);
121
- for (const agentId of this.pendingAfterIdle) {
122
- if (nowWorking.has(agentId))
123
- continue;
124
- const messages = this.lastJsonlMessages.get(agentId);
125
- if (messages?.length) {
126
- this.tryEmit(agentId, messages, state, 'dom-idle');
119
+ const index = this.agentsIndex();
120
+ const now = snapshotWorkingAgents(state, index);
121
+ this.trackWorkingSessions(now);
122
+ this.detectIdleCompletions(now, state);
123
+ }
124
+ trackWorkingSessions(now) {
125
+ for (const id of now.keys()) {
126
+ const polls = (this.workingPolls.get(id) ?? 0) + 1;
127
+ this.workingPolls.set(id, polls);
128
+ if (polls >= AgentCompletionPush.WORKING_POLLS_REQUIRED && !this.confirmedWorking.has(id)) {
129
+ this.confirmedWorking.add(id);
130
+ this.emittedThisSession.delete(id);
131
+ this.idlePolls.delete(id);
127
132
  }
128
133
  }
129
- this.prevWorking = nowWorking;
134
+ for (const id of this.workingPolls.keys()) {
135
+ if (!now.has(id))
136
+ this.workingPolls.delete(id);
137
+ }
130
138
  }
131
- /**
132
- * JSONL file updated — push if a new assistant message was appended for this task
133
- * and DOM no longer shows generation for that composer.
134
- */
135
- onJsonlUpdated(agentId, messages, state) {
136
- if (isSuppressed() || agentPausedForInput(state))
137
- return;
138
- if (!isStableComposerId(agentId))
139
- return;
140
- const lineKey = lastAssistantLineKey(messages);
141
- if (!lineKey)
142
- return;
143
- const prevKey = this.lastJsonlAssistantKey.get(agentId);
144
- if (prevKey === undefined) {
145
- this.lastJsonlAssistantKey.set(agentId, lineKey);
146
- this.lastJsonlMessages.set(agentId, messages);
139
+ detectIdleCompletions(now, state) {
140
+ const prev = this.prevWorking;
141
+ if (prev === null) {
142
+ this.prevWorking = new Map(now);
147
143
  return;
148
144
  }
149
- if (lineKey <= prevKey)
150
- return;
151
- this.lastJsonlAssistantKey.set(agentId, lineKey);
152
- this.lastJsonlMessages.set(agentId, messages);
153
- const nowWorking = snapshotWorkingAgents(state, this.agentsIndex());
154
- if (nowWorking.has(agentId)) {
155
- this.pendingAfterIdle.add(agentId);
156
- if (!this.pendingDeferSince.has(agentId)) {
157
- this.pendingDeferSince.set(agentId, Date.now());
145
+ for (const [agentId] of prev) {
146
+ if (now.has(agentId)) {
147
+ this.idlePolls.delete(agentId);
148
+ continue;
158
149
  }
159
- this.scheduleDeferTimeout(agentId);
160
- console.log(`[agent-completion-push] defer agentId=${agentId.slice(0, 12)} line=${lineKey} (dom still working)`);
161
- return;
150
+ if (!this.confirmedWorking.has(agentId))
151
+ continue;
152
+ const polls = (this.idlePolls.get(agentId) ?? 0) + 1;
153
+ this.idlePolls.set(agentId, polls);
154
+ if (polls < AgentCompletionPush.IDLE_POLLS_REQUIRED)
155
+ continue;
156
+ this.tryEmit(agentId, state);
162
157
  }
163
- this.clearDeferTimer(agentId);
164
- this.pendingDeferSince.delete(agentId);
165
- this.tryEmit(agentId, messages, state, 'jsonl');
166
- }
167
- scheduleDeferTimeout(agentId) {
168
- if (this.deferTimers.has(agentId))
169
- return;
170
- const timer = setTimeout(() => {
171
- this.deferTimers.delete(agentId);
172
- const since = this.pendingDeferSince.get(agentId) ?? 0;
173
- if (!this.pendingAfterIdle.has(agentId))
174
- return;
175
- if (Date.now() - since < AgentCompletionPush.MAX_DEFER_MS - 100)
176
- return;
177
- const messages = this.lastJsonlMessages.get(agentId);
178
- if (!messages?.length)
179
- return;
180
- console.log(`[agent-completion-push] defer timeout agentId=${agentId.slice(0, 12)} (dom still working, jsonl done)`);
181
- this.tryEmit(agentId, messages, this.getState(), 'defer-timeout');
182
- }, AgentCompletionPush.MAX_DEFER_MS);
183
- this.deferTimers.set(agentId, timer);
158
+ for (const id of this.idlePolls.keys()) {
159
+ if (now.has(id))
160
+ this.idlePolls.delete(id);
161
+ }
162
+ this.prevWorking = new Map(now);
184
163
  }
185
- clearDeferTimer(agentId) {
186
- const t = this.deferTimers.get(agentId);
187
- if (t)
188
- clearTimeout(t);
189
- this.deferTimers.delete(agentId);
164
+ syncPrevWorking(state) {
165
+ this.prevWorking = snapshotWorkingAgents(state, this.agentsIndex());
190
166
  }
191
- tryEmit(agentId, messages, state, reason) {
192
- const lineKey = lastAssistantLineKey(messages);
193
- if (!lineKey)
167
+ tryEmit(agentId, state) {
168
+ if (agentPausedForInput(state, agentId))
169
+ return;
170
+ if (this.emittedThisSession.has(agentId))
171
+ return;
172
+ if (!this.confirmedWorking.has(agentId))
194
173
  return;
195
- const nowWorking = snapshotWorkingAgents(state, this.agentsIndex());
196
- if (reason !== 'defer-timeout' && nowWorking.has(agentId))
174
+ const now = snapshotWorkingAgents(state, this.agentsIndex());
175
+ if (now.has(agentId))
197
176
  return;
198
- const isCurrentTask = state.activeComposerId === agentId ||
199
- this.prevWorking.has(agentId) ||
200
- this.pendingAfterIdle.has(agentId);
201
- if (!isCurrentTask)
177
+ const title = normalizeTitle(resolveAgentChatTitle(agentId, state, this.agentsIndex())) || 'Чат';
178
+ if (isStillWorkingWithTitle(title, now))
202
179
  return;
203
180
  const last = this.lastEmitAt.get(agentId) ?? 0;
204
181
  if (Date.now() - last < AgentCompletionPush.MIN_EMIT_GAP_MS)
205
182
  return;
206
- const chatTitle = normalizeTitle(this.prevWorking.get(agentId) ??
207
- resolveAgentChatTitle(agentId, state, this.agentsIndex())) || 'Чат';
208
183
  this.emit({
209
184
  agentId,
210
- chatTitle,
185
+ chatTitle: title,
211
186
  body: AGENT_COMPLETION_PUSH_BODY,
212
187
  });
213
188
  this.lastEmitAt.set(agentId, Date.now());
214
- this.pendingAfterIdle.delete(agentId);
215
- this.pendingDeferSince.delete(agentId);
216
- this.clearDeferTimer(agentId);
217
- this.prevWorking.delete(agentId);
218
- console.log(`[agent-completion-push] → relay reason=${reason} agentId=${agentId.slice(0, 12)} title=${chatTitle.slice(0, 48)} line=${lineKey}`);
189
+ this.emittedThisSession.add(agentId);
190
+ this.confirmedWorking.delete(agentId);
191
+ this.idlePolls.delete(agentId);
192
+ this.workingPolls.delete(agentId);
193
+ console.log(`[agent-completion-push] → relay dom-idle agentId=${agentId.slice(0, 12)} title=${title.slice(0, 48)}`);
219
194
  }
220
195
  }
@@ -1,7 +1,7 @@
1
1
  import type { ChatMessage, HistoryMessage } from './types.js';
2
2
  /**
3
3
  * Chat UI store: JSONL baseline for `agent:messages` (client lenta).
4
- * DOM transcript kept for debug snapshot only not sent as liveMessages.
4
+ * DOM overlay `liveMessages` in `agent:messages` until JSONL has the same turn.
5
5
  */
6
6
  export declare class ChatDisplayStore {
7
7
  private readonly domTranscript;
@@ -2,7 +2,7 @@ import { filterClientDisplayList, historyRowsToChat, mergeHistoryTail, messagesE
2
2
  import { DomTranscriptStore } from './dom-transcript-store.js';
3
3
  /**
4
4
  * Chat UI store: JSONL baseline for `agent:messages` (client lenta).
5
- * DOM transcript kept for debug snapshot only not sent as liveMessages.
5
+ * DOM overlay `liveMessages` in `agent:messages` until JSONL has the same turn.
6
6
  */
7
7
  export class ChatDisplayStore {
8
8
  domTranscript = new DomTranscriptStore();
@@ -24,6 +24,9 @@ export class ChatDisplayStore {
24
24
  /** Reload JSONL; prune DOM rows now covered by archive. */
25
25
  setJsonlBaseline(agentId, rows) {
26
26
  const prevRows = this.jsonlRowCount.get(agentId) ?? 0;
27
+ if (rows.length > 0 && prevRows > 24 && rows.length < prevRows - 12) {
28
+ return;
29
+ }
27
30
  const prev = this.jsonlBaseline.get(agentId) ?? [];
28
31
  let prepared;
29
32
  if (rows.length > prevRows && prevRows > 0 && prev.length && rows.length - prevRows <= 128) {
@@ -29,6 +29,8 @@ export function loadConfig() {
29
29
  relayToken: process.env.RELAY_TOKEN?.trim() ?? '',
30
30
  relayRoomId: identity?.roomId ?? relayRoomFromEnv,
31
31
  pairingClientToken: identity?.clientToken,
32
+ keepAwakeEnabled: process.platform === 'darwin' && process.env.KEEP_AWAKE?.trim() !== '0',
33
+ relayKeepaliveMs: Math.max(0, parseInt(process.env.RELAY_KEEPALIVE_MS ?? '20000', 10) || 0),
32
34
  };
33
35
  }
34
36
  export function loadSelectors() {
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'fs';
2
2
  import { dirname, join } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
- const MIN_FALLBACK = '0.1.5';
4
+ const MIN_FALLBACK = '0.1.7';
5
5
  function readJsonVersion(path) {
6
6
  if (!existsSync(path))
7
7
  return null;
@@ -1,7 +1,7 @@
1
1
  import type { ChatMessage } from './types.js';
2
2
  /**
3
3
  * Append-only per-agent transcript built from DOM snapshots.
4
- * Debug-only DOM transcript; not used for client chat lenta (JSONL-only).
4
+ * DOM transcript for live overlay until JSONL covers the same turn.
5
5
  */
6
6
  export declare class DomTranscriptStore {
7
7
  private rowsByAgent;
@@ -7,7 +7,7 @@ function transcriptKey(m) {
7
7
  }
8
8
  /**
9
9
  * Append-only per-agent transcript built from DOM snapshots.
10
- * Debug-only DOM transcript; not used for client chat lenta (JSONL-only).
10
+ * DOM transcript for live overlay until JSONL covers the same turn.
11
11
  */
12
12
  export class DomTranscriptStore {
13
13
  rowsByAgent = new Map();
@@ -8,8 +8,11 @@ import { WindowMonitor } from './window-monitor.js';
8
8
  import { Relay } from './relay.js';
9
9
  import { MessageDebugStore } from './message-debug-store.js';
10
10
  import { connectorClientVersion } from './connector-client-version.js';
11
+ import { installKeepAwakeShutdown, startKeepAwake } from './keep-awake.js';
11
12
  async function main() {
12
13
  const config = loadConfig();
14
+ startKeepAwake(config.keepAwakeEnabled);
15
+ installKeepAwakeShutdown();
13
16
  const selectors = loadSelectors();
14
17
  const connectorVersion = connectorClientVersion();
15
18
  console.log('=== CursorConnect Bridge ===');
@@ -18,6 +21,12 @@ async function main() {
18
21
  console.log(`Server: http://${config.serverHost}:${config.serverPort}`);
19
22
  if (config.relayUrl) {
20
23
  console.log(`Relay upstream: ${config.relayUrl} (room=${config.relayRoomId})`);
24
+ if (config.relayKeepaliveMs > 0) {
25
+ console.log(`Relay keepalive: every ${config.relayKeepaliveMs}ms`);
26
+ }
27
+ }
28
+ if (config.keepAwakeEnabled) {
29
+ console.log('Keep-awake: on (macOS caffeinate)');
21
30
  }
22
31
  console.log(`Projects: ${config.cursorProjectsDir}`);
23
32
  const stateManager = new StateManager(config.debounceMs);
@@ -0,0 +1,5 @@
1
+ /** macOS: `caffeinate -w <pid>` — Mac не уходит в сон, пока живёт bridge. */
2
+ export declare function startKeepAwake(enabled: boolean): void;
3
+ export declare function stopKeepAwake(): void;
4
+ export declare function isKeepAwakeActive(): boolean;
5
+ export declare function installKeepAwakeShutdown(): void;
@@ -0,0 +1,48 @@
1
+ import { spawn } from 'child_process';
2
+ let caffeinate = null;
3
+ /** macOS: `caffeinate -w <pid>` — Mac не уходит в сон, пока живёт bridge. */
4
+ export function startKeepAwake(enabled) {
5
+ if (!enabled || process.platform !== 'darwin')
6
+ return;
7
+ if (caffeinate)
8
+ return;
9
+ try {
10
+ caffeinate = spawn('caffeinate', ['-w', String(process.pid)], {
11
+ stdio: 'ignore',
12
+ });
13
+ caffeinate.on('error', (err) => {
14
+ console.warn(`[keep-awake] caffeinate error: ${err.message}`);
15
+ caffeinate = null;
16
+ });
17
+ caffeinate.on('exit', (code, signal) => {
18
+ if (code != null && code !== 0) {
19
+ console.warn(`[keep-awake] caffeinate exited code=${code} signal=${signal ?? '-'}`);
20
+ }
21
+ caffeinate = null;
22
+ });
23
+ console.log('[keep-awake] macOS sleep prevention on (caffeinate -w bridge)');
24
+ }
25
+ catch (e) {
26
+ console.warn(`[keep-awake] start failed: ${e.message}`);
27
+ }
28
+ }
29
+ export function stopKeepAwake() {
30
+ if (!caffeinate)
31
+ return;
32
+ try {
33
+ caffeinate.kill('SIGTERM');
34
+ }
35
+ catch {
36
+ /* already dead */
37
+ }
38
+ caffeinate = null;
39
+ }
40
+ export function isKeepAwakeActive() {
41
+ return caffeinate != null && caffeinate.exitCode == null;
42
+ }
43
+ export function installKeepAwakeShutdown() {
44
+ const stop = () => stopKeepAwake();
45
+ process.once('SIGINT', stop);
46
+ process.once('SIGTERM', stop);
47
+ process.once('exit', stop);
48
+ }
@@ -5,9 +5,12 @@ export declare class RelayUpstream {
5
5
  private onClientEvent;
6
6
  private onConnect?;
7
7
  private socket;
8
+ private keepaliveTimer;
8
9
  constructor(config: ServerConfig, onClientEvent: UpstreamCommandHandler, onConnect?: (() => void) | undefined);
9
10
  private registerPairing;
10
11
  connect(): void;
12
+ private startKeepalive;
13
+ private stopKeepalive;
11
14
  emit(event: string, ...args: unknown[]): void;
12
15
  get connected(): boolean;
13
16
  private handleHttpProxy;
@@ -14,6 +14,7 @@ export class RelayUpstream {
14
14
  onClientEvent;
15
15
  onConnect;
16
16
  socket = null;
17
+ keepaliveTimer = null;
17
18
  constructor(config, onClientEvent, onConnect) {
18
19
  this.config = config;
19
20
  this.onClientEvent = onClientEvent;
@@ -85,6 +86,26 @@ export class RelayUpstream {
85
86
  this.socket.on('relay:http', (req) => {
86
87
  void this.handleHttpProxy(req);
87
88
  });
89
+ this.startKeepalive();
90
+ }
91
+ startKeepalive() {
92
+ this.stopKeepalive();
93
+ const ms = this.config.relayKeepaliveMs;
94
+ if (!this.config.relayUrl || ms <= 0)
95
+ return;
96
+ this.keepaliveTimer = setInterval(() => {
97
+ if (!this.socket?.connected)
98
+ return;
99
+ this.socket.emit('relay:connector-ping', {
100
+ roomId: this.config.relayRoomId,
101
+ ts: Date.now(),
102
+ });
103
+ }, ms);
104
+ }
105
+ stopKeepalive() {
106
+ if (this.keepaliveTimer)
107
+ clearInterval(this.keepaliveTimer);
108
+ this.keepaliveTimer = null;
88
109
  }
89
110
  emit(event, ...args) {
90
111
  if (this.socket?.connected)
@@ -28,6 +28,11 @@ export declare class Relay {
28
28
  private readonly chatDisplay;
29
29
  /** Raw JSONL row count last sent per agent (live `append` emits). */
30
30
  private readonly lastEmittedJsonlRows;
31
+ /** Last display tail pushed to app — re-emit when user/assistant tail changes. */
32
+ private readonly lastEmittedLentaSig;
33
+ /** Display bubble count last sent — block regression 214→74 style wipes on phone. */
34
+ private readonly lastEmittedHistLen;
35
+ private domOverlayTimer;
31
36
  constructor(config: ServerConfig, stateManager: StateManager, commandExecutor: CommandExecutor, cdpBridge: CDPBridge, jsonlIndex: JsonlIndex, messageDebugStore: MessageDebugStore, domExtractor: DOMExtractor);
32
37
  private emitAgentCompletedPush;
33
38
  private flushPendingPushPayloads;
@@ -38,9 +43,15 @@ export declare class Relay {
38
43
  private readOnlyChatSnapshot;
39
44
  /** Push JSONL file updates to every subscribed route id (e.g. sidebar-0) for that composer. */
40
45
  private syncJsonlToSubscribedAgents;
41
- /** Live JSONL: push display deltas to app; `totalMessages` = raw `.jsonl` line count. */
46
+ /** JSONL baseline + DOM rows not yet in file (`liveMessages`). */
47
+ private lentaSnapshot;
48
+ private emitLentaForAgent;
49
+ private lentaTailSignature;
50
+ /** Live JSONL → `agent:messages` for subscribed chats (phone lenta). */
42
51
  private emitJsonlLiveForAgent;
43
- /** Client lenta: JSONL only (`liveMessages` always empty). DOM stays in debug snapshot. */
52
+ private scheduleDomOverlayEmit;
53
+ /** DOM poll while agent works — overlay until the same turn lands in JSONL. */
54
+ private emitDomOverlayForSyncedSubscribers;
44
55
  private agentMessagesSnapshot;
45
56
  private checkMediaAuth;
46
57
  private setupHttp;
@@ -49,7 +60,6 @@ export declare class Relay {
49
60
  private historySeqByAgent;
50
61
  private nextHistorySeq;
51
62
  private emitAgentMessages;
52
- /** DOM messages in state:patch — debug/UI chrome only; chat lenta is JSONL via agent:messages. */
53
63
  private prepareStateMessages;
54
64
  private withDisplayState;
55
65
  private wireEvents;
@@ -6,9 +6,9 @@ import { readAllowedMediaFile, resolveMediaPathParam } from './media-path.js';
6
6
  import { Server as SocketServer } from 'socket.io';
7
7
  import { resolveJsonlFilePath } from './agent-title-match.js';
8
8
  import { ChatDisplayStore } from './chat-display-store.js';
9
- import { AGENT_HISTORY_DEFAULT_LIMIT } from './history-limit.js';
10
9
  import { resolveHistoryLimit } from './history-request.js';
11
10
  import { filterClientDisplayList, prepareChatMessagesForDisplay, } from './chat-display.js';
11
+ import { isKeepAwakeActive } from './keep-awake.js';
12
12
  function sleepMs(ms) {
13
13
  return new Promise((resolve) => setTimeout(resolve, ms));
14
14
  }
@@ -20,6 +20,7 @@ import { RelayUpstream } from './relay-upstream.js';
20
20
  import { bridgePipelineLog, bridgePipelineReportLines, bridgePipelineSnapshot, } from './history-pipeline-log.js';
21
21
  import { DEBUG_CHATS_PAGE_HTML } from './debug-chats-page.js';
22
22
  import { isChatHistoryFromJsonl } from './chat-history-mode.js';
23
+ import { isChatSyncedWithCursor } from './chat-sync.js';
23
24
  import { readJsonlLiveSnapshot } from './jsonl-live-debug.js';
24
25
  export class Relay {
25
26
  stateManager;
@@ -44,6 +45,11 @@ export class Relay {
44
45
  chatDisplay = new ChatDisplayStore();
45
46
  /** Raw JSONL row count last sent per agent (live `append` emits). */
46
47
  lastEmittedJsonlRows = new Map();
48
+ /** Last display tail pushed to app — re-emit when user/assistant tail changes. */
49
+ lastEmittedLentaSig = new Map();
50
+ /** Display bubble count last sent — block regression 214→74 style wipes on phone. */
51
+ lastEmittedHistLen = new Map();
52
+ domOverlayTimer = null;
47
53
  constructor(config, stateManager, commandExecutor, cdpBridge, jsonlIndex, messageDebugStore, domExtractor) {
48
54
  this.stateManager = stateManager;
49
55
  this.commandExecutor = commandExecutor;
@@ -67,7 +73,7 @@ export class Relay {
67
73
  this.upstream = new RelayUpstream(this.config, (event, ...args) => {
68
74
  this.handleRemoteClientEvent(event, ...args);
69
75
  }, () => this.flushPendingPushPayloads());
70
- this.agentCompletionPush = new AgentCompletionPush((payload) => this.emitAgentCompletedPush(payload), () => this.lastJsonlIndex, () => this.stateManager.getState());
76
+ this.agentCompletionPush = new AgentCompletionPush((payload) => this.emitAgentCompletedPush(payload), () => this.lastJsonlIndex);
71
77
  this.upstream.connect();
72
78
  }
73
79
  }
@@ -83,11 +89,14 @@ export class Relay {
83
89
  flushPendingPushPayloads() {
84
90
  if (!this.upstream?.connected || !this.pendingPushPayloads.length)
85
91
  return;
86
- const batch = this.pendingPushPayloads.splice(0);
87
- for (const payload of batch) {
92
+ const byAgent = new Map();
93
+ for (const payload of this.pendingPushPayloads.splice(0)) {
94
+ byAgent.set(payload.agentId, payload);
95
+ }
96
+ for (const payload of byAgent.values()) {
88
97
  this.upstream.emit('push:agent-completed', payload);
89
98
  }
90
- console.log(`[agent-completion-push] flushed ${batch.length} queued push(es) to relay`);
99
+ console.log(`[agent-completion-push] flushed ${byAgent.size} queued push(es) to relay`);
91
100
  }
92
101
  observeAgentCompletionForPush() {
93
102
  this.agentCompletionPush?.observe(this.stateManager.getState());
@@ -163,46 +172,96 @@ export class Relay {
163
172
  this.emitJsonlLiveForAgent(agentId, messages, totalMessages);
164
173
  }
165
174
  }
166
- /** Live JSONL: push display deltas to app; `totalMessages` = raw `.jsonl` line count. */
175
+ /** JSONL baseline + DOM rows not yet in file (`liveMessages`). */
176
+ lentaSnapshot(agentId, totalMessages) {
177
+ const historyMessages = this.chatDisplay.getJsonlHistory(agentId);
178
+ const liveMessages = this.chatDisplay.getDomLive(agentId);
179
+ const source = liveMessages.length ? 'hybrid' : 'jsonl';
180
+ return {
181
+ historyMessages,
182
+ liveMessages,
183
+ messages: [...historyMessages, ...liveMessages],
184
+ totalMessages: totalMessages ?? historyMessages.length,
185
+ source: source,
186
+ };
187
+ }
188
+ emitLentaForAgent(agentId, opts) {
189
+ const snap = this.lentaSnapshot(agentId, opts.totalMessages);
190
+ const historyOut = opts.historyPayload ?? snap.historyMessages;
191
+ if (!historyOut.length && !snap.liveMessages.length)
192
+ return;
193
+ this.emitAgentMessages(agentId, historyOut, snap.liveMessages, snap.source, opts.totalMessages, opts.seq, opts.append);
194
+ }
195
+ lentaTailSignature(messages) {
196
+ return messages
197
+ .slice(-4)
198
+ .map((m) => `${m.role}:${m.id ?? ''}:${(m.text ?? '').length}:${(m.text ?? '').slice(-48)}`)
199
+ .join('|');
200
+ }
201
+ /** Live JSONL → `agent:messages` for subscribed chats (phone lenta). */
167
202
  emitJsonlLiveForAgent(agentId, rows, totalMessages) {
168
- const prevDisplay = this.chatDisplay.getJsonlHistory(agentId);
169
- const prevLen = prevDisplay.length;
170
- const prevLast = prevLen ? prevDisplay[prevLen - 1] : undefined;
203
+ const prevTotal = this.lastEmittedJsonlRows.get(agentId) ?? 0;
204
+ const prevSig = this.lastEmittedLentaSig.get(agentId);
171
205
  this.chatDisplay.setJsonlBaseline(agentId, rows);
172
206
  this.lastEmittedJsonlRows.set(agentId, totalMessages);
173
207
  const historyMessages = this.chatDisplay.getJsonlHistory(agentId);
174
- if (!historyMessages.length)
208
+ if (!historyMessages.length && !this.chatDisplay.getDomLive(agentId).length)
175
209
  return;
176
- if (!prevLen) {
177
- this.emitAgentMessages(agentId, historyMessages, [], 'jsonl', totalMessages, this.nextHistorySeq(agentId), false);
210
+ if (!this.jsonlIndex.getSubscribedAgents().has(agentId))
178
211
  return;
179
- }
180
- const appended = historyMessages.slice(prevLen);
181
- if (appended.length) {
182
- this.emitAgentMessages(agentId, appended, [], 'jsonl', totalMessages, this.nextHistorySeq(agentId), true);
212
+ const sig = this.lentaTailSignature(historyMessages);
213
+ const totalGrew = totalMessages > prevTotal;
214
+ if (!totalGrew && sig === prevSig && prevSig)
183
215
  return;
184
- }
185
- const last = historyMessages[historyMessages.length - 1];
186
- if (prevLast &&
187
- last &&
188
- last.role === prevLast.role &&
189
- (last.text ?? '') !== (prevLast.text ?? '')) {
190
- this.emitAgentMessages(agentId, [last], [], 'jsonl', totalMessages, this.nextHistorySeq(agentId), true);
216
+ const histLen = historyMessages.length;
217
+ const prevHistLen = this.lastEmittedHistLen.get(agentId) ?? 0;
218
+ if (prevHistLen > 24 && histLen < prevHistLen - 12) {
219
+ bridgePipelineLog({
220
+ dir: 'internal',
221
+ event: 'agent:messages:SKIP_REGRESSION',
222
+ agentId,
223
+ msgs: histLen,
224
+ detail: `prev=${prevHistLen} total=${totalMessages}`,
225
+ });
191
226
  return;
192
227
  }
193
- if (prevLen !== historyMessages.length || !this.jsonlIndex.getSubscribedAgents().has(agentId)) {
194
- this.emitAgentMessages(agentId, historyMessages, [], 'jsonl', totalMessages, this.nextHistorySeq(agentId), false);
228
+ this.lastEmittedLentaSig.set(agentId, sig);
229
+ this.lastEmittedHistLen.set(agentId, Math.max(prevHistLen, histLen));
230
+ this.emitLentaForAgent(agentId, {
231
+ totalMessages,
232
+ seq: this.nextHistorySeq(agentId),
233
+ });
234
+ }
235
+ scheduleDomOverlayEmit() {
236
+ if (this.domOverlayTimer)
237
+ clearTimeout(this.domOverlayTimer);
238
+ this.domOverlayTimer = setTimeout(() => {
239
+ this.domOverlayTimer = null;
240
+ this.emitDomOverlayForSyncedSubscribers();
241
+ }, 120);
242
+ }
243
+ /** DOM poll while agent works — overlay until the same turn lands in JSONL. */
244
+ emitDomOverlayForSyncedSubscribers() {
245
+ const state = this.stateManager.getState();
246
+ if (!state.messages?.length)
247
+ return;
248
+ const domRows = this.prepareStateMessages(state.messages);
249
+ for (const [subId, meta] of this.jsonlIndex.getSubscribedAgents()) {
250
+ if (!isChatSyncedWithCursor(subId, meta.title, state))
251
+ continue;
252
+ this.chatDisplay.mergeLiveForAgent(subId, domRows);
253
+ const snap = this.lentaSnapshot(subId, this.lastEmittedJsonlRows.get(subId) ?? undefined);
254
+ if (!snap.liveMessages.length && !state.agentWorking)
255
+ continue;
256
+ this.lastEmittedLentaSig.delete(subId);
257
+ this.emitLentaForAgent(subId, {
258
+ totalMessages: snap.totalMessages,
259
+ seq: this.nextHistorySeq(subId),
260
+ });
195
261
  }
196
262
  }
197
- /** Client lenta: JSONL only (`liveMessages` always empty). DOM stays in debug snapshot. */
198
263
  agentMessagesSnapshot(agentId, totalMessages) {
199
- const historyMessages = this.chatDisplay.getJsonlHistory(agentId);
200
- return {
201
- historyMessages,
202
- liveMessages: [],
203
- messages: historyMessages,
204
- totalMessages: totalMessages ?? historyMessages.length,
205
- };
264
+ return this.lentaSnapshot(agentId, totalMessages);
206
265
  }
207
266
  checkMediaAuth(req) {
208
267
  if (!this.authEnabled)
@@ -218,6 +277,7 @@ export class Relay {
218
277
  res.json({
219
278
  ok: true,
220
279
  cdp: this.cdpBridge.getClient()?.isConnected() ?? false,
280
+ keepAwake: isKeepAwakeActive(),
221
281
  debugChats: '/debug/chats',
222
282
  debugSnapshot: '/debug/chat-snapshot',
223
283
  debugJsonlLive: '/debug/jsonl-live',
@@ -342,29 +402,24 @@ export class Relay {
342
402
  detail: `limit=${limit ?? 'all'} title=${title?.slice(0, 32) ?? '-'}`,
343
403
  });
344
404
  try {
405
+ const full = await this.jsonlIndex.loadHistory(agentId, {
406
+ title,
407
+ composerIdByTitle: this.stateManager.getState().composerIdByTitle,
408
+ offset,
409
+ });
410
+ this.chatDisplay.setJsonlBaseline(agentId, full.messages);
411
+ const history = limit > 0 && full.messages.length > limit
412
+ ? { ...full, messages: full.messages.slice(-limit) }
413
+ : full;
345
414
  if (!isChatHistoryFromJsonl()) {
346
- const history = await this.jsonlIndex.loadHistory(agentId, {
347
- title,
348
- composerIdByTitle: this.stateManager.getState().composerIdByTitle,
349
- limit,
350
- offset,
351
- });
352
- this.chatDisplay.setJsonlBaseline(agentId, history.messages);
353
415
  res.json({
354
416
  agentId,
355
- ...this.agentMessagesSnapshot(agentId, history.totalMessages),
417
+ ...this.agentMessagesSnapshot(agentId, full.totalMessages),
356
418
  requestId,
357
419
  updatedAt: Date.now(),
358
420
  });
359
421
  return;
360
422
  }
361
- const history = await this.jsonlIndex.loadHistory(agentId, {
362
- title,
363
- composerIdByTitle: this.stateManager.getState().composerIdByTitle,
364
- limit,
365
- offset,
366
- });
367
- this.chatDisplay.setJsonlBaseline(agentId, history.messages);
368
423
  const snap = this.agentMessagesSnapshot(agentId, history.totalMessages);
369
424
  const body = { ...history, ...snap, requestId, updatedAt: Date.now() };
370
425
  const bytes = JSON.stringify(body).length;
@@ -528,6 +583,16 @@ export class Relay {
528
583
  this.io.on('connection', (socket) => this.onConnect(socket));
529
584
  }
530
585
  broadcast(event, payload) {
586
+ if (event === 'agent:messages') {
587
+ const p = payload;
588
+ bridgePipelineLog({
589
+ dir: 'out',
590
+ event: 'broadcast:agent:messages',
591
+ agentId: p?.agentId,
592
+ msgs: p?.historyMessages?.length ?? 0,
593
+ detail: `live=${p?.liveMessages?.length ?? 0} append=${!!p?.append} seq=${p?.seq ?? '-'} upstream=${this.upstream?.connected ?? false}`,
594
+ });
595
+ }
531
596
  if (event === 'agents:history' || event === 'agent:history') {
532
597
  const h = payload;
533
598
  const bytes = JSON.stringify(payload ?? {}).length;
@@ -565,7 +630,6 @@ export class Relay {
565
630
  append: append || undefined,
566
631
  });
567
632
  }
568
- /** DOM messages in state:patch — debug/UI chrome only; chat lenta is JSONL via agent:messages. */
569
633
  prepareStateMessages(raw) {
570
634
  return filterClientDisplayList(prepareChatMessagesForDisplay(raw));
571
635
  }
@@ -586,6 +650,9 @@ export class Relay {
586
650
  this.stateManager.on('state:patch', (patch) => {
587
651
  this.broadcast('state:patch', this.withDisplayState(patch));
588
652
  this.observeAgentCompletionForPush();
653
+ if (patch.messages?.length) {
654
+ this.scheduleDomOverlayEmit();
655
+ }
589
656
  if (patch.sidebarRepos || patch.composerIdByTitle) {
590
657
  const key = JSON.stringify([patch.sidebarRepos, patch.composerIdByTitle]);
591
658
  if (key !== this.lastSidebarIndexKey) {
@@ -605,7 +672,6 @@ export class Relay {
605
672
  this.jsonlIndex.on('agent:jsonl:updated', (payload) => {
606
673
  const total = payload.totalMessages ?? payload.messages.length;
607
674
  this.syncJsonlToSubscribedAgents(payload.agentId, payload.messages, total);
608
- this.agentCompletionPush?.onJsonlUpdated(payload.agentId, payload.messages, this.stateManager.getState());
609
675
  });
610
676
  this.jsonlIndex.on('agent:history', (history) => {
611
677
  const total = history.totalMessages ?? history.messages.length;
@@ -795,36 +861,28 @@ export class Relay {
795
861
  detail: `limit=${socketLimit} title=${title?.slice(0, 32) ?? '-'} upstream=${viaUpstream}`,
796
862
  });
797
863
  try {
864
+ const full = await this.jsonlIndex.loadHistory(agentId, {
865
+ title,
866
+ composerIdByTitle: this.stateManager.getState().composerIdByTitle,
867
+ offset,
868
+ });
869
+ this.emitJsonlLiveForAgent(agentId, full.messages, full.totalMessages);
870
+ const snap = this.agentMessagesSnapshot(agentId, full.totalMessages);
798
871
  if (!isChatHistoryFromJsonl()) {
799
- const history = await this.jsonlIndex.loadHistory(agentId, {
800
- title,
801
- composerIdByTitle: this.stateManager.getState().composerIdByTitle,
802
- limit: socketLimit,
803
- offset,
804
- });
805
- this.emitJsonlLiveForAgent(agentId, history.messages, history.totalMessages);
806
- const snap = this.agentMessagesSnapshot(agentId, history.totalMessages);
807
872
  const seq = this.historySeqByAgent.get(agentId) ?? this.nextHistorySeq(agentId);
808
873
  const ms = Date.now() - t0;
809
- console.log(`[relay] agents:history jsonl agentId=${agentId} hist=${snap.historyMessages.length} total=${history.totalMessages} ms=${ms} rid=${requestId ?? '-'}`);
874
+ console.log(`[relay] agents:history jsonl agentId=${agentId} hist=${snap.historyMessages.length} total=${full.totalMessages} ms=${ms} rid=${requestId ?? '-'}`);
810
875
  reply('agents:history', {
811
876
  agentId,
812
877
  ...snap,
813
- totalMessages: history.totalMessages,
878
+ totalMessages: full.totalMessages,
814
879
  requestId,
815
880
  updatedAt: Date.now(),
816
881
  seq,
817
882
  });
818
883
  return;
819
884
  }
820
- const history = await this.jsonlIndex.loadHistory(agentId, {
821
- title,
822
- composerIdByTitle: this.stateManager.getState().composerIdByTitle,
823
- limit: socketLimit,
824
- offset,
825
- });
826
- this.emitJsonlLiveForAgent(agentId, history.messages, history.totalMessages);
827
- const snap = this.agentMessagesSnapshot(agentId, history.totalMessages);
885
+ const history = full;
828
886
  const ms = Date.now() - t0;
829
887
  const bytes = JSON.stringify(snap.messages).length;
830
888
  console.log(`[relay] agents:history ok agentId=${history.agentId} msgs=${history.messages?.length ?? 0}/${history.totalMessages ?? '?'} bytes=${bytes} ms=${ms} rid=${requestId ?? '-'}`);
@@ -872,11 +930,12 @@ export class Relay {
872
930
  async runAgentsSubscribe({ agentId, title, focus, }) {
873
931
  if (!agentId)
874
932
  return;
875
- this.jsonlIndex.subscribe(agentId, title, { emitHistory: true });
933
+ const alreadySubscribed = this.jsonlIndex.getSubscribedAgents().has(agentId);
934
+ this.jsonlIndex.subscribe(agentId, title, { emitHistory: !alreadySubscribed });
876
935
  this.stateManager.patchNow({ lastError: undefined });
877
936
  await this.trySwitchWindowForAgent(agentId);
878
937
  if (focus === false) {
879
- void this.refreshDomChatOnSubscribe(agentId, title);
938
+ void this.refreshDomChatOnSubscribe(agentId, title, { clear: !alreadySubscribed });
880
939
  return;
881
940
  }
882
941
  const result = await this.commandExecutor.execute({
@@ -888,18 +947,22 @@ export class Relay {
888
947
  if (!result.ok) {
889
948
  console.warn('[relay] subscribe focus (non-fatal):', result.error);
890
949
  }
891
- void this.refreshDomChatOnSubscribe(agentId, title);
950
+ void this.refreshDomChatOnSubscribe(agentId, title, { clear: !alreadySubscribed });
892
951
  }
893
952
  /** Открытие чата: JSONL baseline + focus/scroll (DOM poll только для state: working/approve). */
894
- async refreshDomChatOnSubscribe(agentId, title) {
953
+ async refreshDomChatOnSubscribe(agentId, title, opts) {
895
954
  try {
896
- this.chatDisplay.clearAgent(agentId);
897
- this.lastEmittedJsonlRows.delete(agentId);
955
+ if (opts?.clear !== false) {
956
+ this.chatDisplay.clearAgent(agentId);
957
+ this.lastEmittedJsonlRows.delete(agentId);
958
+ this.lastEmittedLentaSig.delete(agentId);
959
+ this.lastEmittedHistLen.delete(agentId);
960
+ }
898
961
  const history = await this.jsonlIndex.loadHistory(agentId, {
899
962
  title,
900
963
  composerIdByTitle: this.stateManager.getState().composerIdByTitle,
901
- limit: AGENT_HISTORY_DEFAULT_LIMIT,
902
964
  });
965
+ this.lastEmittedLentaSig.delete(agentId);
903
966
  this.emitJsonlLiveForAgent(agentId, history.messages, history.totalMessages);
904
967
  await this.commandExecutor.scrollChatToBottom();
905
968
  this.domExtractor.pollNow();
@@ -913,6 +976,8 @@ export class Relay {
913
976
  this.jsonlIndex.unsubscribe(agentId);
914
977
  this.chatDisplay.clearAgent(agentId);
915
978
  this.lastEmittedJsonlRows.delete(agentId);
979
+ this.lastEmittedLentaSig.delete(agentId);
980
+ this.lastEmittedHistLen.delete(agentId);
916
981
  }
917
982
  }
918
983
  async runAgentsFocus({ agentId, title }, reply) {
@@ -33,6 +33,10 @@ export interface ServerConfig {
33
33
  relayUrl: string;
34
34
  relayToken: string;
35
35
  relayRoomId: string;
36
+ /** macOS: prevent system sleep while bridge runs (`KEEP_AWAKE=0` to disable). */
37
+ keepAwakeEnabled: boolean;
38
+ /** Periodic upstream ping when `relayUrl` set; `0` = off. */
39
+ relayKeepaliveMs: number;
36
40
  /** Client token from ~/.cursorconnect/identity.json (relay room pairing). */
37
41
  pairingClientToken?: string;
38
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursorconnect",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "CLI: Cursor Connect on Mac + relay pairing — install once, run from anywhere",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,7 +1,7 @@
1
1
  {
2
- "minCliVersion": "0.1.4",
2
+ "minCliVersion": "0.1.7",
3
3
  "minAppVersion": "0.2.2",
4
- "latestCliVersion": "0.1.7",
4
+ "latestCliVersion": "0.1.8",
5
5
  "latestAppVersion": "0.2.2",
6
6
  "updateCliCommand": "npm install -g cursorconnect@latest",
7
7
  "updateAppHint": "Обновите CursorConnect в App Store / TestFlight до последней сборки."