cursorconnect 0.1.7 → 0.1.9
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 +7 -1
- package/bridge-runtime/connector-version.json +1 -1
- package/bridge-runtime/dist/agent-completion-push.d.ts +27 -22
- package/bridge-runtime/dist/agent-completion-push.js +242 -122
- package/bridge-runtime/dist/agent-completion-readiness.d.ts +19 -0
- package/bridge-runtime/dist/agent-completion-readiness.js +42 -0
- package/bridge-runtime/dist/chat-display-store.d.ts +32 -7
- package/bridge-runtime/dist/chat-display-store.js +99 -21
- package/bridge-runtime/dist/chat-display.d.ts +36 -0
- package/bridge-runtime/dist/chat-display.js +287 -24
- package/bridge-runtime/dist/chat-sync.d.ts +3 -1
- package/bridge-runtime/dist/chat-sync.js +20 -0
- package/bridge-runtime/dist/config.js +2 -0
- package/bridge-runtime/dist/connector-client-version.js +1 -1
- package/bridge-runtime/dist/debug-chats-page.d.ts +1 -1
- package/bridge-runtime/dist/debug-chats-page.js +148 -26
- package/bridge-runtime/dist/dom-transcript-store.d.ts +3 -1
- package/bridge-runtime/dist/dom-transcript-store.js +18 -3
- package/bridge-runtime/dist/extract-page.js +5 -4
- 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/lenta-capture.d.ts +46 -0
- package/bridge-runtime/dist/lenta-capture.js +146 -0
- package/bridge-runtime/dist/lenta-debug.d.ts +42 -0
- package/bridge-runtime/dist/lenta-debug.js +221 -0
- package/bridge-runtime/dist/lenta-delivery.d.ts +3 -0
- package/bridge-runtime/dist/lenta-delivery.js +10 -0
- package/bridge-runtime/dist/lenta-seq-journal.d.ts +48 -0
- package/bridge-runtime/dist/lenta-seq-journal.js +109 -0
- package/bridge-runtime/dist/message-filter.d.ts +5 -0
- package/bridge-runtime/dist/message-filter.js +4 -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 +47 -3
- package/bridge-runtime/dist/relay.js +667 -96
- package/bridge-runtime/dist/types.d.ts +13 -4
- package/dist/bridge-build.js +50 -0
- package/dist/index.js +9 -6
- package/dist/launch.js +5 -1
- package/dist/run-service.js +10 -4
- package/dist/startup-check.js +6 -0
- package/package.json +1 -1
- package/version-policy.json +2 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mkdirSync } from 'fs';
|
|
1
2
|
import express from 'express';
|
|
2
3
|
import { createServer } from 'http';
|
|
3
4
|
import { randomBytes, timingSafeEqual } from 'crypto';
|
|
@@ -6,9 +7,9 @@ import { readAllowedMediaFile, resolveMediaPathParam } from './media-path.js';
|
|
|
6
7
|
import { Server as SocketServer } from 'socket.io';
|
|
7
8
|
import { resolveJsonlFilePath } from './agent-title-match.js';
|
|
8
9
|
import { ChatDisplayStore } from './chat-display-store.js';
|
|
9
|
-
import { AGENT_HISTORY_DEFAULT_LIMIT } from './history-limit.js';
|
|
10
10
|
import { resolveHistoryLimit } from './history-request.js';
|
|
11
|
-
import { filterClientDisplayList, prepareChatMessagesForDisplay, } from './chat-display.js';
|
|
11
|
+
import { assertMonotonicLenta, filterClientDisplayList, prepareChatMessagesForDisplay, } from './chat-display.js';
|
|
12
|
+
import { isKeepAwakeActive } from './keep-awake.js';
|
|
12
13
|
function sleepMs(ms) {
|
|
13
14
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
15
|
}
|
|
@@ -20,7 +21,12 @@ import { RelayUpstream } from './relay-upstream.js';
|
|
|
20
21
|
import { bridgePipelineLog, bridgePipelineReportLines, bridgePipelineSnapshot, } from './history-pipeline-log.js';
|
|
21
22
|
import { DEBUG_CHATS_PAGE_HTML } from './debug-chats-page.js';
|
|
22
23
|
import { isChatHistoryFromJsonl } from './chat-history-mode.js';
|
|
24
|
+
import { isAgentGenerating, isChatSyncedWithCursor, isComposerUuid, resolveCursorActiveComposerId, } from './chat-sync.js';
|
|
23
25
|
import { readJsonlLiveSnapshot } from './jsonl-live-debug.js';
|
|
26
|
+
import { checkMonotonicOrder, checkOverlayAfterArchive, checkOverlayDuplicatesArchive, checkPayloadCompose, checkStoreCompose, checkSyncGate, compareDomTailToLenta, previewRow, } from './lenta-debug.js';
|
|
27
|
+
import { getLastLentaEmit, getLentaSeqJournal, getLentaSeqLogPath, messagesFingerprint, recordLentaSeqEmit, } from './lenta-seq-journal.js';
|
|
28
|
+
import { isLentaDeliveryPending } from './lenta-delivery.js';
|
|
29
|
+
import { captureLentaIfEnabled, captureSessionDir, listCaptureSteps, readCaptureConfig, writeCaptureConfig, clearCaptureConfig, } from './lenta-capture.js';
|
|
24
30
|
export class Relay {
|
|
25
31
|
stateManager;
|
|
26
32
|
commandExecutor;
|
|
@@ -44,6 +50,16 @@ export class Relay {
|
|
|
44
50
|
chatDisplay = new ChatDisplayStore();
|
|
45
51
|
/** Raw JSONL row count last sent per agent (live `append` emits). */
|
|
46
52
|
lastEmittedJsonlRows = new Map();
|
|
53
|
+
/** Last display tail pushed to app — re-emit when user/assistant tail changes. */
|
|
54
|
+
lastEmittedLentaSig = new Map();
|
|
55
|
+
/** Display bubble count last sent — block regression 214→74 style wipes on phone. */
|
|
56
|
+
lastEmittedHistLen = new Map();
|
|
57
|
+
lentaPendingSince = new Map();
|
|
58
|
+
static LENTA_PENDING_FORCE_MS = 12_000;
|
|
59
|
+
/** Socket `agent:messages` coalesce — store ingest is immediate (see ingestDomOverlayFromState). */
|
|
60
|
+
static DOM_OVERLAY_EMIT_MS = parseInt(process.env.DOM_OVERLAY_EMIT_MS ?? '50', 10);
|
|
61
|
+
domOverlayTimer = null;
|
|
62
|
+
cachedLentaPendingByAgent = {};
|
|
47
63
|
constructor(config, stateManager, commandExecutor, cdpBridge, jsonlIndex, messageDebugStore, domExtractor) {
|
|
48
64
|
this.stateManager = stateManager;
|
|
49
65
|
this.commandExecutor = commandExecutor;
|
|
@@ -67,7 +83,15 @@ export class Relay {
|
|
|
67
83
|
this.upstream = new RelayUpstream(this.config, (event, ...args) => {
|
|
68
84
|
this.handleRemoteClientEvent(event, ...args);
|
|
69
85
|
}, () => this.flushPendingPushPayloads());
|
|
70
|
-
this.agentCompletionPush = new AgentCompletionPush((payload) => this.emitAgentCompletedPush(payload), () => this.lastJsonlIndex, () => this.stateManager.getState()
|
|
86
|
+
this.agentCompletionPush = new AgentCompletionPush((payload) => this.emitAgentCompletedPush(payload), () => this.lastJsonlIndex, () => this.stateManager.getState(), {
|
|
87
|
+
readiness: {
|
|
88
|
+
isSubscribed: (agentId) => this.jsonlIndex.getSubscribedAgents().has(agentId),
|
|
89
|
+
getSubscribeTitle: (agentId) => this.jsonlIndex.getSubscribedAgents().get(agentId)?.title,
|
|
90
|
+
getDisplayMessages: (agentId) => this.chatDisplay.getDisplayMessages(agentId),
|
|
91
|
+
getJsonlHistory: (agentId) => this.chatDisplay.getJsonlHistory(agentId),
|
|
92
|
+
},
|
|
93
|
+
requestContentSync: (agentId, state) => this.requestAgentCompletionContentSync(agentId, state),
|
|
94
|
+
});
|
|
71
95
|
this.upstream.connect();
|
|
72
96
|
}
|
|
73
97
|
}
|
|
@@ -75,6 +99,7 @@ export class Relay {
|
|
|
75
99
|
emitAgentCompletedPush(payload) {
|
|
76
100
|
if (this.upstream?.connected) {
|
|
77
101
|
this.upstream.emit('push:agent-completed', payload);
|
|
102
|
+
console.log(`[agent-completion-push] upstream emit agentId=${payload.agentId.slice(0, 12)} title=${payload.chatTitle.slice(0, 40)}`);
|
|
78
103
|
return;
|
|
79
104
|
}
|
|
80
105
|
this.pendingPushPayloads.push(payload);
|
|
@@ -83,15 +108,22 @@ export class Relay {
|
|
|
83
108
|
flushPendingPushPayloads() {
|
|
84
109
|
if (!this.upstream?.connected || !this.pendingPushPayloads.length)
|
|
85
110
|
return;
|
|
86
|
-
const
|
|
87
|
-
for (const payload of
|
|
111
|
+
const byAgent = new Map();
|
|
112
|
+
for (const payload of this.pendingPushPayloads.splice(0)) {
|
|
113
|
+
byAgent.set(payload.agentId, payload);
|
|
114
|
+
}
|
|
115
|
+
for (const payload of byAgent.values()) {
|
|
88
116
|
this.upstream.emit('push:agent-completed', payload);
|
|
89
117
|
}
|
|
90
|
-
console.log(`[agent-completion-push] flushed ${
|
|
118
|
+
console.log(`[agent-completion-push] flushed ${byAgent.size} queued push(es) to relay`);
|
|
91
119
|
}
|
|
92
120
|
observeAgentCompletionForPush() {
|
|
93
121
|
this.agentCompletionPush?.observe(this.stateManager.getState());
|
|
94
122
|
}
|
|
123
|
+
requestAgentCompletionContentSync(agentId, state) {
|
|
124
|
+
void this.ensureJsonlBaselineForAgent(agentId, state);
|
|
125
|
+
this.flushLentaDelivery(agentId, 'push_ready');
|
|
126
|
+
}
|
|
95
127
|
listen() {
|
|
96
128
|
return new Promise((resolve) => {
|
|
97
129
|
this.httpServer.listen(this.config.serverPort, this.config.serverHost, () => {
|
|
@@ -103,23 +135,33 @@ export class Relay {
|
|
|
103
135
|
get authEnabled() {
|
|
104
136
|
return this.config.webappPassword.length > 0;
|
|
105
137
|
}
|
|
138
|
+
lentaAgentIds(state) {
|
|
139
|
+
const subscribed = [...this.jsonlIndex.getSubscribedAgents().keys()];
|
|
140
|
+
return new Set([
|
|
141
|
+
...subscribed,
|
|
142
|
+
...(state.activeComposerId ? [state.activeComposerId] : []),
|
|
143
|
+
]);
|
|
144
|
+
}
|
|
106
145
|
/** Read-only view of in-memory bridge state (no writes to stores). */
|
|
107
146
|
readOnlyChatSnapshot() {
|
|
108
147
|
const state = this.stateManager.getState();
|
|
109
148
|
const subscribed = [...this.jsonlIndex.getSubscribedAgents().entries()].map(([agentId, meta]) => ({ agentId, title: meta.title }));
|
|
110
149
|
const displayCache = {};
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
150
|
+
const domOverlay = {};
|
|
151
|
+
const displayMessages = {};
|
|
152
|
+
const domRaw = {};
|
|
153
|
+
const lentaByAgent = {};
|
|
154
|
+
for (const agentId of this.lentaAgentIds(state)) {
|
|
155
|
+
const lenta = this.chatDisplay.getLentaDebug(agentId);
|
|
156
|
+
lentaByAgent[agentId] = lenta;
|
|
157
|
+
if (lenta.jsonlHistory.length)
|
|
158
|
+
displayCache[agentId] = lenta.jsonlHistory;
|
|
159
|
+
if (lenta.domOverlay.length)
|
|
160
|
+
domOverlay[agentId] = lenta.domOverlay;
|
|
161
|
+
if (lenta.messages.length)
|
|
162
|
+
displayMessages[agentId] = lenta.messages;
|
|
163
|
+
if (lenta.domRaw.length)
|
|
164
|
+
domRaw[agentId] = lenta.domRaw;
|
|
123
165
|
}
|
|
124
166
|
return {
|
|
125
167
|
at: Date.now(),
|
|
@@ -129,6 +171,7 @@ export class Relay {
|
|
|
129
171
|
activeChatTitle: state.activeChatTitle,
|
|
130
172
|
updatedAt: state.updatedAt,
|
|
131
173
|
lastError: state.lastError,
|
|
174
|
+
agentWorking: state.agentWorking,
|
|
132
175
|
tabs: state.tabs,
|
|
133
176
|
composerIdByTitle: state.composerIdByTitle,
|
|
134
177
|
domMessageCount: state.messages.length,
|
|
@@ -139,9 +182,83 @@ export class Relay {
|
|
|
139
182
|
ringSize: this.messageDebugStore.list().length,
|
|
140
183
|
},
|
|
141
184
|
subscribed,
|
|
185
|
+
lentaByAgent,
|
|
142
186
|
displayCache,
|
|
187
|
+
domOverlay,
|
|
188
|
+
displayMessages,
|
|
189
|
+
domRaw,
|
|
190
|
+
/** @deprecated use `domOverlay` */
|
|
191
|
+
domTranscript: domOverlay,
|
|
143
192
|
chatHistoryJsonl: isChatHistoryFromJsonl(),
|
|
144
|
-
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async buildLentaDebugReport(agentId, title, opts) {
|
|
196
|
+
const state = this.stateManager.getState();
|
|
197
|
+
const subMeta = this.jsonlIndex.getSubscribedAgents().get(agentId);
|
|
198
|
+
const subTitle = title ?? subMeta?.title;
|
|
199
|
+
const synced = isChatSyncedWithCursor(agentId, subTitle, state);
|
|
200
|
+
if (opts?.reloadJsonl) {
|
|
201
|
+
const history = await this.jsonlIndex.loadHistory(agentId, {
|
|
202
|
+
title: subTitle,
|
|
203
|
+
composerIdByTitle: state.composerIdByTitle,
|
|
204
|
+
});
|
|
205
|
+
this.chatDisplay.setJsonlBaseline(agentId, history.messages);
|
|
206
|
+
}
|
|
207
|
+
this.syncGeneratingFlags(state);
|
|
208
|
+
if (state.messages?.length)
|
|
209
|
+
this.ingestDomOverlayFromState(state);
|
|
210
|
+
const lenta = this.chatDisplay.getLentaDebug(agentId);
|
|
211
|
+
const snap = this.agentMessagesSnapshot(agentId);
|
|
212
|
+
const issues = [
|
|
213
|
+
...checkStoreCompose(lenta.jsonlHistory, lenta.domOverlay, lenta.messages),
|
|
214
|
+
...checkPayloadCompose(snap.messages, snap.historyMessages, snap.liveMessages),
|
|
215
|
+
...checkMonotonicOrder(lenta.messages, 'store'),
|
|
216
|
+
...checkOverlayAfterArchive(lenta.jsonlHistory, lenta.domOverlay),
|
|
217
|
+
...checkOverlayDuplicatesArchive(lenta.jsonlHistory, lenta.domOverlay),
|
|
218
|
+
...checkSyncGate(synced, lenta.domOverlay, lenta.domRaw.length),
|
|
219
|
+
];
|
|
220
|
+
const activeId = state.activeComposerId;
|
|
221
|
+
const domViewport = activeId && activeId !== agentId
|
|
222
|
+
? []
|
|
223
|
+
: state.messages.map((m) => previewRow(m, 'dom-viewport'));
|
|
224
|
+
const domNote = activeId && activeId !== agentId
|
|
225
|
+
? `DOM viewport is active ${activeId.slice(0, 8)}, not ${agentId.slice(0, 8)}`
|
|
226
|
+
: null;
|
|
227
|
+
const domCmp = synced
|
|
228
|
+
? compareDomTailToLenta(domViewport, lenta.messages.map((m) => previewRow(m, 'lenta')), 12, {
|
|
229
|
+
hasDomOverlay: lenta.domOverlay.length > 0,
|
|
230
|
+
hasJsonlArchive: lenta.jsonlHistory.length > 0,
|
|
231
|
+
agentWorking: state.agentWorking,
|
|
232
|
+
})
|
|
233
|
+
: { issues: [], domTail: [], lentaTail: [] };
|
|
234
|
+
issues.push(...domCmp.issues);
|
|
235
|
+
return {
|
|
236
|
+
at: new Date().toISOString(),
|
|
237
|
+
agentId,
|
|
238
|
+
synced,
|
|
239
|
+
domNote,
|
|
240
|
+
health: { cdp: this.cdpBridge.getClient()?.isConnected() ?? false },
|
|
241
|
+
activeComposerId: activeId,
|
|
242
|
+
activeChatTitle: state.activeChatTitle,
|
|
243
|
+
agentWorking: state.agentWorking,
|
|
244
|
+
agentGenerating: lenta.agentGenerating,
|
|
245
|
+
jsonlRowCount: lenta.jsonlRowCount,
|
|
246
|
+
counts: {
|
|
247
|
+
jsonl: lenta.jsonlHistory.length,
|
|
248
|
+
domOverlay: lenta.domOverlay.length,
|
|
249
|
+
domRaw: lenta.domRaw.length,
|
|
250
|
+
messages: lenta.messages.length,
|
|
251
|
+
domViewport: domViewport.length,
|
|
252
|
+
},
|
|
253
|
+
lenta,
|
|
254
|
+
emitSnapshot: snap,
|
|
255
|
+
issues,
|
|
256
|
+
match: issues.length === 0,
|
|
257
|
+
domTail: domCmp.domTail,
|
|
258
|
+
lentaTail: domCmp.lentaTail,
|
|
259
|
+
seqJournal: getLentaSeqJournal(agentId).slice(-24),
|
|
260
|
+
seqLogFile: getLentaSeqLogPath(),
|
|
261
|
+
currentSeq: this.historySeqByAgent.get(agentId) ?? 0,
|
|
145
262
|
};
|
|
146
263
|
}
|
|
147
264
|
/** Push JSONL file updates to every subscribed route id (e.g. sidebar-0) for that composer. */
|
|
@@ -163,47 +280,262 @@ export class Relay {
|
|
|
163
280
|
this.emitJsonlLiveForAgent(agentId, messages, totalMessages);
|
|
164
281
|
}
|
|
165
282
|
}
|
|
166
|
-
/**
|
|
283
|
+
/** JSONL baseline + DOM tail (`liveMessages`); `messages` = append-only compose. */
|
|
284
|
+
lentaSnapshot(agentId, totalMessages) {
|
|
285
|
+
const historyMessages = this.chatDisplay.getJsonlHistory(agentId);
|
|
286
|
+
const liveMessages = this.chatDisplay.getDomLive(agentId);
|
|
287
|
+
const messages = this.chatDisplay.getDisplayMessages(agentId);
|
|
288
|
+
assertMonotonicLenta(messages, agentId.slice(0, 8));
|
|
289
|
+
const source = liveMessages.length ? 'hybrid' : 'jsonl';
|
|
290
|
+
return {
|
|
291
|
+
historyMessages,
|
|
292
|
+
liveMessages,
|
|
293
|
+
messages,
|
|
294
|
+
totalMessages: totalMessages ?? messages.length,
|
|
295
|
+
source: source,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
emitLentaForAgent(agentId, opts) {
|
|
299
|
+
const snap = this.lentaSnapshot(agentId, opts.totalMessages);
|
|
300
|
+
const historyOut = opts.historyPayload ?? snap.historyMessages;
|
|
301
|
+
if (!historyOut.length && !snap.liveMessages.length)
|
|
302
|
+
return;
|
|
303
|
+
this.emitAgentMessages(agentId, historyOut, snap.liveMessages, snap.source, {
|
|
304
|
+
totalMessages: opts.totalMessages,
|
|
305
|
+
append: opts.append,
|
|
306
|
+
reason: opts.reason,
|
|
307
|
+
forceSeq: opts.forceSeq,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
lentaTailSignature(messages) {
|
|
311
|
+
return messages
|
|
312
|
+
.slice(-4)
|
|
313
|
+
.map((m) => `${m.role}:${m.id ?? ''}:${(m.text ?? '').length}:${(m.text ?? '').slice(-48)}`)
|
|
314
|
+
.join('|');
|
|
315
|
+
}
|
|
316
|
+
/** Live JSONL → `agent:messages` for subscribed chats (phone lenta). */
|
|
167
317
|
emitJsonlLiveForAgent(agentId, rows, totalMessages) {
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
const
|
|
318
|
+
const prevTotal = this.lastEmittedJsonlRows.get(agentId) ?? 0;
|
|
319
|
+
const prevSig = this.lastEmittedLentaSig.get(agentId);
|
|
320
|
+
const state = this.stateManager.getState();
|
|
321
|
+
this.chatDisplay.setAgentGenerating(agentId, isAgentGenerating(agentId, state));
|
|
171
322
|
this.chatDisplay.setJsonlBaseline(agentId, rows);
|
|
172
323
|
this.lastEmittedJsonlRows.set(agentId, totalMessages);
|
|
324
|
+
this.writeLentaCapture(agentId, 'jsonl_file');
|
|
173
325
|
const historyMessages = this.chatDisplay.getJsonlHistory(agentId);
|
|
174
|
-
|
|
326
|
+
const liveN = this.chatDisplay.getDomLive(agentId).length;
|
|
327
|
+
if (!historyMessages.length && !liveN)
|
|
175
328
|
return;
|
|
176
|
-
if (!
|
|
177
|
-
|
|
329
|
+
if (!this.jsonlIndex.getSubscribedAgents().has(agentId))
|
|
330
|
+
return;
|
|
331
|
+
const storeMessages = this.chatDisplay.getDisplayMessages(agentId);
|
|
332
|
+
const deliveryPending = isLentaDeliveryPending(agentId, storeMessages);
|
|
333
|
+
const sig = this.lentaTailSignature(historyMessages);
|
|
334
|
+
const totalGrew = totalMessages > prevTotal;
|
|
335
|
+
const generating = isAgentGenerating(agentId, state);
|
|
336
|
+
if (!deliveryPending && !generating && !totalGrew && sig === prevSig && prevSig)
|
|
337
|
+
return;
|
|
338
|
+
const histLen = historyMessages.length;
|
|
339
|
+
const prevHistLen = this.lastEmittedHistLen.get(agentId) ?? 0;
|
|
340
|
+
if (prevHistLen > 24 && histLen < prevHistLen - 12) {
|
|
341
|
+
bridgePipelineLog({
|
|
342
|
+
dir: 'internal',
|
|
343
|
+
event: 'agent:messages:SKIP_REGRESSION',
|
|
344
|
+
agentId,
|
|
345
|
+
msgs: histLen,
|
|
346
|
+
detail: `prev=${prevHistLen} total=${totalMessages}`,
|
|
347
|
+
});
|
|
178
348
|
return;
|
|
179
349
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
350
|
+
this.emitLentaForAgent(agentId, {
|
|
351
|
+
totalMessages,
|
|
352
|
+
reason: 'jsonl_live',
|
|
353
|
+
forceSeq: deliveryPending || generating,
|
|
354
|
+
});
|
|
355
|
+
this.lastEmittedLentaSig.set(agentId, sig);
|
|
356
|
+
this.lastEmittedHistLen.set(agentId, Math.max(prevHistLen, histLen));
|
|
357
|
+
if (isLentaDeliveryPending(agentId, this.chatDisplay.getDisplayMessages(agentId))) {
|
|
358
|
+
this.flushLentaDelivery(agentId, 'delivery_flush');
|
|
359
|
+
}
|
|
360
|
+
this.agentCompletionPush?.retryPendingContent(state);
|
|
361
|
+
}
|
|
362
|
+
/** Ingest CDP viewport into ChatDisplayStore — no debounce (debug + compose). */
|
|
363
|
+
ingestDomOverlayFromState(state) {
|
|
364
|
+
if (!state.messages?.length)
|
|
183
365
|
return;
|
|
366
|
+
const domRows = this.prepareStateMessages(state.messages);
|
|
367
|
+
const targets = this.resolveDomOverlayAgentIds(state);
|
|
368
|
+
if (!targets.size)
|
|
369
|
+
return;
|
|
370
|
+
for (const agentId of targets) {
|
|
371
|
+
this.chatDisplay.setAgentGenerating(agentId, isAgentGenerating(agentId, state));
|
|
372
|
+
if (!this.chatDisplay.getJsonlHistory(agentId).length) {
|
|
373
|
+
void this.ensureJsonlBaselineForAgent(agentId, state);
|
|
374
|
+
}
|
|
375
|
+
const prevLive = this.chatDisplay.getDomLive(agentId).length;
|
|
376
|
+
this.chatDisplay.mergeLiveForAgent(agentId, domRows);
|
|
377
|
+
const liveN = this.chatDisplay.getDomLive(agentId).length;
|
|
378
|
+
if (liveN > 0 && liveN !== prevLive) {
|
|
379
|
+
bridgePipelineLog({
|
|
380
|
+
dir: 'internal',
|
|
381
|
+
event: 'dom:overlay:ingest',
|
|
382
|
+
agentId,
|
|
383
|
+
msgs: liveN,
|
|
384
|
+
detail: `domRows=${domRows.length} generating=${isAgentGenerating(agentId, state)} viewport=${domRows.length}`,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
this.writeLentaCapture(agentId, 'dom_ingest');
|
|
184
388
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
389
|
+
}
|
|
390
|
+
/** Push lenta with DOM live to clients while generation is in flight. */
|
|
391
|
+
emitDomLiveWhileGenerating(state) {
|
|
392
|
+
for (const agentId of this.resolveDomOverlayAgentIds(state)) {
|
|
393
|
+
if (!isAgentGenerating(agentId, state))
|
|
394
|
+
continue;
|
|
395
|
+
if (!this.jsonlIndex.getSubscribedAgents().has(agentId))
|
|
396
|
+
continue;
|
|
397
|
+
const meta = this.jsonlIndex.getSubscribedAgents().get(agentId);
|
|
398
|
+
if (!meta || !isChatSyncedWithCursor(agentId, meta.title, state))
|
|
399
|
+
continue;
|
|
400
|
+
const snap = this.lentaSnapshot(agentId, this.lastEmittedJsonlRows.get(agentId) ?? undefined);
|
|
401
|
+
if (!snap.liveMessages.length)
|
|
402
|
+
continue;
|
|
403
|
+
this.lastEmittedLentaSig.delete(agentId);
|
|
404
|
+
this.emitLentaForAgent(agentId, {
|
|
405
|
+
totalMessages: snap.totalMessages,
|
|
406
|
+
reason: 'dom_overlay',
|
|
407
|
+
forceSeq: true,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
syncGeneratingFlags(state) {
|
|
412
|
+
const targets = this.resolveDomOverlayAgentIds(state);
|
|
413
|
+
for (const agentId of targets) {
|
|
414
|
+
this.chatDisplay.setAgentGenerating(agentId, isAgentGenerating(agentId, state));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/** Push composed lenta to subscribed clients (debounced). */
|
|
418
|
+
emitDomOverlaySocketForSubscribers(state) {
|
|
419
|
+
for (const [subId, meta] of this.jsonlIndex.getSubscribedAgents()) {
|
|
420
|
+
if (!isChatSyncedWithCursor(subId, meta.title, state))
|
|
421
|
+
continue;
|
|
422
|
+
const snap = this.lentaSnapshot(subId, this.lastEmittedJsonlRows.get(subId) ?? undefined);
|
|
423
|
+
const deliveryPending = isLentaDeliveryPending(subId, snap.messages);
|
|
424
|
+
if (!snap.liveMessages.length && !state.agentWorking && !deliveryPending)
|
|
425
|
+
continue;
|
|
426
|
+
this.lastEmittedLentaSig.delete(subId);
|
|
427
|
+
this.emitLentaForAgent(subId, {
|
|
428
|
+
totalMessages: snap.totalMessages,
|
|
429
|
+
reason: 'dom_overlay',
|
|
430
|
+
forceSeq: deliveryPending,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/** While generating, push overlay growth without waiting for emit debounce tail. */
|
|
435
|
+
maybeEmitDomOverlayLeading(state) {
|
|
436
|
+
if (!state.agentWorking)
|
|
191
437
|
return;
|
|
438
|
+
for (const [subId, meta] of this.jsonlIndex.getSubscribedAgents()) {
|
|
439
|
+
if (!isChatSyncedWithCursor(subId, meta.title, state))
|
|
440
|
+
continue;
|
|
441
|
+
if (!this.chatDisplay.getDomLive(subId).length)
|
|
442
|
+
continue;
|
|
443
|
+
const snap = this.lentaSnapshot(subId, this.lastEmittedJsonlRows.get(subId) ?? undefined);
|
|
444
|
+
const sig = messagesFingerprint(snap.messages);
|
|
445
|
+
const last = getLastLentaEmit(subId);
|
|
446
|
+
if (last?.messagesSig === sig)
|
|
447
|
+
continue;
|
|
448
|
+
this.emitLentaForAgent(subId, {
|
|
449
|
+
totalMessages: snap.totalMessages,
|
|
450
|
+
reason: 'dom_overlay',
|
|
451
|
+
});
|
|
192
452
|
}
|
|
193
|
-
|
|
194
|
-
|
|
453
|
+
}
|
|
454
|
+
applyDomOverlayFromState(state) {
|
|
455
|
+
this.syncGeneratingFlags(state);
|
|
456
|
+
this.ingestDomOverlayFromState(state);
|
|
457
|
+
this.emitDomLiveWhileGenerating(state);
|
|
458
|
+
this.maybeEmitDomOverlayLeading(state);
|
|
459
|
+
this.scheduleDomOverlayEmit();
|
|
460
|
+
}
|
|
461
|
+
scheduleDomOverlayEmit() {
|
|
462
|
+
if (this.domOverlayTimer)
|
|
463
|
+
clearTimeout(this.domOverlayTimer);
|
|
464
|
+
this.domOverlayTimer = setTimeout(() => {
|
|
465
|
+
this.domOverlayTimer = null;
|
|
466
|
+
this.emitDomOverlaySocketForSubscribers(this.stateManager.getState());
|
|
467
|
+
}, Relay.DOM_OVERLAY_EMIT_MS);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Agent ids that should receive DOM ingest: active Cursor composer + synced subscriptions.
|
|
471
|
+
* Store must update even without phone subscribe (debug/lenta, later subscribe).
|
|
472
|
+
*/
|
|
473
|
+
resolveDomOverlayAgentIds(state) {
|
|
474
|
+
const targets = new Set();
|
|
475
|
+
const activeTab = state.tabs.find((t) => t.active);
|
|
476
|
+
const activeId = resolveCursorActiveComposerId(state);
|
|
477
|
+
if (isComposerUuid(activeId))
|
|
478
|
+
targets.add(activeId);
|
|
479
|
+
for (const [subId, meta] of this.jsonlIndex.getSubscribedAgents()) {
|
|
480
|
+
if (!isChatSyncedWithCursor(subId, meta.title, state))
|
|
481
|
+
continue;
|
|
482
|
+
targets.add(subId);
|
|
483
|
+
if (isComposerUuid(subId))
|
|
484
|
+
continue;
|
|
485
|
+
const path = resolveJsonlFilePath(this.config.cursorProjectsDir, subId, {
|
|
486
|
+
title: meta.title,
|
|
487
|
+
composerIdByTitle: state.composerIdByTitle,
|
|
488
|
+
activeComposerId: state.activeComposerId,
|
|
489
|
+
activeTabTitle: activeTab?.title,
|
|
490
|
+
});
|
|
491
|
+
if (path)
|
|
492
|
+
targets.add(basename(path, '.jsonl'));
|
|
195
493
|
}
|
|
494
|
+
return targets;
|
|
196
495
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
496
|
+
writeLentaCapture(agentId, reason, extra) {
|
|
497
|
+
const state = this.stateManager.getState();
|
|
498
|
+
const lenta = this.chatDisplay.getLentaDebug(agentId);
|
|
499
|
+
const activeTab = state.tabs.find((t) => t.active);
|
|
500
|
+
const jsonlPath = resolveJsonlFilePath(this.config.cursorProjectsDir, agentId, {
|
|
501
|
+
title: this.jsonlIndex.getSubscribedAgents().get(agentId)?.title,
|
|
502
|
+
composerIdByTitle: state.composerIdByTitle,
|
|
503
|
+
activeComposerId: state.activeComposerId,
|
|
504
|
+
activeTabTitle: activeTab?.title,
|
|
505
|
+
});
|
|
506
|
+
const domViewport = resolveCursorActiveComposerId(state) === agentId
|
|
507
|
+
? this.prepareStateMessages(state.messages)
|
|
508
|
+
: [];
|
|
509
|
+
captureLentaIfEnabled(agentId, {
|
|
510
|
+
reason,
|
|
511
|
+
source: lenta.source,
|
|
512
|
+
emitSeq: extra?.emitSeq,
|
|
513
|
+
seqHeld: extra?.seqHeld,
|
|
514
|
+
transition: extra?.transition,
|
|
515
|
+
jsonlRowCount: lenta.jsonlRowCount,
|
|
516
|
+
historyMessages: lenta.jsonlHistory,
|
|
517
|
+
liveMessages: lenta.domOverlay,
|
|
518
|
+
messages: lenta.messages,
|
|
519
|
+
domRaw: lenta.domRaw,
|
|
520
|
+
domViewport,
|
|
521
|
+
jsonlFilePath: jsonlPath ?? undefined,
|
|
522
|
+
activeComposerId: state.activeComposerId,
|
|
523
|
+
agentWorking: state.agentWorking,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
resolveHistoryOpts(agentId) {
|
|
527
|
+
const state = this.stateManager.getState();
|
|
528
|
+
const activeTab = state.tabs.find((t) => t.active);
|
|
200
529
|
return {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
530
|
+
title: this.jsonlIndex.getSubscribedAgents().get(agentId)?.title,
|
|
531
|
+
composerIdByTitle: state.composerIdByTitle,
|
|
532
|
+
activeComposerId: state.activeComposerId,
|
|
533
|
+
activeTabTitle: activeTab?.title,
|
|
205
534
|
};
|
|
206
535
|
}
|
|
536
|
+
agentMessagesSnapshot(agentId, totalMessages) {
|
|
537
|
+
return this.lentaSnapshot(agentId, totalMessages);
|
|
538
|
+
}
|
|
207
539
|
checkMediaAuth(req) {
|
|
208
540
|
if (!this.authEnabled)
|
|
209
541
|
return true;
|
|
@@ -218,8 +550,11 @@ export class Relay {
|
|
|
218
550
|
res.json({
|
|
219
551
|
ok: true,
|
|
220
552
|
cdp: this.cdpBridge.getClient()?.isConnected() ?? false,
|
|
553
|
+
keepAwake: isKeepAwakeActive(),
|
|
221
554
|
debugChats: '/debug/chats',
|
|
222
555
|
debugSnapshot: '/debug/chat-snapshot',
|
|
556
|
+
debugLenta: '/debug/lenta?agentId=',
|
|
557
|
+
debugLentaSeq: '/debug/lenta-seq?agentId=',
|
|
223
558
|
debugJsonlLive: '/debug/jsonl-live',
|
|
224
559
|
});
|
|
225
560
|
});
|
|
@@ -239,6 +574,67 @@ export class Relay {
|
|
|
239
574
|
this.app.get('/debug/chat-snapshot', (_req, res) => {
|
|
240
575
|
res.json(this.readOnlyChatSnapshot());
|
|
241
576
|
});
|
|
577
|
+
this.app.get('/debug/lenta', async (req, res) => {
|
|
578
|
+
const agentId = typeof req.query.agentId === 'string' ? req.query.agentId.trim() : '';
|
|
579
|
+
if (!agentId) {
|
|
580
|
+
res.status(400).json({ error: 'agentId required' });
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const title = typeof req.query.title === 'string' ? req.query.title.trim() : undefined;
|
|
584
|
+
const reloadJsonl = req.query.reloadJsonl === '1' || req.query.reloadJsonl === 'true';
|
|
585
|
+
try {
|
|
586
|
+
const report = await this.buildLentaDebugReport(agentId, title, { reloadJsonl });
|
|
587
|
+
res.json(report);
|
|
588
|
+
}
|
|
589
|
+
catch (err) {
|
|
590
|
+
res.status(500).json({ error: err.message });
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
this.app.post('/debug/lenta-capture/start', express.json(), (req, res) => {
|
|
594
|
+
const body = (req.body ?? {});
|
|
595
|
+
const agentId = String(body.agentId ?? (typeof req.query.agentId === 'string' ? req.query.agentId : '') ?? '').trim();
|
|
596
|
+
if (!agentId) {
|
|
597
|
+
res.status(400).json({ error: 'agentId required' });
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
writeCaptureConfig({
|
|
601
|
+
enabled: true,
|
|
602
|
+
agentId,
|
|
603
|
+
title: body?.title?.trim(),
|
|
604
|
+
});
|
|
605
|
+
mkdirSync(captureSessionDir(agentId), { recursive: true });
|
|
606
|
+
res.json({
|
|
607
|
+
ok: true,
|
|
608
|
+
agentId,
|
|
609
|
+
sessionDir: captureSessionDir(agentId),
|
|
610
|
+
hint: 'Reproduce chat activity; snapshots append under sessionDir',
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
this.app.post('/debug/lenta-capture/stop', (_req, res) => {
|
|
614
|
+
const cfg = readCaptureConfig();
|
|
615
|
+
clearCaptureConfig();
|
|
616
|
+
res.json({ ok: true, stopped: cfg?.agentId ?? null });
|
|
617
|
+
});
|
|
618
|
+
this.app.get('/debug/lenta-capture/status', (_req, res) => {
|
|
619
|
+
const cfg = readCaptureConfig();
|
|
620
|
+
res.json({
|
|
621
|
+
config: cfg,
|
|
622
|
+
sessionDir: cfg ? captureSessionDir(cfg.agentId) : null,
|
|
623
|
+
steps: cfg ? listCaptureSteps(cfg.agentId) : [],
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
this.app.get('/debug/lenta-seq', (req, res) => {
|
|
627
|
+
const agentId = typeof req.query.agentId === 'string' ? req.query.agentId.trim() : undefined;
|
|
628
|
+
const journal = getLentaSeqJournal(agentId || undefined);
|
|
629
|
+
res.json({
|
|
630
|
+
at: new Date().toISOString(),
|
|
631
|
+
agentId: agentId ?? null,
|
|
632
|
+
logFile: getLentaSeqLogPath(),
|
|
633
|
+
currentSeq: agentId ? this.historySeqByAgent.get(agentId) ?? 0 : undefined,
|
|
634
|
+
journal,
|
|
635
|
+
handoffs: journal.filter((e) => e.transition === 'dom_to_jsonl'),
|
|
636
|
+
});
|
|
637
|
+
});
|
|
242
638
|
this.app.get('/debug/jsonl-live', async (req, res) => {
|
|
243
639
|
if (!this.checkMediaAuth(req)) {
|
|
244
640
|
res.status(401).json({ error: 'Unauthorized' });
|
|
@@ -342,29 +738,24 @@ export class Relay {
|
|
|
342
738
|
detail: `limit=${limit ?? 'all'} title=${title?.slice(0, 32) ?? '-'}`,
|
|
343
739
|
});
|
|
344
740
|
try {
|
|
741
|
+
const full = await this.jsonlIndex.loadHistory(agentId, {
|
|
742
|
+
title,
|
|
743
|
+
composerIdByTitle: this.stateManager.getState().composerIdByTitle,
|
|
744
|
+
offset,
|
|
745
|
+
});
|
|
746
|
+
this.chatDisplay.setJsonlBaseline(agentId, full.messages);
|
|
747
|
+
const history = limit > 0 && full.messages.length > limit
|
|
748
|
+
? { ...full, messages: full.messages.slice(-limit) }
|
|
749
|
+
: full;
|
|
345
750
|
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
751
|
res.json({
|
|
354
752
|
agentId,
|
|
355
|
-
...this.agentMessagesSnapshot(agentId,
|
|
753
|
+
...this.agentMessagesSnapshot(agentId, full.totalMessages),
|
|
356
754
|
requestId,
|
|
357
755
|
updatedAt: Date.now(),
|
|
358
756
|
});
|
|
359
757
|
return;
|
|
360
758
|
}
|
|
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
759
|
const snap = this.agentMessagesSnapshot(agentId, history.totalMessages);
|
|
369
760
|
const body = { ...history, ...snap, requestId, updatedAt: Date.now() };
|
|
370
761
|
const bytes = JSON.stringify(body).length;
|
|
@@ -528,6 +919,16 @@ export class Relay {
|
|
|
528
919
|
this.io.on('connection', (socket) => this.onConnect(socket));
|
|
529
920
|
}
|
|
530
921
|
broadcast(event, payload) {
|
|
922
|
+
if (event === 'agent:messages') {
|
|
923
|
+
const p = payload;
|
|
924
|
+
bridgePipelineLog({
|
|
925
|
+
dir: 'out',
|
|
926
|
+
event: 'broadcast:agent:messages',
|
|
927
|
+
agentId: p?.agentId,
|
|
928
|
+
msgs: p?.historyMessages?.length ?? 0,
|
|
929
|
+
detail: `live=${p?.liveMessages?.length ?? 0} append=${!!p?.append} seq=${p?.seq ?? '-'} upstream=${this.upstream?.connected ?? false}`,
|
|
930
|
+
});
|
|
931
|
+
}
|
|
531
932
|
if (event === 'agents:history' || event === 'agent:history') {
|
|
532
933
|
const h = payload;
|
|
533
934
|
const bytes = JSON.stringify(payload ?? {}).length;
|
|
@@ -550,22 +951,60 @@ export class Relay {
|
|
|
550
951
|
this.historySeqByAgent.set(agentId, n);
|
|
551
952
|
return n;
|
|
552
953
|
}
|
|
553
|
-
emitAgentMessages(agentId, historyMessages, liveMessages, source,
|
|
554
|
-
const outSeq = seq ?? this.nextHistorySeq(agentId);
|
|
954
|
+
emitAgentMessages(agentId, historyMessages, liveMessages, source, opts) {
|
|
555
955
|
const messages = [...historyMessages, ...liveMessages];
|
|
956
|
+
const sig = messagesFingerprint(messages);
|
|
957
|
+
const prevSeq = this.historySeqByAgent.get(agentId) ?? 0;
|
|
958
|
+
const prevHeld = getLastLentaEmit(agentId);
|
|
959
|
+
let outSeq;
|
|
960
|
+
let seqHeld = false;
|
|
961
|
+
if (opts?.forceSeq) {
|
|
962
|
+
outSeq = this.nextHistorySeq(agentId);
|
|
963
|
+
}
|
|
964
|
+
else if (prevHeld && prevHeld.messagesSig === sig) {
|
|
965
|
+
outSeq = prevHeld.seq;
|
|
966
|
+
seqHeld = true;
|
|
967
|
+
this.historySeqByAgent.set(agentId, outSeq);
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
outSeq = this.nextHistorySeq(agentId);
|
|
971
|
+
}
|
|
972
|
+
const entry = recordLentaSeqEmit(agentId, outSeq, prevSeq, seqHeld, messages, {
|
|
973
|
+
reason: opts?.reason ?? 'emit',
|
|
974
|
+
source,
|
|
975
|
+
historyLen: historyMessages.length,
|
|
976
|
+
liveLen: liveMessages.length,
|
|
977
|
+
messagesLen: messages.length,
|
|
978
|
+
append: opts?.append,
|
|
979
|
+
});
|
|
980
|
+
bridgePipelineLog({
|
|
981
|
+
dir: 'out',
|
|
982
|
+
event: 'lenta:seq',
|
|
983
|
+
agentId,
|
|
984
|
+
msgs: messages.length,
|
|
985
|
+
detail: `seq=${outSeq} prev=${prevSeq} held=${seqHeld ? 1 : 0} ` +
|
|
986
|
+
`reason=${entry.reason} transition=${entry.transition} ` +
|
|
987
|
+
`hist=${entry.historyLen} live=${entry.liveLen} src=${source}`,
|
|
988
|
+
});
|
|
556
989
|
this.broadcast('agent:messages', {
|
|
557
990
|
agentId,
|
|
558
991
|
historyMessages,
|
|
559
992
|
liveMessages,
|
|
560
993
|
messages,
|
|
561
|
-
totalMessages: totalMessages ?? messages.length,
|
|
994
|
+
totalMessages: opts?.totalMessages ?? messages.length,
|
|
562
995
|
source,
|
|
563
996
|
updatedAt: Date.now(),
|
|
564
997
|
seq: outSeq,
|
|
565
|
-
append: append || undefined,
|
|
998
|
+
append: opts?.append || undefined,
|
|
999
|
+
});
|
|
1000
|
+
this.writeLentaCapture(agentId, 'emit_agent_messages', {
|
|
1001
|
+
emitSeq: outSeq,
|
|
1002
|
+
seqHeld,
|
|
1003
|
+
transition: entry.transition,
|
|
566
1004
|
});
|
|
1005
|
+
this.noteLentaDelivery(agentId, messages);
|
|
1006
|
+
this.refreshLentaPendingPatch();
|
|
567
1007
|
}
|
|
568
|
-
/** DOM messages in state:patch — debug/UI chrome only; chat lenta is JSONL via agent:messages. */
|
|
569
1008
|
prepareStateMessages(raw) {
|
|
570
1009
|
return filterClientDisplayList(prepareChatMessagesForDisplay(raw));
|
|
571
1010
|
}
|
|
@@ -577,15 +1016,125 @@ export class Relay {
|
|
|
577
1016
|
messages: this.prepareStateMessages(payload.messages),
|
|
578
1017
|
};
|
|
579
1018
|
}
|
|
1019
|
+
buildLentaPendingByAgent() {
|
|
1020
|
+
const state = this.stateManager.getState();
|
|
1021
|
+
const out = {};
|
|
1022
|
+
for (const [agentId, meta] of this.jsonlIndex.getSubscribedAgents()) {
|
|
1023
|
+
if (!isChatSyncedWithCursor(agentId, meta.title, state))
|
|
1024
|
+
continue;
|
|
1025
|
+
const messages = this.chatDisplay.getDisplayMessages(agentId);
|
|
1026
|
+
if (isLentaDeliveryPending(agentId, messages))
|
|
1027
|
+
out[agentId] = true;
|
|
1028
|
+
}
|
|
1029
|
+
return out;
|
|
1030
|
+
}
|
|
1031
|
+
refreshLentaPendingCache() {
|
|
1032
|
+
this.cachedLentaPendingByAgent = this.buildLentaPendingByAgent();
|
|
1033
|
+
}
|
|
1034
|
+
withRelayState(payload) {
|
|
1035
|
+
const base = this.withDisplayState(payload);
|
|
1036
|
+
if (payload.messages?.length) {
|
|
1037
|
+
this.refreshLentaPendingCache();
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
...base,
|
|
1041
|
+
lentaPendingByAgent: this.cachedLentaPendingByAgent,
|
|
1042
|
+
updatedAt: base.updatedAt ?? Date.now(),
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
refreshLentaPendingPatch() {
|
|
1046
|
+
this.refreshLentaPendingCache();
|
|
1047
|
+
this.broadcast('state:patch', {
|
|
1048
|
+
lentaPendingByAgent: this.cachedLentaPendingByAgent,
|
|
1049
|
+
updatedAt: Date.now(),
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
noteLentaDelivery(agentId, storeMessages) {
|
|
1053
|
+
if (isLentaDeliveryPending(agentId, storeMessages)) {
|
|
1054
|
+
if (!this.lentaPendingSince.has(agentId)) {
|
|
1055
|
+
this.lentaPendingSince.set(agentId, Date.now());
|
|
1056
|
+
}
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
this.lentaPendingSince.delete(agentId);
|
|
1060
|
+
this.agentCompletionPush?.retryPendingContent(this.stateManager.getState());
|
|
1061
|
+
}
|
|
1062
|
+
/** Push `agent:messages` when store is ahead of last emit (subscribed + synced). */
|
|
1063
|
+
flushLentaDelivery(agentId, reason) {
|
|
1064
|
+
if (!this.jsonlIndex.getSubscribedAgents().has(agentId))
|
|
1065
|
+
return false;
|
|
1066
|
+
const state = this.stateManager.getState();
|
|
1067
|
+
const meta = this.jsonlIndex.getSubscribedAgents().get(agentId);
|
|
1068
|
+
if (!meta || !isChatSyncedWithCursor(agentId, meta.title, state))
|
|
1069
|
+
return false;
|
|
1070
|
+
const messages = this.chatDisplay.getDisplayMessages(agentId);
|
|
1071
|
+
if (!isLentaDeliveryPending(agentId, messages)) {
|
|
1072
|
+
this.lentaPendingSince.delete(agentId);
|
|
1073
|
+
return false;
|
|
1074
|
+
}
|
|
1075
|
+
const since = this.lentaPendingSince.get(agentId) ?? Date.now();
|
|
1076
|
+
if (!this.lentaPendingSince.has(agentId))
|
|
1077
|
+
this.lentaPendingSince.set(agentId, since);
|
|
1078
|
+
bridgePipelineLog({
|
|
1079
|
+
dir: 'internal',
|
|
1080
|
+
event: 'lenta:delivery:FLUSH',
|
|
1081
|
+
agentId,
|
|
1082
|
+
msgs: messages.length,
|
|
1083
|
+
detail: `reason=${reason} ageMs=${Date.now() - since}`,
|
|
1084
|
+
});
|
|
1085
|
+
this.emitLentaForAgent(agentId, {
|
|
1086
|
+
totalMessages: this.lastEmittedJsonlRows.get(agentId) ?? messages.length,
|
|
1087
|
+
reason,
|
|
1088
|
+
forceSeq: true,
|
|
1089
|
+
});
|
|
1090
|
+
this.agentCompletionPush?.retryPendingContent(this.stateManager.getState());
|
|
1091
|
+
return true;
|
|
1092
|
+
}
|
|
1093
|
+
flushPendingLentaForSubscribers(reason) {
|
|
1094
|
+
for (const agentId of this.jsonlIndex.getSubscribedAgents().keys()) {
|
|
1095
|
+
this.flushLentaDelivery(agentId, reason);
|
|
1096
|
+
}
|
|
1097
|
+
this.refreshLentaPendingPatch();
|
|
1098
|
+
}
|
|
1099
|
+
maybeForceStaleLentaDelivery() {
|
|
1100
|
+
const now = Date.now();
|
|
1101
|
+
for (const agentId of this.jsonlIndex.getSubscribedAgents().keys()) {
|
|
1102
|
+
const since = this.lentaPendingSince.get(agentId);
|
|
1103
|
+
if (since == null)
|
|
1104
|
+
continue;
|
|
1105
|
+
if (now - since < Relay.LENTA_PENDING_FORCE_MS)
|
|
1106
|
+
continue;
|
|
1107
|
+
this.flushLentaDelivery(agentId, 'delivery_flush');
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
580
1110
|
wireEvents() {
|
|
581
1111
|
this.stateManager.on('state:full', (state) => {
|
|
582
|
-
this.
|
|
1112
|
+
this.refreshLentaPendingCache();
|
|
1113
|
+
this.broadcast('state:full', this.withRelayState(state));
|
|
583
1114
|
this.emitAgentsIndex();
|
|
584
1115
|
this.observeAgentCompletionForPush();
|
|
1116
|
+
if (state.messages?.length)
|
|
1117
|
+
this.applyDomOverlayFromState(state);
|
|
1118
|
+
if (!state.agentWorking)
|
|
1119
|
+
this.flushPendingLentaForSubscribers('dom_overlay');
|
|
1120
|
+
this.maybeForceStaleLentaDelivery();
|
|
585
1121
|
});
|
|
586
1122
|
this.stateManager.on('state:patch', (patch) => {
|
|
587
|
-
this.broadcast('state:patch', this.
|
|
1123
|
+
this.broadcast('state:patch', this.withRelayState(patch));
|
|
588
1124
|
this.observeAgentCompletionForPush();
|
|
1125
|
+
const state = this.stateManager.getState();
|
|
1126
|
+
if (patch.messages?.length || state.messages?.length) {
|
|
1127
|
+
this.applyDomOverlayFromState(state);
|
|
1128
|
+
}
|
|
1129
|
+
if (patch.agentWorking === false || (!state.agentWorking && patch.agentWorking !== true)) {
|
|
1130
|
+
this.syncGeneratingFlags(state);
|
|
1131
|
+
this.flushPendingLentaForSubscribers('jsonl_live');
|
|
1132
|
+
this.refreshLentaPendingCache();
|
|
1133
|
+
}
|
|
1134
|
+
else if (state.agentWorking) {
|
|
1135
|
+
this.syncGeneratingFlags(state);
|
|
1136
|
+
}
|
|
1137
|
+
this.maybeForceStaleLentaDelivery();
|
|
589
1138
|
if (patch.sidebarRepos || patch.composerIdByTitle) {
|
|
590
1139
|
const key = JSON.stringify([patch.sidebarRepos, patch.composerIdByTitle]);
|
|
591
1140
|
if (key !== this.lastSidebarIndexKey) {
|
|
@@ -605,7 +1154,7 @@ export class Relay {
|
|
|
605
1154
|
this.jsonlIndex.on('agent:jsonl:updated', (payload) => {
|
|
606
1155
|
const total = payload.totalMessages ?? payload.messages.length;
|
|
607
1156
|
this.syncJsonlToSubscribedAgents(payload.agentId, payload.messages, total);
|
|
608
|
-
this.
|
|
1157
|
+
this.writeLentaCapture(payload.agentId, 'jsonl_file');
|
|
609
1158
|
});
|
|
610
1159
|
this.jsonlIndex.on('agent:history', (history) => {
|
|
611
1160
|
const total = history.totalMessages ?? history.messages.length;
|
|
@@ -655,7 +1204,7 @@ export class Relay {
|
|
|
655
1204
|
});
|
|
656
1205
|
}
|
|
657
1206
|
pushFullStateToRemote() {
|
|
658
|
-
this.broadcast('state:full', this.
|
|
1207
|
+
this.broadcast('state:full', this.withRelayState(this.stateManager.getState()));
|
|
659
1208
|
if (this.lastJsonlIndex.updatedAt > 0) {
|
|
660
1209
|
this.emitAgentsIndex(true);
|
|
661
1210
|
return;
|
|
@@ -692,7 +1241,7 @@ export class Relay {
|
|
|
692
1241
|
this.dispatchClientEvent(event, args, reply);
|
|
693
1242
|
}
|
|
694
1243
|
onConnect(socket) {
|
|
695
|
-
socket.emit('state:full', this.
|
|
1244
|
+
socket.emit('state:full', this.withRelayState(this.stateManager.getState()));
|
|
696
1245
|
const sendIndex = (index) => {
|
|
697
1246
|
socket.emit('agents:index', mergeSidebarWithJsonl(this.stateManager.getState().sidebarRepos, index, this.stateManager.getState().composerIdByTitle));
|
|
698
1247
|
};
|
|
@@ -795,36 +1344,28 @@ export class Relay {
|
|
|
795
1344
|
detail: `limit=${socketLimit} title=${title?.slice(0, 32) ?? '-'} upstream=${viaUpstream}`,
|
|
796
1345
|
});
|
|
797
1346
|
try {
|
|
1347
|
+
const full = await this.jsonlIndex.loadHistory(agentId, {
|
|
1348
|
+
title,
|
|
1349
|
+
composerIdByTitle: this.stateManager.getState().composerIdByTitle,
|
|
1350
|
+
offset,
|
|
1351
|
+
});
|
|
1352
|
+
this.emitJsonlLiveForAgent(agentId, full.messages, full.totalMessages);
|
|
1353
|
+
const snap = this.agentMessagesSnapshot(agentId, full.totalMessages);
|
|
798
1354
|
if (!isChatHistoryFromJsonl()) {
|
|
799
|
-
const
|
|
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
|
-
const seq = this.historySeqByAgent.get(agentId) ?? this.nextHistorySeq(agentId);
|
|
1355
|
+
const seq = this.historySeqByAgent.get(agentId) ?? 0;
|
|
808
1356
|
const ms = Date.now() - t0;
|
|
809
|
-
console.log(`[relay] agents:history jsonl agentId=${agentId} hist=${snap.historyMessages.length} total=${
|
|
1357
|
+
console.log(`[relay] agents:history jsonl agentId=${agentId} hist=${snap.historyMessages.length} total=${full.totalMessages} ms=${ms} rid=${requestId ?? '-'}`);
|
|
810
1358
|
reply('agents:history', {
|
|
811
1359
|
agentId,
|
|
812
1360
|
...snap,
|
|
813
|
-
totalMessages:
|
|
1361
|
+
totalMessages: full.totalMessages,
|
|
814
1362
|
requestId,
|
|
815
1363
|
updatedAt: Date.now(),
|
|
816
1364
|
seq,
|
|
817
1365
|
});
|
|
818
1366
|
return;
|
|
819
1367
|
}
|
|
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);
|
|
1368
|
+
const history = full;
|
|
828
1369
|
const ms = Date.now() - t0;
|
|
829
1370
|
const bytes = JSON.stringify(snap.messages).length;
|
|
830
1371
|
console.log(`[relay] agents:history ok agentId=${history.agentId} msgs=${history.messages?.length ?? 0}/${history.totalMessages ?? '?'} bytes=${bytes} ms=${ms} rid=${requestId ?? '-'}`);
|
|
@@ -837,7 +1378,7 @@ export class Relay {
|
|
|
837
1378
|
msgs: history.messages?.length ?? 0,
|
|
838
1379
|
detail: `total=${history.totalMessages ?? '?'} ms=${ms}`,
|
|
839
1380
|
});
|
|
840
|
-
const seq = this.
|
|
1381
|
+
const seq = this.historySeqByAgent.get(history.agentId) ?? 0;
|
|
841
1382
|
reply('agents:history', {
|
|
842
1383
|
...history,
|
|
843
1384
|
historyMessages: snap.historyMessages,
|
|
@@ -872,11 +1413,12 @@ export class Relay {
|
|
|
872
1413
|
async runAgentsSubscribe({ agentId, title, focus, }) {
|
|
873
1414
|
if (!agentId)
|
|
874
1415
|
return;
|
|
875
|
-
this.jsonlIndex.
|
|
1416
|
+
const alreadySubscribed = this.jsonlIndex.getSubscribedAgents().has(agentId);
|
|
1417
|
+
this.jsonlIndex.subscribe(agentId, title, { emitHistory: !alreadySubscribed });
|
|
876
1418
|
this.stateManager.patchNow({ lastError: undefined });
|
|
877
1419
|
await this.trySwitchWindowForAgent(agentId);
|
|
878
1420
|
if (focus === false) {
|
|
879
|
-
void this.refreshDomChatOnSubscribe(agentId, title);
|
|
1421
|
+
void this.refreshDomChatOnSubscribe(agentId, title, { clear: !alreadySubscribed });
|
|
880
1422
|
return;
|
|
881
1423
|
}
|
|
882
1424
|
const result = await this.commandExecutor.execute({
|
|
@@ -888,18 +1430,43 @@ export class Relay {
|
|
|
888
1430
|
if (!result.ok) {
|
|
889
1431
|
console.warn('[relay] subscribe focus (non-fatal):', result.error);
|
|
890
1432
|
}
|
|
891
|
-
void this.refreshDomChatOnSubscribe(agentId, title);
|
|
1433
|
+
void this.refreshDomChatOnSubscribe(agentId, title, { clear: !alreadySubscribed });
|
|
1434
|
+
}
|
|
1435
|
+
/** Load JSONL into store when DOM ingest runs but baseline empty (e.g. after bridge restart). */
|
|
1436
|
+
async ensureJsonlBaselineForAgent(agentId, state) {
|
|
1437
|
+
if (this.chatDisplay.getJsonlHistory(agentId).length > 0)
|
|
1438
|
+
return;
|
|
1439
|
+
const sub = this.jsonlIndex.getSubscribedAgents().get(agentId);
|
|
1440
|
+
try {
|
|
1441
|
+
const history = await this.jsonlIndex.loadHistory(agentId, {
|
|
1442
|
+
title: sub?.title,
|
|
1443
|
+
composerIdByTitle: state.composerIdByTitle,
|
|
1444
|
+
});
|
|
1445
|
+
if (!history.messages.length)
|
|
1446
|
+
return;
|
|
1447
|
+
this.chatDisplay.setJsonlBaseline(agentId, history.messages);
|
|
1448
|
+
this.lastEmittedJsonlRows.set(agentId, history.totalMessages);
|
|
1449
|
+
this.domExtractor.pollNow();
|
|
1450
|
+
}
|
|
1451
|
+
catch (err) {
|
|
1452
|
+
console.warn('[relay] ensureJsonlBaseline (non-fatal):', err.message);
|
|
1453
|
+
}
|
|
892
1454
|
}
|
|
893
1455
|
/** Открытие чата: JSONL baseline + focus/scroll (DOM poll только для state: working/approve). */
|
|
894
|
-
async refreshDomChatOnSubscribe(agentId, title) {
|
|
1456
|
+
async refreshDomChatOnSubscribe(agentId, title, opts) {
|
|
895
1457
|
try {
|
|
896
|
-
|
|
897
|
-
|
|
1458
|
+
if (opts?.clear !== false) {
|
|
1459
|
+
this.chatDisplay.clearAgent(agentId);
|
|
1460
|
+
this.lastEmittedJsonlRows.delete(agentId);
|
|
1461
|
+
this.lastEmittedLentaSig.delete(agentId);
|
|
1462
|
+
this.lastEmittedHistLen.delete(agentId);
|
|
1463
|
+
this.lentaPendingSince.delete(agentId);
|
|
1464
|
+
}
|
|
898
1465
|
const history = await this.jsonlIndex.loadHistory(agentId, {
|
|
899
1466
|
title,
|
|
900
1467
|
composerIdByTitle: this.stateManager.getState().composerIdByTitle,
|
|
901
|
-
limit: AGENT_HISTORY_DEFAULT_LIMIT,
|
|
902
1468
|
});
|
|
1469
|
+
this.lastEmittedLentaSig.delete(agentId);
|
|
903
1470
|
this.emitJsonlLiveForAgent(agentId, history.messages, history.totalMessages);
|
|
904
1471
|
await this.commandExecutor.scrollChatToBottom();
|
|
905
1472
|
this.domExtractor.pollNow();
|
|
@@ -913,6 +1480,10 @@ export class Relay {
|
|
|
913
1480
|
this.jsonlIndex.unsubscribe(agentId);
|
|
914
1481
|
this.chatDisplay.clearAgent(agentId);
|
|
915
1482
|
this.lastEmittedJsonlRows.delete(agentId);
|
|
1483
|
+
this.lastEmittedLentaSig.delete(agentId);
|
|
1484
|
+
this.lastEmittedHistLen.delete(agentId);
|
|
1485
|
+
this.lentaPendingSince.delete(agentId);
|
|
1486
|
+
this.refreshLentaPendingPatch();
|
|
916
1487
|
}
|
|
917
1488
|
}
|
|
918
1489
|
async runAgentsFocus({ agentId, title }, reply) {
|