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.
- package/bridge-runtime/.env.example +5 -1
- package/bridge-runtime/connector-version.json +1 -1
- package/bridge-runtime/dist/agent-completion-push.d.ts +16 -23
- package/bridge-runtime/dist/agent-completion-push.js +89 -114
- package/bridge-runtime/dist/chat-display-store.d.ts +1 -1
- package/bridge-runtime/dist/chat-display-store.js +4 -1
- package/bridge-runtime/dist/config.js +2 -0
- package/bridge-runtime/dist/connector-client-version.js +1 -1
- package/bridge-runtime/dist/dom-transcript-store.d.ts +1 -1
- package/bridge-runtime/dist/dom-transcript-store.js +1 -1
- package/bridge-runtime/dist/index.js +9 -0
- package/bridge-runtime/dist/keep-awake.d.ts +5 -0
- package/bridge-runtime/dist/keep-awake.js +48 -0
- package/bridge-runtime/dist/relay-upstream.d.ts +3 -0
- package/bridge-runtime/dist/relay-upstream.js +21 -0
- package/bridge-runtime/dist/relay.d.ts +13 -3
- package/bridge-runtime/dist/relay.js +140 -75
- package/bridge-runtime/dist/types.d.ts +4 -0
- package/package.json +1 -1
- package/version-policy.json +2 -2
|
@@ -13,7 +13,7 @@ WEBAPP_PASSWORD=
|
|
|
13
13
|
# CURSOR_PROJECTS_DIR=
|
|
14
14
|
LOG_LEVEL=info
|
|
15
15
|
|
|
16
|
-
# Chat lenta: JSONL → agent:messages
|
|
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.
|
|
1
|
+
{"cliVersion":"0.1.8","bundledAt":"2026-05-26T06:15:47Z"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentsIndex, CursorState
|
|
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
|
|
12
|
-
*
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
private
|
|
26
|
-
|
|
27
|
-
private
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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.
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
89
|
+
return false;
|
|
86
90
|
}
|
|
87
91
|
/**
|
|
88
|
-
* Push
|
|
89
|
-
*
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
static MIN_EMIT_GAP_MS =
|
|
106
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
this.prevWorking = snapshotWorkingAgents(state, index);
|
|
115
|
+
if (isSuppressed()) {
|
|
116
|
+
this.syncPrevWorking(state);
|
|
118
117
|
return;
|
|
119
118
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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.
|
|
134
|
+
for (const id of this.workingPolls.keys()) {
|
|
135
|
+
if (!now.has(id))
|
|
136
|
+
this.workingPolls.delete(id);
|
|
137
|
+
}
|
|
130
138
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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.
|
|
160
|
-
|
|
161
|
-
|
|
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.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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,
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
196
|
-
if (
|
|
174
|
+
const now = snapshotWorkingAgents(state, this.agentsIndex());
|
|
175
|
+
if (now.has(agentId))
|
|
197
176
|
return;
|
|
198
|
-
const
|
|
199
|
-
|
|
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.
|
|
215
|
-
this.
|
|
216
|
-
this.
|
|
217
|
-
this.
|
|
218
|
-
console.log(`[agent-completion-push] → relay
|
|
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
|
|
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
|
|
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 type { ChatMessage } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Append-only per-agent transcript built from DOM snapshots.
|
|
4
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
|
87
|
-
for (const payload of
|
|
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 ${
|
|
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
|
-
/**
|
|
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
|
|
169
|
-
const
|
|
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 (!
|
|
177
|
-
this.emitAgentMessages(agentId, historyMessages, [], 'jsonl', totalMessages, this.nextHistorySeq(agentId), false);
|
|
210
|
+
if (!this.jsonlIndex.getSubscribedAgents().has(agentId))
|
|
178
211
|
return;
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
if (
|
|
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
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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,
|
|
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=${
|
|
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:
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
897
|
-
|
|
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
package/version-policy.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"minCliVersion": "0.1.
|
|
2
|
+
"minCliVersion": "0.1.7",
|
|
3
3
|
"minAppVersion": "0.2.2",
|
|
4
|
-
"latestCliVersion": "0.1.
|
|
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 до последней сборки."
|