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.
Files changed (44) hide show
  1. package/bridge-runtime/.env.example +7 -1
  2. package/bridge-runtime/connector-version.json +1 -1
  3. package/bridge-runtime/dist/agent-completion-push.d.ts +27 -22
  4. package/bridge-runtime/dist/agent-completion-push.js +242 -122
  5. package/bridge-runtime/dist/agent-completion-readiness.d.ts +19 -0
  6. package/bridge-runtime/dist/agent-completion-readiness.js +42 -0
  7. package/bridge-runtime/dist/chat-display-store.d.ts +32 -7
  8. package/bridge-runtime/dist/chat-display-store.js +99 -21
  9. package/bridge-runtime/dist/chat-display.d.ts +36 -0
  10. package/bridge-runtime/dist/chat-display.js +287 -24
  11. package/bridge-runtime/dist/chat-sync.d.ts +3 -1
  12. package/bridge-runtime/dist/chat-sync.js +20 -0
  13. package/bridge-runtime/dist/config.js +2 -0
  14. package/bridge-runtime/dist/connector-client-version.js +1 -1
  15. package/bridge-runtime/dist/debug-chats-page.d.ts +1 -1
  16. package/bridge-runtime/dist/debug-chats-page.js +148 -26
  17. package/bridge-runtime/dist/dom-transcript-store.d.ts +3 -1
  18. package/bridge-runtime/dist/dom-transcript-store.js +18 -3
  19. package/bridge-runtime/dist/extract-page.js +5 -4
  20. package/bridge-runtime/dist/index.js +9 -0
  21. package/bridge-runtime/dist/keep-awake.d.ts +5 -0
  22. package/bridge-runtime/dist/keep-awake.js +48 -0
  23. package/bridge-runtime/dist/lenta-capture.d.ts +46 -0
  24. package/bridge-runtime/dist/lenta-capture.js +146 -0
  25. package/bridge-runtime/dist/lenta-debug.d.ts +42 -0
  26. package/bridge-runtime/dist/lenta-debug.js +221 -0
  27. package/bridge-runtime/dist/lenta-delivery.d.ts +3 -0
  28. package/bridge-runtime/dist/lenta-delivery.js +10 -0
  29. package/bridge-runtime/dist/lenta-seq-journal.d.ts +48 -0
  30. package/bridge-runtime/dist/lenta-seq-journal.js +109 -0
  31. package/bridge-runtime/dist/message-filter.d.ts +5 -0
  32. package/bridge-runtime/dist/message-filter.js +4 -0
  33. package/bridge-runtime/dist/relay-upstream.d.ts +3 -0
  34. package/bridge-runtime/dist/relay-upstream.js +21 -0
  35. package/bridge-runtime/dist/relay.d.ts +47 -3
  36. package/bridge-runtime/dist/relay.js +667 -96
  37. package/bridge-runtime/dist/types.d.ts +13 -4
  38. package/dist/bridge-build.js +50 -0
  39. package/dist/index.js +9 -6
  40. package/dist/launch.js +5 -1
  41. package/dist/run-service.js +10 -4
  42. package/dist/startup-check.js +6 -0
  43. package/package.json +1 -1
  44. package/version-policy.json +2 -2
