@jhizzard/termdeck 0.3.0 → 0.3.2
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/package.json +1 -1
- package/packages/cli/src/index.js +8 -0
- package/packages/client/public/app.js +485 -0
- package/packages/client/public/style.css +337 -0
- package/packages/server/src/index.js +116 -3
- package/packages/server/src/mnestra-bridge/index.js +1 -1
- package/packages/server/src/preflight.js +373 -0
- package/packages/server/src/rag.js +41 -1
- package/packages/server/src/session.js +8 -1
- package/packages/server/src/transcripts.js +296 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// TermDeck Preflight Health Check
|
|
2
|
+
// Runs at startup to verify the entire memory stack is operational.
|
|
3
|
+
// Each check is independent — one failure does not block others.
|
|
4
|
+
//
|
|
5
|
+
// Exports:
|
|
6
|
+
// runPreflight(config) — run all checks, return result object
|
|
7
|
+
// createHealthHandler(config) — Express route handler for GET /api/health
|
|
8
|
+
|
|
9
|
+
const http = require('http');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
// Cache preflight results for 60s
|
|
15
|
+
let _cachedResult = null;
|
|
16
|
+
let _cachedAt = 0;
|
|
17
|
+
const CACHE_TTL_MS = 60_000;
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Individual checks
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
async function checkMnestra(config) {
|
|
24
|
+
const rag = config.rag || {};
|
|
25
|
+
const url = rag.mnestraWebhookUrl
|
|
26
|
+
? rag.mnestraWebhookUrl.replace(/\/mnestra\/?$/, '/health')
|
|
27
|
+
: 'http://localhost:37778/health';
|
|
28
|
+
|
|
29
|
+
const body = await httpGet(url, 3000);
|
|
30
|
+
const data = tryParseJSON(body);
|
|
31
|
+
const total = data && (data.total || data.memories || data.count);
|
|
32
|
+
if (total != null) {
|
|
33
|
+
return { name: 'mnestra_reachable', passed: true, detail: `${Number(total).toLocaleString()} memories` };
|
|
34
|
+
}
|
|
35
|
+
// Got 200 but no count — still reachable
|
|
36
|
+
return { name: 'mnestra_reachable', passed: true, detail: 'reachable (no memory count)' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function checkMnestraMemories(config) {
|
|
40
|
+
// If Mnestra responded with a count in the reachable check we can skip
|
|
41
|
+
// a second request — but since checks run independently we check the
|
|
42
|
+
// memory_status endpoint separately.
|
|
43
|
+
const rag = config.rag || {};
|
|
44
|
+
const baseUrl = rag.mnestraWebhookUrl
|
|
45
|
+
? rag.mnestraWebhookUrl.replace(/\/mnestra\/?$/, '')
|
|
46
|
+
: 'http://localhost:37778';
|
|
47
|
+
|
|
48
|
+
const body = await httpGet(`${baseUrl}/health`, 3000);
|
|
49
|
+
const data = tryParseJSON(body);
|
|
50
|
+
const total = data && (data.total || data.memories || data.count);
|
|
51
|
+
if (total != null && Number(total) > 0) {
|
|
52
|
+
return { name: 'mnestra_has_memories', passed: true, detail: `${Number(total).toLocaleString()} memories loaded` };
|
|
53
|
+
}
|
|
54
|
+
if (total != null && Number(total) === 0) {
|
|
55
|
+
return { name: 'mnestra_has_memories', passed: false, detail: 'Mnestra running but 0 memories — run `mnestra ingest`' };
|
|
56
|
+
}
|
|
57
|
+
return { name: 'mnestra_has_memories', passed: false, detail: 'could not determine memory count' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function checkRumen(config) {
|
|
61
|
+
// Try to query rumen_jobs table via DATABASE_URL for last successful job
|
|
62
|
+
const dbUrl = process.env.DATABASE_URL;
|
|
63
|
+
if (!dbUrl) {
|
|
64
|
+
return { name: 'rumen_recent', passed: false, detail: 'DATABASE_URL not set — cannot check Rumen jobs' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let pg;
|
|
68
|
+
try { pg = require('pg'); } catch (err) { pg = null; }
|
|
69
|
+
if (!pg) {
|
|
70
|
+
return { name: 'rumen_recent', passed: false, detail: 'pg module not installed' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const pool = new pg.Pool({
|
|
74
|
+
connectionString: dbUrl,
|
|
75
|
+
max: 1,
|
|
76
|
+
connectionTimeoutMillis: 5000,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const res = await pool.query(
|
|
81
|
+
`SELECT status, completed_at, insights_generated
|
|
82
|
+
FROM rumen_jobs
|
|
83
|
+
WHERE status = 'done'
|
|
84
|
+
ORDER BY completed_at DESC
|
|
85
|
+
LIMIT 1`
|
|
86
|
+
);
|
|
87
|
+
if (res.rows.length === 0) {
|
|
88
|
+
return { name: 'rumen_recent', passed: false, detail: 'no completed Rumen jobs found' };
|
|
89
|
+
}
|
|
90
|
+
const row = res.rows[0];
|
|
91
|
+
const completedAt = new Date(row.completed_at);
|
|
92
|
+
const agoMs = Date.now() - completedAt.getTime();
|
|
93
|
+
const agoMin = Math.round(agoMs / 60_000);
|
|
94
|
+
const insights = row.insights_generated || 0;
|
|
95
|
+
const recent = agoMs < 30 * 60_000; // within 30 minutes
|
|
96
|
+
return {
|
|
97
|
+
name: 'rumen_recent',
|
|
98
|
+
passed: recent,
|
|
99
|
+
detail: recent
|
|
100
|
+
? `last job ${agoMin}m ago, ${insights} insights`
|
|
101
|
+
: `last job ${agoMin}m ago (stale — expected within 30m), ${insights} insights`,
|
|
102
|
+
};
|
|
103
|
+
} finally {
|
|
104
|
+
await pool.end().catch(() => {});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function checkDatabase() {
|
|
109
|
+
const dbUrl = process.env.DATABASE_URL;
|
|
110
|
+
if (!dbUrl) {
|
|
111
|
+
return { name: 'database_url', passed: false, detail: 'DATABASE_URL not set' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let pg;
|
|
115
|
+
try { pg = require('pg'); } catch (err) { pg = null; }
|
|
116
|
+
if (!pg) {
|
|
117
|
+
return { name: 'database_url', passed: false, detail: 'pg module not installed' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const pool = new pg.Pool({
|
|
121
|
+
connectionString: dbUrl,
|
|
122
|
+
max: 1,
|
|
123
|
+
connectionTimeoutMillis: 5000,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const t0 = Date.now();
|
|
127
|
+
try {
|
|
128
|
+
const res = await pool.query('SELECT 1 AS ok');
|
|
129
|
+
const ms = Date.now() - t0;
|
|
130
|
+
if (res.rows[0] && res.rows[0].ok === 1) {
|
|
131
|
+
return { name: 'database_url', passed: true, detail: `connected in ${ms}ms` };
|
|
132
|
+
}
|
|
133
|
+
return { name: 'database_url', passed: false, detail: 'SELECT 1 returned unexpected result' };
|
|
134
|
+
} finally {
|
|
135
|
+
await pool.end().catch(() => {});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function checkProjectPaths(config) {
|
|
140
|
+
const projects = config.projects || {};
|
|
141
|
+
const names = Object.keys(projects);
|
|
142
|
+
if (names.length === 0) {
|
|
143
|
+
return { name: 'project_paths', passed: true, detail: 'no projects configured' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let ok = 0;
|
|
147
|
+
const missing = [];
|
|
148
|
+
for (const name of names) {
|
|
149
|
+
const p = projects[name];
|
|
150
|
+
const resolved = (p.path || '').replace(/^~/, os.homedir());
|
|
151
|
+
if (fs.existsSync(resolved)) {
|
|
152
|
+
ok++;
|
|
153
|
+
} else {
|
|
154
|
+
missing.push(name);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const total = names.length;
|
|
159
|
+
if (missing.length === 0) {
|
|
160
|
+
return { name: 'project_paths', passed: true, detail: `${ok}/${total} paths exist` };
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
name: 'project_paths',
|
|
164
|
+
passed: false,
|
|
165
|
+
detail: `${ok}/${total} paths exist — missing: ${missing.join(', ')}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function checkShellSanity() {
|
|
170
|
+
const shell = process.env.SHELL || '/bin/bash';
|
|
171
|
+
const shellName = path.basename(shell);
|
|
172
|
+
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
let ptyMod;
|
|
175
|
+
try { ptyMod = require('@homebridge/node-pty-prebuilt-multiarch'); } catch (err) { ptyMod = null; }
|
|
176
|
+
if (!ptyMod) {
|
|
177
|
+
try { ptyMod = require('node-pty'); } catch (err) { ptyMod = null; }
|
|
178
|
+
}
|
|
179
|
+
if (!ptyMod) {
|
|
180
|
+
resolve({ name: 'shell_sanity', passed: false, detail: 'node-pty not available' });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const t0 = Date.now();
|
|
185
|
+
let output = '';
|
|
186
|
+
let resolved = false;
|
|
187
|
+
|
|
188
|
+
const proc = ptyMod.spawn(shell, ['-l', '-c', 'echo TERMDECK_OK'], {
|
|
189
|
+
name: 'xterm-256color',
|
|
190
|
+
cols: 80,
|
|
191
|
+
rows: 24,
|
|
192
|
+
cwd: os.homedir(),
|
|
193
|
+
env: process.env,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
proc.onData((data) => {
|
|
197
|
+
output += data;
|
|
198
|
+
if (output.includes('TERMDECK_OK') && !resolved) {
|
|
199
|
+
resolved = true;
|
|
200
|
+
const ms = ((Date.now() - t0) / 1000).toFixed(1);
|
|
201
|
+
proc.kill();
|
|
202
|
+
resolve({ name: 'shell_sanity', passed: true, detail: `${shellName} OK in ${ms}s` });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
proc.onExit(({ exitCode }) => {
|
|
207
|
+
if (!resolved) {
|
|
208
|
+
resolved = true;
|
|
209
|
+
const ms = ((Date.now() - t0) / 1000).toFixed(1);
|
|
210
|
+
resolve({
|
|
211
|
+
name: 'shell_sanity',
|
|
212
|
+
passed: false,
|
|
213
|
+
detail: `${shellName} exited ${exitCode} after ${ms}s without OK`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// 3s timeout
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
if (!resolved) {
|
|
221
|
+
resolved = true;
|
|
222
|
+
try { proc.kill(); } catch (err) { /* cleanup — process may already be dead */ }
|
|
223
|
+
resolve({ name: 'shell_sanity', passed: false, detail: `${shellName} timed out after 3s` });
|
|
224
|
+
}
|
|
225
|
+
}, 3000);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// HTTP helper (no external dependencies — uses built-in http)
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
function httpGet(url, timeoutMs) {
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const req = http.get(url, { timeout: timeoutMs }, (res) => {
|
|
236
|
+
if (res.statusCode !== 200) {
|
|
237
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
238
|
+
res.resume();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
let body = '';
|
|
242
|
+
res.setEncoding('utf8');
|
|
243
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
244
|
+
res.on('end', () => resolve(body));
|
|
245
|
+
});
|
|
246
|
+
req.on('error', reject);
|
|
247
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function tryParseJSON(str) {
|
|
252
|
+
try { return JSON.parse(str); } catch (err) { return null; }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Main preflight runner
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
async function runPreflight(config) {
|
|
260
|
+
const checks = await Promise.all([
|
|
261
|
+
checkMnestra(config).catch((err) => ({
|
|
262
|
+
name: 'mnestra_reachable', passed: false,
|
|
263
|
+
detail: `unreachable — ${err.message}. Start with \`mnestra serve\``,
|
|
264
|
+
})),
|
|
265
|
+
checkMnestraMemories(config).catch((err) => ({
|
|
266
|
+
name: 'mnestra_has_memories', passed: false,
|
|
267
|
+
detail: `check failed — ${err.message}`,
|
|
268
|
+
})),
|
|
269
|
+
checkRumen(config).catch((err) => ({
|
|
270
|
+
name: 'rumen_recent', passed: false,
|
|
271
|
+
detail: `check failed — ${err.message}`,
|
|
272
|
+
})),
|
|
273
|
+
checkDatabase().catch((err) => ({
|
|
274
|
+
name: 'database_url', passed: false,
|
|
275
|
+
detail: `connection failed — ${err.message}`,
|
|
276
|
+
})),
|
|
277
|
+
checkProjectPaths(config).catch((err) => ({
|
|
278
|
+
name: 'project_paths', passed: false,
|
|
279
|
+
detail: `check failed — ${err.message}`,
|
|
280
|
+
})),
|
|
281
|
+
checkShellSanity().catch((err) => ({
|
|
282
|
+
name: 'shell_sanity', passed: false,
|
|
283
|
+
detail: `check failed — ${err.message}`,
|
|
284
|
+
})),
|
|
285
|
+
]);
|
|
286
|
+
|
|
287
|
+
const result = {
|
|
288
|
+
passed: checks.every((c) => c.passed),
|
|
289
|
+
checks,
|
|
290
|
+
timestamp: new Date().toISOString(),
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
_cachedResult = result;
|
|
294
|
+
_cachedAt = Date.now();
|
|
295
|
+
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// Express route handler factory (GET /api/health)
|
|
301
|
+
//
|
|
302
|
+
// T3 or index.js wires this into the app:
|
|
303
|
+
// const { createHealthHandler } = require('./preflight');
|
|
304
|
+
// app.get('/api/health', createHealthHandler(config));
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
function createHealthHandler(config) {
|
|
308
|
+
return async (_req, res) => {
|
|
309
|
+
// Return cached result if fresh
|
|
310
|
+
if (_cachedResult && (Date.now() - _cachedAt) < CACHE_TTL_MS) {
|
|
311
|
+
return res.json(_cachedResult);
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const result = await runPreflight(config);
|
|
315
|
+
res.json(result);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
res.status(500).json({ error: err.message });
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// CLI banner printer
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
const REMEDIATION = {
|
|
327
|
+
mnestra_reachable: 'Start Mnestra with `mnestra serve`',
|
|
328
|
+
mnestra_has_memories: 'Run `mnestra ingest` to populate the memory store',
|
|
329
|
+
rumen_recent: 'Check Rumen Edge Function deployment or run `termdeck init --rumen`',
|
|
330
|
+
database_url: 'Set DATABASE_URL in ~/.termdeck/secrets.env',
|
|
331
|
+
project_paths: 'Fix paths in ~/.termdeck/config.yaml → projects',
|
|
332
|
+
shell_sanity: 'Check $SHELL and your login profile (~/.zshrc or ~/.bashrc)',
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const CHECK_LABELS = {
|
|
336
|
+
mnestra_reachable: 'Mnestra',
|
|
337
|
+
mnestra_has_memories: 'Mnestra data',
|
|
338
|
+
rumen_recent: 'Rumen',
|
|
339
|
+
database_url: 'Database',
|
|
340
|
+
project_paths: 'Project paths',
|
|
341
|
+
shell_sanity: 'Shell',
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
function printHealthBanner(result) {
|
|
345
|
+
const green = '\x1b[32m';
|
|
346
|
+
const red = '\x1b[31m';
|
|
347
|
+
const dim = '\x1b[2m';
|
|
348
|
+
const reset = '\x1b[0m';
|
|
349
|
+
const bold = '\x1b[1m';
|
|
350
|
+
|
|
351
|
+
for (const check of result.checks) {
|
|
352
|
+
const label = (CHECK_LABELS[check.name] || check.name).padEnd(18, ' ');
|
|
353
|
+
const dots = '.'.repeat(Math.max(1, 20 - label.length));
|
|
354
|
+
if (check.passed) {
|
|
355
|
+
console.log(` ${green}✓${reset} ${dim}[health]${reset} ${label}${dim}${dots}${reset} ${green}OK${reset} ${dim}(${check.detail})${reset}`);
|
|
356
|
+
} else {
|
|
357
|
+
console.log(` ${red}✗${reset} ${dim}[health]${reset} ${label}${dim}${dots}${reset} ${red}FAIL${reset} ${dim}(${check.detail})${reset}`);
|
|
358
|
+
const hint = REMEDIATION[check.name];
|
|
359
|
+
if (hint) {
|
|
360
|
+
console.log(` ${dim}→ ${hint}${reset}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const failCount = result.checks.filter((c) => !c.passed).length;
|
|
366
|
+
if (failCount === 0) {
|
|
367
|
+
console.log(`\n ${green}${bold}All ${result.checks.length} health checks passed.${reset}\n`);
|
|
368
|
+
} else {
|
|
369
|
+
console.log(`\n ${red}${bold}${failCount}/${result.checks.length} health checks failed.${reset} TermDeck will still run, but memory features may be degraded.\n`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
module.exports = { runPreflight, createHealthHandler, printHealthBanner };
|
|
@@ -22,6 +22,10 @@ class RAGIntegration {
|
|
|
22
22
|
commandLog: config.rag?.tables?.commands || 'mnestra_commands'
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
// Circuit breaker: track consecutive 404s per table name.
|
|
26
|
+
// After 3 consecutive 404s, disable pushes to that table until restart.
|
|
27
|
+
this._circuitBreaker = new Map(); // table -> { count: number, open: boolean }
|
|
28
|
+
|
|
25
29
|
if (this.enabled) {
|
|
26
30
|
this._startSync();
|
|
27
31
|
}
|
|
@@ -95,6 +99,33 @@ class RAGIntegration {
|
|
|
95
99
|
}, session.meta.project);
|
|
96
100
|
}
|
|
97
101
|
|
|
102
|
+
// Circuit breaker check — returns true if pushes to this table are disabled
|
|
103
|
+
_isCircuitOpen(table) {
|
|
104
|
+
const state = this._circuitBreaker.get(table);
|
|
105
|
+
return !!(state && state.open);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Record a 404 for a table; opens the breaker after 3 consecutive hits
|
|
109
|
+
_record404(table) {
|
|
110
|
+
let state = this._circuitBreaker.get(table);
|
|
111
|
+
if (!state) {
|
|
112
|
+
state = { count: 0, open: false };
|
|
113
|
+
this._circuitBreaker.set(table, state);
|
|
114
|
+
}
|
|
115
|
+
state.count += 1;
|
|
116
|
+
if (state.count >= 3 && !state.open) {
|
|
117
|
+
state.open = true;
|
|
118
|
+
console.error(`[rag] circuit breaker open for ${table} — 3 consecutive 404s, disabling pushes until server restart`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Reset the breaker for a table on successful push
|
|
123
|
+
_resetCircuit(table) {
|
|
124
|
+
if (this._circuitBreaker.has(table)) {
|
|
125
|
+
this._circuitBreaker.delete(table);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
98
129
|
// Push a single event to Supabase
|
|
99
130
|
async _pushEvent(event) {
|
|
100
131
|
if (!this.enabled) return;
|
|
@@ -102,6 +133,9 @@ class RAGIntegration {
|
|
|
102
133
|
const layer = this._determineLayer(event);
|
|
103
134
|
const table = this.tables[layer];
|
|
104
135
|
|
|
136
|
+
// Skip if circuit breaker is open for this table
|
|
137
|
+
if (this._isCircuitOpen(table)) return;
|
|
138
|
+
|
|
105
139
|
try {
|
|
106
140
|
const response = await fetch(`${this.supabaseUrl}/rest/v1/${table}`, {
|
|
107
141
|
method: 'POST',
|
|
@@ -122,11 +156,17 @@ class RAGIntegration {
|
|
|
122
156
|
});
|
|
123
157
|
|
|
124
158
|
if (!response.ok) {
|
|
159
|
+
if (response.status === 404) {
|
|
160
|
+
this._record404(table);
|
|
161
|
+
}
|
|
125
162
|
throw new Error(`Supabase responded ${response.status}`);
|
|
126
163
|
}
|
|
164
|
+
|
|
165
|
+
// Success — reset any accumulated 404 count for this table
|
|
166
|
+
this._resetCircuit(table);
|
|
127
167
|
} catch (err) {
|
|
128
|
-
// Will be retried by sync loop
|
|
129
168
|
console.error('[mnestra] Push failed:', err.message);
|
|
169
|
+
throw err; // Propagate to caller so sync loop knows this event failed
|
|
130
170
|
}
|
|
131
171
|
}
|
|
132
172
|
|
|
@@ -93,6 +93,9 @@ class Session {
|
|
|
93
93
|
ragEvents: [] // buffer before flush
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
+
// Transcript chunk counter — monotonic per session for deterministic replay
|
|
97
|
+
this.transcriptChunkIndex = 0;
|
|
98
|
+
|
|
96
99
|
// Output analysis state
|
|
97
100
|
this._outputBuffer = '';
|
|
98
101
|
this._outputFlushTimer = null;
|
|
@@ -283,6 +286,10 @@ class Session {
|
|
|
283
286
|
}
|
|
284
287
|
}
|
|
285
288
|
|
|
289
|
+
getNextChunkIndex() {
|
|
290
|
+
return this.transcriptChunkIndex++;
|
|
291
|
+
}
|
|
292
|
+
|
|
286
293
|
_detectErrors(clean) {
|
|
287
294
|
if (!PATTERNS.error.test(clean)) return;
|
|
288
295
|
|
|
@@ -293,7 +300,7 @@ class Session {
|
|
|
293
300
|
// Mirror status-change callback so T1 sees 'errored' in status_broadcast without
|
|
294
301
|
// waiting for the 3s debounce.
|
|
295
302
|
if (oldStatus !== 'errored' && this.onStatusChange) {
|
|
296
|
-
try { this.onStatusChange(this, oldStatus, 'errored'); } catch {}
|
|
303
|
+
try { this.onStatusChange(this, oldStatus, 'errored'); } catch (err) { console.error('[pty] onStatusChange error:', err.message); }
|
|
297
304
|
}
|
|
298
305
|
|
|
299
306
|
// Server-side rate limit: at most one error_detected event every 30s per session
|