@@ -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
+ }
@@ -0,0 +1,46 @@
1
+ import type { ChatMessage, HistoryMessage } from './types.js';
2
+ export type LentaCaptureReason = 'jsonl_file' | 'dom_ingest' | 'emit_agent_messages' | 'manual';
3
+ export interface LentaCaptureConfig {
4
+ enabled: boolean;
5
+ agentId: string;
6
+ title?: string;
7
+ }
8
+ export interface LentaCapturePayload {
9
+ reason: LentaCaptureReason;
10
+ source?: 'dom' | 'jsonl' | 'hybrid';
11
+ emitSeq?: number;
12
+ seqHeld?: boolean;
13
+ transition?: string;
14
+ jsonlRowCount?: number;
15
+ historyMessages: ChatMessage[];
16
+ liveMessages: ChatMessage[];
17
+ messages: ChatMessage[];
18
+ domRaw?: ChatMessage[];
19
+ domViewport?: ChatMessage[];
20
+ jsonlRawRows?: HistoryMessage[];
21
+ jsonlFilePath?: string;
22
+ activeComposerId?: string;
23
+ agentWorking?: boolean;
24
+ }
25
+ export declare function captureConfigPath(): string;
26
+ export declare function readCaptureConfig(): LentaCaptureConfig | null;
27
+ export declare function writeCaptureConfig(cfg: LentaCaptureConfig): void;
28
+ export declare function clearCaptureConfig(): void;
29
+ export declare function captureSessionDir(agentId: string): string;
30
+ export declare class LentaCaptureSession {
31
+ private readonly agentId;
32
+ private step;
33
+ private lastSig;
34
+ constructor(agentId: string);
35
+ shouldCapture(targetAgentId: string): boolean;
36
+ record(payload: LentaCapturePayload): string | null;
37
+ }
38
+ export declare function getLentaCaptureSession(agentId: string): LentaCaptureSession | null;
39
+ export declare function captureLentaIfEnabled(agentId: string, payload: LentaCapturePayload): void;
40
+ export declare function resolveAgentJsonlPath(projectsDir: string, agentId: string, opts: {
41
+ title?: string;
42
+ composerIdByTitle?: Record<string, string>;
43
+ activeComposerId?: string;
44
+ activeTabTitle?: string;
45
+ }): string | undefined;
46
+ export declare function listCaptureSteps(agentId: string): string[];
@@ -0,0 +1,146 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ import { messagesFingerprint } from './lenta-seq-journal.js';
5
+ import { resolveJsonlFilePath } from './agent-title-match.js';
6
+ const CAPTURE_ROOT = join(homedir(), '.cursorconnect', 'capture');
7
+ export function captureConfigPath() {
8
+ return join(CAPTURE_ROOT, 'config.json');
9
+ }
10
+ export function readCaptureConfig() {
11
+ const path = captureConfigPath();
12
+ if (!existsSync(path))
13
+ return null;
14
+ try {
15
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
16
+ if (!raw.enabled || !raw.agentId?.trim())
17
+ return null;
18
+ return { ...raw, agentId: raw.agentId.trim() };
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export function writeCaptureConfig(cfg) {
25
+ mkdirSync(CAPTURE_ROOT, { recursive: true });
26
+ writeFileSync(captureConfigPath(), `${JSON.stringify(cfg, null, 2)}\n`);
27
+ }
28
+ export function clearCaptureConfig() {
29
+ writeCaptureConfig({ enabled: false, agentId: '' });
30
+ }
31
+ export function captureSessionDir(agentId) {
32
+ return join(CAPTURE_ROOT, 'sessions', agentId);
33
+ }
34
+ export class LentaCaptureSession {
35
+ agentId;
36
+ step = 0;
37
+ lastSig = '';
38
+ constructor(agentId) {
39
+ this.agentId = agentId;
40
+ const dir = captureSessionDir(agentId);
41
+ mkdirSync(dir, { recursive: true });
42
+ const manifestPath = join(dir, 'manifest.jsonl');
43
+ if (!existsSync(manifestPath)) {
44
+ writeFileSync(manifestPath, `${JSON.stringify({
45
+ startedAt: new Date().toISOString(),
46
+ agentId,
47
+ note: 'Full JSONL+DOM snapshots per bridge event',
48
+ })}\n`);
49
+ }
50
+ }
51
+ shouldCapture(targetAgentId) {
52
+ return targetAgentId === this.agentId;
53
+ }
54
+ record(payload) {
55
+ const sig = messagesFingerprint(payload.messages);
56
+ if (sig === this.lastSig && payload.reason !== 'jsonl_file') {
57
+ return null;
58
+ }
59
+ this.lastSig = sig;
60
+ this.step += 1;
61
+ const stepId = `${String(this.step).padStart(5, '0')}-${payload.reason}`;
62
+ const dir = join(captureSessionDir(this.agentId), stepId);
63
+ mkdirSync(dir, { recursive: true });
64
+ const meta = {
65
+ at: new Date().toISOString(),
66
+ step: this.step,
67
+ stepId,
68
+ agentId: this.agentId,
69
+ reason: payload.reason,
70
+ source: payload.source,
71
+ emitSeq: payload.emitSeq,
72
+ seqHeld: payload.seqHeld,
73
+ transition: payload.transition,
74
+ jsonlRowCount: payload.jsonlRowCount,
75
+ counts: {
76
+ messages: payload.messages.length,
77
+ history: payload.historyMessages.length,
78
+ live: payload.liveMessages.length,
79
+ domRaw: payload.domRaw?.length ?? 0,
80
+ domViewport: payload.domViewport?.length ?? 0,
81
+ jsonlRaw: payload.jsonlRawRows?.length ?? 0,
82
+ },
83
+ messagesSig: sig.slice(0, 500),
84
+ activeComposerId: payload.activeComposerId,
85
+ agentWorking: payload.agentWorking,
86
+ jsonlFile: payload.jsonlFilePath,
87
+ };
88
+ writeFileSync(join(dir, 'meta.json'), `${JSON.stringify(meta, null, 2)}\n`);
89
+ writeFileSync(join(dir, 'messages.json'), `${JSON.stringify(payload.messages, null, 2)}\n`);
90
+ writeFileSync(join(dir, 'historyMessages.json'), `${JSON.stringify(payload.historyMessages, null, 2)}\n`);
91
+ writeFileSync(join(dir, 'liveMessages.json'), `${JSON.stringify(payload.liveMessages, null, 2)}\n`);
92
+ if (payload.domRaw?.length) {
93
+ writeFileSync(join(dir, 'domRaw.json'), `${JSON.stringify(payload.domRaw, null, 2)}\n`);
94
+ }
95
+ if (payload.domViewport?.length) {
96
+ writeFileSync(join(dir, 'domViewport.json'), `${JSON.stringify(payload.domViewport, null, 2)}\n`);
97
+ }
98
+ if (payload.jsonlRawRows?.length) {
99
+ writeFileSync(join(dir, 'jsonl.rows.json'), `${JSON.stringify(payload.jsonlRawRows, null, 2)}\n`);
100
+ }
101
+ if (payload.jsonlFilePath && existsSync(payload.jsonlFilePath)) {
102
+ try {
103
+ copyFileSync(payload.jsonlFilePath, join(dir, 'transcript.jsonl'));
104
+ }
105
+ catch {
106
+ /* non-fatal */
107
+ }
108
+ }
109
+ const manifestLine = JSON.stringify(meta);
110
+ writeFileSync(join(captureSessionDir(this.agentId), 'manifest.jsonl'), manifestLine + '\n', { flag: 'a' });
111
+ bridgeCaptureLog(stepId, meta.counts, payload.reason);
112
+ return dir;
113
+ }
114
+ }
115
+ function bridgeCaptureLog(stepId, counts, reason) {
116
+ console.log(`[lenta-capture] ${stepId} reason=${reason} msgs=${counts.messages} hist=${counts.history} live=${counts.live} domRaw=${counts.domRaw}`);
117
+ }
118
+ let session = null;
119
+ let sessionAgentId = '';
120
+ export function getLentaCaptureSession(agentId) {
121
+ const cfg = readCaptureConfig();
122
+ if (!cfg || cfg.agentId !== agentId)
123
+ return null;
124
+ if (!session || sessionAgentId !== agentId) {
125
+ session = new LentaCaptureSession(agentId);
126
+ sessionAgentId = agentId;
127
+ }
128
+ return session;
129
+ }
130
+ export function captureLentaIfEnabled(agentId, payload) {
131
+ const cap = getLentaCaptureSession(agentId);
132
+ if (!cap)
133
+ return;
134
+ cap.record(payload);
135
+ }
136
+ export function resolveAgentJsonlPath(projectsDir, agentId, opts) {
137
+ return resolveJsonlFilePath(projectsDir, agentId, opts) ?? undefined;
138
+ }
139
+ export function listCaptureSteps(agentId) {
140
+ const dir = captureSessionDir(agentId);
141
+ if (!existsSync(dir))
142
+ return [];
143
+ return readdirSync(dir)
144
+ .filter((n) => /^\d{5}-/.test(n))
145
+ .sort();
146
+ }
@@ -0,0 +1,42 @@
1
+ import type { ChatMessage } from './types.js';
2
+ export type LentaRowPreview = {
3
+ source: string;
4
+ role: string;
5
+ flat: number | null;
6
+ domSeq: number | null;
7
+ id: string;
8
+ len: number;
9
+ head: string;
10
+ };
11
+ export type LentaInvariantIssue = {
12
+ type: string;
13
+ detail?: string;
14
+ dom?: LentaRowPreview;
15
+ bridge?: LentaRowPreview;
16
+ skipped?: string[];
17
+ index?: number;
18
+ };
19
+ export declare function normPreviewText(t: string | undefined): string;
20
+ export declare function previewRow(m: ChatMessage, source: string): LentaRowPreview;
21
+ /** `messages` must equal `[...historyMessages, ...liveMessages]` (socket/HTTP payload). */
22
+ export declare function checkPayloadCompose(messages: ChatMessage[], historyMessages: ChatMessage[], liveMessages: ChatMessage[]): LentaInvariantIssue[];
23
+ /** In-memory store: `messages` = jsonlHistory + domOverlay. */
24
+ export declare function checkStoreCompose(jsonlHistory: ChatMessage[], domOverlay: ChatMessage[], messages: ChatMessage[]): LentaInvariantIssue[];
25
+ export declare function checkMonotonicOrder(messages: ChatMessage[], label: string): LentaInvariantIssue[];
26
+ /** DOM overlay must not repeat a turn already in JSONL archive. */
27
+ export declare function checkOverlayDuplicatesArchive(jsonlHistory: ChatMessage[], domOverlay: ChatMessage[]): LentaInvariantIssue[];
28
+ export declare function checkOverlayAfterArchive(jsonlHistory: ChatMessage[], domOverlay: ChatMessage[]): LentaInvariantIssue[];
29
+ /** When not synced with Cursor, DOM overlay for subscribed chat should stay empty. */
30
+ export declare function checkSyncGate(synced: boolean, domOverlay: ChatMessage[], domRawCount: number): LentaInvariantIssue[];
31
+ export declare function rowSimilar(a: LentaRowPreview, b: LentaRowPreview): boolean;
32
+ /** DOM viewport vs full lenta — match each viewport row to lenta from the end (avoids duplicate-text false gaps). */
33
+ export declare function compareDomTailToLenta(domRows: LentaRowPreview[], lentaRows: LentaRowPreview[], tail: number, opts?: {
34
+ hasDomOverlay?: boolean;
35
+ hasJsonlArchive?: boolean;
36
+ agentWorking?: boolean;
37
+ }): {
38
+ issues: LentaInvariantIssue[];
39
+ domTail: LentaRowPreview[];
40
+ lentaTail: LentaRowPreview[];
41
+ };
42
+ export declare function checkApiMatchesStore(apiMessages: ChatMessage[], storeMessages: ChatMessage[]): LentaInvariantIssue[];
@@ -0,0 +1,221 @@
1
+ import { archiveCoversOverlay, messageOrderKey } from './chat-display.js';
2
+ export function normPreviewText(t) {
3
+ return String(t ?? '')
4
+ .replace(/\s+/g, ' ')
5
+ .trim()
6
+ .slice(0, 400);
7
+ }
8
+ export function previewRow(m, source) {
9
+ const text = normPreviewText(m.text);
10
+ return {
11
+ source,
12
+ role: m.role ?? '?',
13
+ flat: m.flatIndex ?? null,
14
+ domSeq: m.domSeq ?? null,
15
+ id: (m.id ?? '').slice(0, 28),
16
+ len: text.length,
17
+ head: text.slice(0, 72),
18
+ };
19
+ }
20
+ function rowFingerprint(m) {
21
+ const text = normPreviewText(m.text);
22
+ return `${m.role}|${m.id ?? ''}|${text.length}|${text.slice(0, 64)}`;
23
+ }
24
+ function rowsFingerprint(rows) {
25
+ return rows.map(rowFingerprint);
26
+ }
27
+ /** `messages` must equal `[...historyMessages, ...liveMessages]` (socket/HTTP payload). */
28
+ export function checkPayloadCompose(messages, historyMessages, liveMessages) {
29
+ const issues = [];
30
+ const expectedLen = historyMessages.length + liveMessages.length;
31
+ if (messages.length !== expectedLen) {
32
+ issues.push({
33
+ type: 'PAYLOAD_COMPOSE_LEN',
34
+ detail: `messages=${messages.length} history+live=${expectedLen} (${historyMessages.length}+${liveMessages.length})`,
35
+ });
36
+ }
37
+ const composed = [...historyMessages, ...liveMessages];
38
+ const a = rowsFingerprint(messages);
39
+ const b = rowsFingerprint(composed);
40
+ const n = Math.min(a.length, b.length);
41
+ for (let i = 0; i < n; i++) {
42
+ if (a[i] !== b[i]) {
43
+ issues.push({
44
+ type: 'PAYLOAD_COMPOSE_ROW',
45
+ index: i,
46
+ detail: `messages[${i}] !== concat(history,live)[${i}]`,
47
+ });
48
+ break;
49
+ }
50
+ }
51
+ if (a.length !== b.length && !issues.some((x) => x.type === 'PAYLOAD_COMPOSE_LEN')) {
52
+ issues.push({
53
+ type: 'PAYLOAD_COMPOSE_ROW',
54
+ detail: `fingerprint len ${a.length} vs ${b.length}`,
55
+ });
56
+ }
57
+ return issues;
58
+ }
59
+ /** In-memory store: `messages` = jsonlHistory + domOverlay. */
60
+ export function checkStoreCompose(jsonlHistory, domOverlay, messages) {
61
+ const issues = [];
62
+ const expectedLen = jsonlHistory.length + domOverlay.length;
63
+ if (messages.length !== expectedLen) {
64
+ issues.push({
65
+ type: 'STORE_COMPOSE_LEN',
66
+ detail: `messages=${messages.length} jsonl+overlay=${expectedLen}`,
67
+ });
68
+ }
69
+ const composed = [...jsonlHistory, ...domOverlay];
70
+ const a = rowsFingerprint(messages);
71
+ const b = rowsFingerprint(composed);
72
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
73
+ if (a[i] !== b[i]) {
74
+ issues.push({
75
+ type: 'STORE_COMPOSE_ROW',
76
+ index: i,
77
+ detail: `messages[${i}] !== jsonl+overlay[${i}]`,
78
+ });
79
+ break;
80
+ }
81
+ }
82
+ return issues;
83
+ }
84
+ export function checkMonotonicOrder(messages, label) {
85
+ const issues = [];
86
+ for (let i = 1; i < messages.length; i++) {
87
+ const prev = messageOrderKey(messages[i - 1]);
88
+ const cur = messageOrderKey(messages[i]);
89
+ if (cur < prev) {
90
+ issues.push({
91
+ type: 'LENTA_ORDER_INVERSION',
92
+ index: i,
93
+ detail: `${label} i=${i} ${prev}>${cur}`,
94
+ });
95
+ break;
96
+ }
97
+ }
98
+ return issues;
99
+ }
100
+ /** DOM overlay must not repeat a turn already in JSONL archive. */
101
+ export function checkOverlayDuplicatesArchive(jsonlHistory, domOverlay) {
102
+ const issues = [];
103
+ for (const dom of domOverlay) {
104
+ const hit = jsonlHistory.find((h) => archiveCoversOverlay(h, dom));
105
+ if (hit) {
106
+ issues.push({
107
+ type: 'OVERLAY_DUPLICATES_ARCHIVE',
108
+ detail: `dom flat=${dom.flatIndex ?? '?'} covered by jsonl flat=${hit.flatIndex ?? '?'}`,
109
+ dom: previewRow(dom, 'domOverlay'),
110
+ bridge: previewRow(hit, 'jsonlHistory'),
111
+ });
112
+ }
113
+ }
114
+ return issues;
115
+ }
116
+ export function checkOverlayAfterArchive(jsonlHistory, domOverlay) {
117
+ if (!jsonlHistory.length || !domOverlay.length)
118
+ return [];
119
+ let floor = 0;
120
+ for (const m of jsonlHistory) {
121
+ floor = Math.max(floor, messageOrderKey(m));
122
+ }
123
+ const minOverlay = Math.min(...domOverlay.map((m) => messageOrderKey(m)));
124
+ if (minOverlay <= floor) {
125
+ return [
126
+ {
127
+ type: 'OVERLAY_NOT_AFTER_ARCHIVE',
128
+ detail: `archive floor=${floor} overlay min key=${minOverlay}`,
129
+ },
130
+ ];
131
+ }
132
+ return [];
133
+ }
134
+ /** When not synced with Cursor, DOM overlay for subscribed chat should stay empty. */
135
+ export function checkSyncGate(synced, domOverlay, domRawCount) {
136
+ if (synced)
137
+ return [];
138
+ if (domOverlay.length > 0) {
139
+ return [
140
+ {
141
+ type: 'OVERLAY_WHILE_UNSYNCED',
142
+ detail: `overlay=${domOverlay.length} domRaw=${domRawCount}`,
143
+ },
144
+ ];
145
+ }
146
+ return [];
147
+ }
148
+ export function rowSimilar(a, b) {
149
+ if (a.role !== b.role)
150
+ return false;
151
+ const ah = a.head.toLowerCase();
152
+ const bh = b.head.toLowerCase();
153
+ if (!ah || !bh)
154
+ return ah === bh;
155
+ if (ah === bh)
156
+ return true;
157
+ const short = ah.length <= bh.length ? ah : bh;
158
+ const long = ah.length <= bh.length ? bh : ah;
159
+ if (short.length >= 8 && long.includes(short))
160
+ return true;
161
+ return long.includes(short) && short.length >= 16;
162
+ }
163
+ /** DOM viewport vs full lenta — match each viewport row to lenta from the end (avoids duplicate-text false gaps). */
164
+ export function compareDomTailToLenta(domRows, lentaRows, tail, opts) {
165
+ const issues = [];
166
+ const domTailN = opts?.hasJsonlArchive ? Math.min(tail, 4) : tail;
167
+ const d = domRows.slice(-domTailN);
168
+ const full = lentaRows;
169
+ const lentaTail = full.slice(-Math.max(tail, 16));
170
+ if (!d.length || !full.length) {
171
+ return { issues, domTail: d, lentaTail };
172
+ }
173
+ const searchWindow = Math.min(full.length, Math.max(tail * 10, full.length > 80 ? 120 : 48));
174
+ let searchEnd = full.length;
175
+ for (let di = d.length - 1; di >= 0; di--) {
176
+ const dom = d[di];
177
+ let found = -1;
178
+ const from = Math.max(0, searchEnd - searchWindow);
179
+ for (let j = searchEnd - 1; j >= from; j--) {
180
+ if (rowSimilar(dom, full[j])) {
181
+ found = j;
182
+ break;
183
+ }
184
+ }
185
+ if (found < 0) {
186
+ const domFlat = dom.flat ?? 0;
187
+ const maxLentaFlat = full.reduce((m, r) => Math.max(m, r.flat ?? 0), 0);
188
+ const type = !opts?.hasDomOverlay && (opts?.agentWorking || domFlat > maxLentaFlat + 5)
189
+ ? 'NEEDS_DOM_OVERLAY'
190
+ : 'DOM_MISSING_IN_LENTA';
191
+ issues.push({ type, dom, detail: `domFlat=${domFlat} lentaMax=${maxLentaFlat}` });
192
+ continue;
193
+ }
194
+ searchEnd = found;
195
+ }
196
+ return { issues, domTail: d, lentaTail };
197
+ }
198
+ export function checkApiMatchesStore(apiMessages, storeMessages) {
199
+ const a = rowsFingerprint(apiMessages);
200
+ const b = rowsFingerprint(storeMessages);
201
+ if (a.length !== b.length) {
202
+ return [
203
+ {
204
+ type: 'API_STORE_LEN',
205
+ detail: `api=${a.length} store=${b.length}`,
206
+ },
207
+ ];
208
+ }
209
+ for (let i = Math.max(0, a.length - 8); i < a.length; i++) {
210
+ if (a[i] !== b[i]) {
211
+ return [
212
+ {
213
+ type: 'API_STORE_TAIL',
214
+ index: i,
215
+ detail: `tail mismatch at ${i}`,
216
+ },
217
+ ];
218
+ }
219
+ }
220
+ return [];
221
+ }
@@ -0,0 +1,3 @@
1
+ import type { ChatMessage } from './types.js';
2
+ /** Canonical lenta in store vs last socket `agent:messages` emit. */
3
+ export declare function isLentaDeliveryPending(agentId: string, storeMessages: ChatMessage[]): boolean;
@@ -0,0 +1,10 @@
1
+ import { getLastLentaEmit, messagesFingerprint } from './lenta-seq-journal.js';
2
+ /** Canonical lenta in store vs last socket `agent:messages` emit. */
3
+ export function isLentaDeliveryPending(agentId, storeMessages) {
4
+ if (!storeMessages.length)
5
+ return false;
6
+ const last = getLastLentaEmit(agentId);
7
+ if (!last)
8
+ return true;
9
+ return messagesFingerprint(storeMessages) !== last.messagesSig;
10
+ }
@@ -0,0 +1,48 @@
1
+ import type { ChatMessage } from './types.js';
2
+ export type LentaSeqReason = 'dom_overlay' | 'jsonl_live' | 'agents_history' | 'subscribe_refresh' | 'delivery_flush' | 'push_ready' | 'emit';
3
+ export type LentaSeqTransition = 'dom_to_jsonl' | 'overlay_grow' | 'overlay_shrink' | 'jsonl_grow' | 'none';
4
+ export interface LentaSeqEmitMeta {
5
+ reason: LentaSeqReason;
6
+ source: 'dom' | 'jsonl' | 'hybrid';
7
+ historyLen: number;
8
+ liveLen: number;
9
+ messagesLen: number;
10
+ append?: boolean;
11
+ }
12
+ export interface LentaSeqEntry {
13
+ at: number;
14
+ agentId: string;
15
+ seq: number;
16
+ prevSeq: number;
17
+ seqHeld: boolean;
18
+ reason: LentaSeqReason;
19
+ transition: LentaSeqTransition;
20
+ source: string;
21
+ historyLen: number;
22
+ liveLen: number;
23
+ messagesLen: number;
24
+ messagesSig: string;
25
+ tailPreview: string;
26
+ append?: boolean;
27
+ }
28
+ export declare function messagesFingerprint(messages: ChatMessage[]): string;
29
+ export declare function tailPreview(messages: ChatMessage[], n?: number): string;
30
+ export declare function detectSeqTransition(prev: {
31
+ historyLen: number;
32
+ liveLen: number;
33
+ source: string;
34
+ } | undefined, next: {
35
+ historyLen: number;
36
+ liveLen: number;
37
+ source: string;
38
+ }): LentaSeqTransition;
39
+ export declare function recordLentaSeqEmit(agentId: string, seq: number, prevSeq: number, seqHeld: boolean, messages: ChatMessage[], meta: LentaSeqEmitMeta): LentaSeqEntry;
40
+ export declare function getLastLentaEmit(agentId: string): {
41
+ seq: number;
42
+ messagesSig: string;
43
+ historyLen: number;
44
+ liveLen: number;
45
+ source: string;
46
+ } | undefined;
47
+ export declare function getLentaSeqJournal(agentId?: string): LentaSeqEntry[];
48
+ export declare function getLentaSeqLogPath(): string;
@@ -0,0 +1,109 @@
1
+ import { appendFileSync, mkdirSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ const RING_BY_AGENT = new Map();
5
+ const LAST_BY_AGENT = new Map();
6
+ const MAX_PER_AGENT = 80;
7
+ const LOG_DIR = join(homedir(), '.cursorconnect');
8
+ const LOG_FILE = join(LOG_DIR, 'lenta-seq.jsonl');
9
+ export function messagesFingerprint(messages) {
10
+ return messages
11
+ .map((m) => {
12
+ const t = String(m.text ?? '')
13
+ .replace(/\s+/g, ' ')
14
+ .trim()
15
+ .slice(0, 80);
16
+ return `${m.role}|${m.id ?? ''}|${t.length}|${t}`;
17
+ })
18
+ .join(';;');
19
+ }
20
+ export function tailPreview(messages, n = 3) {
21
+ return messages
22
+ .slice(-n)
23
+ .map((m) => `${m.role}:${(m.text ?? '').replace(/\s+/g, ' ').trim().slice(0, 48)}`)
24
+ .join(' | ');
25
+ }
26
+ export function detectSeqTransition(prev, next) {
27
+ if (!prev)
28
+ return 'none';
29
+ if (prev.liveLen > 0 && next.liveLen === 0 && next.historyLen >= prev.historyLen) {
30
+ return 'dom_to_jsonl';
31
+ }
32
+ if (next.liveLen > prev.liveLen && next.historyLen === prev.historyLen) {
33
+ return 'overlay_grow';
34
+ }
35
+ if (next.liveLen < prev.liveLen && next.historyLen >= prev.historyLen) {
36
+ return 'overlay_shrink';
37
+ }
38
+ if (next.historyLen > prev.historyLen)
39
+ return 'jsonl_grow';
40
+ return 'none';
41
+ }
42
+ export function recordLentaSeqEmit(agentId, seq, prevSeq, seqHeld, messages, meta) {
43
+ const prev = LAST_BY_AGENT.get(agentId);
44
+ const messagesSig = messagesFingerprint(messages);
45
+ const transition = detectSeqTransition(prev, {
46
+ historyLen: meta.historyLen,
47
+ liveLen: meta.liveLen,
48
+ source: meta.source,
49
+ });
50
+ const entry = {
51
+ at: Date.now(),
52
+ agentId,
53
+ seq,
54
+ prevSeq,
55
+ seqHeld,
56
+ reason: meta.reason,
57
+ transition,
58
+ source: meta.source,
59
+ historyLen: meta.historyLen,
60
+ liveLen: meta.liveLen,
61
+ messagesLen: meta.messagesLen,
62
+ messagesSig: messagesSig.slice(0, 400),
63
+ tailPreview: tailPreview(messages),
64
+ append: meta.append,
65
+ };
66
+ LAST_BY_AGENT.set(agentId, {
67
+ seq,
68
+ messagesSig,
69
+ historyLen: meta.historyLen,
70
+ liveLen: meta.liveLen,
71
+ source: meta.source,
72
+ });
73
+ let ring = RING_BY_AGENT.get(agentId);
74
+ if (!ring) {
75
+ ring = [];
76
+ RING_BY_AGENT.set(agentId, ring);
77
+ }
78
+ ring.push(entry);
79
+ while (ring.length > MAX_PER_AGENT)
80
+ ring.shift();
81
+ try {
82
+ mkdirSync(LOG_DIR, { recursive: true });
83
+ appendFileSync(LOG_FILE, `${JSON.stringify(entry)}\n`);
84
+ }
85
+ catch {
86
+ /* non-fatal */
87
+ }
88
+ if (process.env.LENTA_SEQ_ASSERT === '1') {
89
+ if (!seqHeld && prev && prev.messagesSig === messagesSig) {
90
+ console.warn(`[lenta-seq] seq bump without lenta change agent=${agentId.slice(0, 8)} seq ${prevSeq}→${seq} transition=${transition}`);
91
+ }
92
+ if (transition === 'dom_to_jsonl' && !seqHeld && prevSeq === seq) {
93
+ console.warn(`[lenta-seq] dom→jsonl handoff but seq held agent=${agentId.slice(0, 8)} seq=${seq}`);
94
+ }
95
+ }
96
+ return entry;
97
+ }
98
+ export function getLastLentaEmit(agentId) {
99
+ return LAST_BY_AGENT.get(agentId);
100
+ }
101
+ export function getLentaSeqJournal(agentId) {
102
+ if (!agentId) {
103
+ return [...RING_BY_AGENT.values()].flat().sort((a, b) => a.at - b.at).slice(-MAX_PER_AGENT);
104
+ }
105
+ return [...(RING_BY_AGENT.get(agentId) ?? [])];
106
+ }
107
+ export function getLentaSeqLogPath() {
108
+ return LOG_FILE;
109
+ }