@jhizzard/termdeck 0.11.0 → 0.13.0
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 +4 -2
- package/packages/cli/src/init-rumen.js +95 -54
- package/packages/client/public/app.js +17 -4
- package/packages/client/public/flashback-history.html +331 -0
- package/packages/client/public/flashback-history.js +258 -0
- package/packages/client/public/graph-controls.js +217 -0
- package/packages/client/public/graph.html +36 -0
- package/packages/client/public/graph.js +131 -15
- package/packages/client/public/index.html +1 -0
- package/packages/client/public/style.css +55 -0
- package/packages/server/src/agent-adapters/claude.js +158 -0
- package/packages/server/src/agent-adapters/index.js +55 -0
- package/packages/server/src/database.js +49 -1
- package/packages/server/src/flashback-diag.js +187 -13
- package/packages/server/src/index.js +58 -2
- package/packages/server/src/session.js +62 -31
- package/packages/server/src/setup/migrations.js +44 -4
- package/packages/server/src/setup/rumen/functions/graph-inference/index.ts +381 -0
- package/packages/server/src/setup/rumen/functions/graph-inference/tsconfig.json +14 -0
|
@@ -1,15 +1,37 @@
|
|
|
1
|
-
// Flashback diagnostic ring buffer (Sprint 39 T1)
|
|
1
|
+
// Flashback diagnostic ring buffer (Sprint 39 T1) + durable audit table
|
|
2
|
+
// (Sprint 43 T2).
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
// here so production-flow regressions surface as a readable timeline instead
|
|
5
|
-
// of a silent gate failure. The ring is in-memory and lost on restart by
|
|
6
|
-
// design — persistence is a Sprint-40+ concern. Public surface:
|
|
4
|
+
// Two layers of observability for the Flashback pipeline:
|
|
7
5
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
6
|
+
// (1) IN-MEMORY RING — six decision points along the pipeline write
|
|
7
|
+
// structured events to a 200-event ring. Lost on restart. Powers the
|
|
8
|
+
// /api/flashback/diag endpoint and the live diagnostic UI. This is
|
|
9
|
+
// fine-grained: every pattern match, every rate-limit hit, every
|
|
10
|
+
// bridge query gets logged.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
12
|
+
// (2) SQLITE AUDIT TABLE (flashback_events) — every actual fire (the
|
|
13
|
+
// moment a proactive_memory frame is sent over WS to the user's
|
|
14
|
+
// panel) gets one durable row. Survives restart. Powers the
|
|
15
|
+
// /flashback-history.html dashboard and the click-through funnel.
|
|
16
|
+
// This is coarse-grained: one row per fire, plus dismiss/click-through
|
|
17
|
+
// outcome.
|
|
18
|
+
//
|
|
19
|
+
// Public surface:
|
|
20
|
+
//
|
|
21
|
+
// In-memory ring (Sprint 39):
|
|
22
|
+
// log({ sessionId, event, ...fields }) — append one event
|
|
23
|
+
// snapshot({ sessionId?, eventType?, limit? }) — read back filtered tail
|
|
24
|
+
// _resetForTest() — test-only ring clear
|
|
25
|
+
//
|
|
26
|
+
// SQLite audit (Sprint 43 T2):
|
|
27
|
+
// recordFlashback(db, { sessionId, project, error_text, hits_count,
|
|
28
|
+
// top_hit_id, top_hit_score, fired_at? }) → id
|
|
29
|
+
// markDismissed(db, eventId, dismissedAt?) → bool
|
|
30
|
+
// markClickedThrough(db, eventId) → bool
|
|
31
|
+
// getRecentFlashbacks(db, { since?, limit? }) → row[]
|
|
32
|
+
// getFunnelStats(db, { since? }) → { fires, dismissed, clicked_through }
|
|
33
|
+
//
|
|
34
|
+
// Event shape (ring): { ts, sessionId, event, ...event-specific fields }.
|
|
13
35
|
//
|
|
14
36
|
// Event types and their producers:
|
|
15
37
|
// pattern_match — session.js _detectErrors (PATTERNS.error /
|
|
@@ -21,9 +43,13 @@
|
|
|
21
43
|
// bridge_result — mnestra-bridge queryMnestra at call return
|
|
22
44
|
// proactive_memory_emit — index.js onErrorDetected WS send block
|
|
23
45
|
//
|
|
24
|
-
// The
|
|
25
|
-
//
|
|
26
|
-
//
|
|
46
|
+
// The audit table is an EXTENSION of the ring, not a replacement. Ring stays
|
|
47
|
+
// for the live UI; SQLite is for the historical question "did flashback fire
|
|
48
|
+
// when I needed it, and did I act on it?"
|
|
49
|
+
//
|
|
50
|
+
// SQLite functions are SAFE when db is null/undefined: they no-op and return
|
|
51
|
+
// null/false/[] so test fixtures and Database-unavailable installs don't
|
|
52
|
+
// break the live emit path.
|
|
27
53
|
|
|
28
54
|
const RING_SIZE = 200;
|
|
29
55
|
|
|
@@ -48,4 +74,152 @@ function _resetForTest() {
|
|
|
48
74
|
ring = [];
|
|
49
75
|
}
|
|
50
76
|
|
|
51
|
-
|
|
77
|
+
// ---- SQLite audit (Sprint 43 T2) ----------------------------------------
|
|
78
|
+
|
|
79
|
+
// Persists one row per actual flashback fire. Returns the inserted row id
|
|
80
|
+
// (number) or null when persistence is unavailable. Errors are caught and
|
|
81
|
+
// logged — flashback persistence must never break the live emit path.
|
|
82
|
+
function recordFlashback(db, event) {
|
|
83
|
+
if (!db) return null;
|
|
84
|
+
if (!event || (!event.sessionId && !event.session_id)) return null;
|
|
85
|
+
try {
|
|
86
|
+
const fired_at = event.fired_at || new Date().toISOString();
|
|
87
|
+
const session_id = event.session_id || event.sessionId;
|
|
88
|
+
const hits_count = Number.isFinite(event.hits_count) ? event.hits_count : 0;
|
|
89
|
+
const top_hit_score = (typeof event.top_hit_score === 'number'
|
|
90
|
+
&& Number.isFinite(event.top_hit_score)) ? event.top_hit_score : null;
|
|
91
|
+
const result = db.prepare(`
|
|
92
|
+
INSERT INTO flashback_events
|
|
93
|
+
(fired_at, session_id, project, error_text, hits_count,
|
|
94
|
+
top_hit_id, top_hit_score)
|
|
95
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
96
|
+
`).run(
|
|
97
|
+
fired_at,
|
|
98
|
+
session_id,
|
|
99
|
+
event.project || null,
|
|
100
|
+
event.error_text || '',
|
|
101
|
+
hits_count,
|
|
102
|
+
event.top_hit_id || null,
|
|
103
|
+
top_hit_score,
|
|
104
|
+
);
|
|
105
|
+
// better-sqlite3 returns BigInt for lastInsertRowid; coerce to Number
|
|
106
|
+
// so it serializes naturally into JSON and the WS frame.
|
|
107
|
+
return Number(result.lastInsertRowid);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.warn('[flashback-diag] recordFlashback INSERT failed:', err.message);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Marks an event as dismissed (toast went away — by user, by 30s timeout,
|
|
115
|
+
// or implicitly via click-through). Idempotent: only writes when
|
|
116
|
+
// dismissed_at is currently NULL, so the FIRST dismiss wins. Returns true
|
|
117
|
+
// when a row was actually updated.
|
|
118
|
+
function markDismissed(db, eventId, dismissedAt) {
|
|
119
|
+
if (!db || !eventId) return false;
|
|
120
|
+
const id = Number(eventId);
|
|
121
|
+
if (!Number.isFinite(id) || id <= 0) return false;
|
|
122
|
+
try {
|
|
123
|
+
const ts = dismissedAt || new Date().toISOString();
|
|
124
|
+
const result = db.prepare(`
|
|
125
|
+
UPDATE flashback_events
|
|
126
|
+
SET dismissed_at = ?
|
|
127
|
+
WHERE id = ? AND dismissed_at IS NULL
|
|
128
|
+
`).run(ts, id);
|
|
129
|
+
return result.changes > 0;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.warn('[flashback-diag] markDismissed UPDATE failed:', err.message);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Marks an event as clicked-through (user opened the modal). Click-through
|
|
137
|
+
// is also an implicit dismiss, so if dismissed_at is still NULL we set it
|
|
138
|
+
// at the same moment. Idempotent: clicking twice is a no-op on the second
|
|
139
|
+
// pass. Returns true when a row was actually updated.
|
|
140
|
+
function markClickedThrough(db, eventId) {
|
|
141
|
+
if (!db || !eventId) return false;
|
|
142
|
+
const id = Number(eventId);
|
|
143
|
+
if (!Number.isFinite(id) || id <= 0) return false;
|
|
144
|
+
try {
|
|
145
|
+
const ts = new Date().toISOString();
|
|
146
|
+
const result = db.prepare(`
|
|
147
|
+
UPDATE flashback_events
|
|
148
|
+
SET clicked_through = 1,
|
|
149
|
+
dismissed_at = COALESCE(dismissed_at, ?)
|
|
150
|
+
WHERE id = ? AND clicked_through = 0
|
|
151
|
+
`).run(ts, id);
|
|
152
|
+
return result.changes > 0;
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.warn('[flashback-diag] markClickedThrough UPDATE failed:', err.message);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Reads the most-recent N flashback fires, optionally filtered to events
|
|
160
|
+
// fired at-or-after the `since` ISO timestamp. Hard cap of 500 rows so
|
|
161
|
+
// pathological queries can't OOM the dashboard.
|
|
162
|
+
function getRecentFlashbacks(db, { since, limit } = {}) {
|
|
163
|
+
if (!db) return [];
|
|
164
|
+
try {
|
|
165
|
+
const cap = Math.max(1, Math.min(500, Number(limit) || 100));
|
|
166
|
+
const cols = `id, fired_at, session_id, project, error_text, hits_count,
|
|
167
|
+
top_hit_id, top_hit_score, dismissed_at, clicked_through`;
|
|
168
|
+
if (since) {
|
|
169
|
+
return db.prepare(
|
|
170
|
+
`SELECT ${cols} FROM flashback_events
|
|
171
|
+
WHERE fired_at >= ?
|
|
172
|
+
ORDER BY fired_at DESC
|
|
173
|
+
LIMIT ?`
|
|
174
|
+
).all(since, cap);
|
|
175
|
+
}
|
|
176
|
+
return db.prepare(
|
|
177
|
+
`SELECT ${cols} FROM flashback_events
|
|
178
|
+
ORDER BY fired_at DESC
|
|
179
|
+
LIMIT ?`
|
|
180
|
+
).all(cap);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.warn('[flashback-diag] getRecentFlashbacks SELECT failed:', err.message);
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Click-through funnel aggregates: total fires, dismissed (any reason),
|
|
188
|
+
// clicked-through (modal opened). Optional `since` ISO timestamp filter.
|
|
189
|
+
// All three are scalar counts — the dashboard renders them as a percentage
|
|
190
|
+
// funnel chart.
|
|
191
|
+
function getFunnelStats(db, { since } = {}) {
|
|
192
|
+
const empty = { fires: 0, dismissed: 0, clicked_through: 0 };
|
|
193
|
+
if (!db) return empty;
|
|
194
|
+
try {
|
|
195
|
+
const where = since ? `WHERE fired_at >= ?` : '';
|
|
196
|
+
const args = since ? [since] : [];
|
|
197
|
+
const row = db.prepare(
|
|
198
|
+
`SELECT
|
|
199
|
+
COUNT(*) AS fires,
|
|
200
|
+
SUM(CASE WHEN dismissed_at IS NOT NULL THEN 1 ELSE 0 END) AS dismissed,
|
|
201
|
+
SUM(CASE WHEN clicked_through = 1 THEN 1 ELSE 0 END) AS clicked_through
|
|
202
|
+
FROM flashback_events ${where}`
|
|
203
|
+
).get(...args);
|
|
204
|
+
return {
|
|
205
|
+
fires: Number(row?.fires || 0),
|
|
206
|
+
dismissed: Number(row?.dismissed || 0),
|
|
207
|
+
clicked_through: Number(row?.clicked_through || 0),
|
|
208
|
+
};
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.warn('[flashback-diag] getFunnelStats SELECT failed:', err.message);
|
|
211
|
+
return empty;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = {
|
|
216
|
+
log,
|
|
217
|
+
snapshot,
|
|
218
|
+
_resetForTest,
|
|
219
|
+
RING_SIZE,
|
|
220
|
+
recordFlashback,
|
|
221
|
+
markDismissed,
|
|
222
|
+
markClickedThrough,
|
|
223
|
+
getRecentFlashbacks,
|
|
224
|
+
getFunnelStats,
|
|
225
|
+
};
|
|
@@ -911,10 +911,22 @@ function createServer(config) {
|
|
|
911
911
|
return;
|
|
912
912
|
}
|
|
913
913
|
if (sess.ws && sess.ws.readyState === 1) {
|
|
914
|
-
|
|
914
|
+
// Sprint 43 T2: persist the fire to flashback_events BEFORE
|
|
915
|
+
// serializing the WS frame so we can include the row id. The
|
|
916
|
+
// client uses flashback_event_id to POST dismiss/click-through
|
|
917
|
+
// updates back to the audit dashboard.
|
|
918
|
+
const flashback_event_id = flashbackDiag.recordFlashback(db, {
|
|
919
|
+
sessionId: sess.id,
|
|
920
|
+
project: sess.meta.project || null,
|
|
921
|
+
error_text: question,
|
|
922
|
+
hits_count: count,
|
|
923
|
+
top_hit_id: hit.id || null,
|
|
924
|
+
top_hit_score: typeof hit.similarity === 'number' ? hit.similarity : null,
|
|
925
|
+
});
|
|
926
|
+
const frame = JSON.stringify({ type: 'proactive_memory', hit, flashback_event_id });
|
|
915
927
|
try {
|
|
916
928
|
sess.ws.send(frame);
|
|
917
|
-
console.log(`[flashback] proactive_memory sent to session ${sess.id} (source_type=${hit.source_type}, project=${hit.project})`);
|
|
929
|
+
console.log(`[flashback] proactive_memory sent to session ${sess.id} (source_type=${hit.source_type}, project=${hit.project}, event_id=${flashback_event_id})`);
|
|
918
930
|
flashbackDiag.log({
|
|
919
931
|
sessionId: sess.id,
|
|
920
932
|
event: 'proactive_memory_emit',
|
|
@@ -922,6 +934,7 @@ function createServer(config) {
|
|
|
922
934
|
frame_size_bytes: Buffer.byteLength(frame, 'utf8'),
|
|
923
935
|
result_count_in_frame: 1,
|
|
924
936
|
outcome: 'emitted',
|
|
937
|
+
flashback_event_id,
|
|
925
938
|
});
|
|
926
939
|
} catch (err) {
|
|
927
940
|
console.error('[flashback] proactive_memory send failed:', err);
|
|
@@ -1442,6 +1455,49 @@ function createServer(config) {
|
|
|
1442
1455
|
res.json({ count: events.length, events });
|
|
1443
1456
|
});
|
|
1444
1457
|
|
|
1458
|
+
// GET /api/flashback/history - Sprint 43 T2 durable audit dashboard.
|
|
1459
|
+
// Returns the most-recent flashback fires from SQLite (survives restart)
|
|
1460
|
+
// plus the click-through funnel aggregate. The dashboard uses one fetch
|
|
1461
|
+
// for both so it can render the table and the funnel in lockstep.
|
|
1462
|
+
// Optional filters: ?since=<ISO8601>, ?limit=N (default 100, max 500).
|
|
1463
|
+
app.get('/api/flashback/history', (req, res) => {
|
|
1464
|
+
const rawSince = req.query && req.query.since;
|
|
1465
|
+
const since = (typeof rawSince === 'string' && rawSince.length) ? rawSince : undefined;
|
|
1466
|
+
const rawLimit = req.query && req.query.limit;
|
|
1467
|
+
const limit = rawLimit != null ? parseInt(rawLimit, 10) : undefined;
|
|
1468
|
+
const events = flashbackDiag.getRecentFlashbacks(db, {
|
|
1469
|
+
since,
|
|
1470
|
+
limit: Number.isFinite(limit) && limit > 0 ? limit : undefined,
|
|
1471
|
+
});
|
|
1472
|
+
const funnel = flashbackDiag.getFunnelStats(db, { since });
|
|
1473
|
+
res.json({ count: events.length, events, funnel });
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
// POST /api/flashback/:id/dismissed - mark a flashback toast as dismissed.
|
|
1477
|
+
// Called by the client when the user clicks ×, presses Escape, lets the
|
|
1478
|
+
// 30s auto-timer fire, OR clicks "Not relevant" / "Dismiss" in the modal.
|
|
1479
|
+
// Idempotent: subsequent calls are no-ops (first dismiss timestamp wins).
|
|
1480
|
+
app.post('/api/flashback/:id/dismissed', (req, res) => {
|
|
1481
|
+
const id = parseInt(req.params.id, 10);
|
|
1482
|
+
if (!Number.isFinite(id) || id <= 0) {
|
|
1483
|
+
return res.status(400).json({ error: 'Invalid id' });
|
|
1484
|
+
}
|
|
1485
|
+
const updated = flashbackDiag.markDismissed(db, id);
|
|
1486
|
+
res.json({ ok: true, updated });
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
// POST /api/flashback/:id/clicked - mark a flashback toast as clicked-
|
|
1490
|
+
// through (user opened the modal). Click-through is also an implicit
|
|
1491
|
+
// dismiss, so this updates dismissed_at if it's still NULL. Idempotent.
|
|
1492
|
+
app.post('/api/flashback/:id/clicked', (req, res) => {
|
|
1493
|
+
const id = parseInt(req.params.id, 10);
|
|
1494
|
+
if (!Number.isFinite(id) || id <= 0) {
|
|
1495
|
+
return res.status(400).json({ error: 'Invalid id' });
|
|
1496
|
+
}
|
|
1497
|
+
const updated = flashbackDiag.markClickedThrough(db, id);
|
|
1498
|
+
res.json({ ok: true, updated });
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1445
1501
|
// GET /api/pty-reaper/status — Sprint 42 T2 observability surface.
|
|
1446
1502
|
// Returns the live registry (per-session PTY pid + tracked descendants) and
|
|
1447
1503
|
// the reaped-history ring buffer so heavy-use installs can tell whether the
|
|
@@ -15,6 +15,8 @@ const os = require('os');
|
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { resolveTheme } = require('./theme-resolver');
|
|
17
17
|
const flashbackDiag = require('./flashback-diag');
|
|
18
|
+
const claudeAdapter = require('./agent-adapters/claude');
|
|
19
|
+
const { detectAdapter, getAdapterForSessionType } = require('./agent-adapters');
|
|
18
20
|
|
|
19
21
|
// Strip ANSI escape codes for pattern matching
|
|
20
22
|
function stripAnsi(str) {
|
|
@@ -25,14 +27,22 @@ function stripAnsi(str) {
|
|
|
25
27
|
.replace(/\x1b[>=<]/g, ''); // Keypad/cursor modes
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
// Pattern matchers for detecting terminal type and status
|
|
30
|
+
// Pattern matchers for detecting terminal type and status.
|
|
31
|
+
//
|
|
32
|
+
// Sprint 44 T3: claudeCode patterns are owned by the Claude adapter at
|
|
33
|
+
// ./agent-adapters/claude.js. This object continues to expose them under
|
|
34
|
+
// the legacy `PATTERNS.claudeCode.*` shape so external callers
|
|
35
|
+
// (tests/rcfile-noise.test.js, tests/analyzer-error-fixtures.test.js, the
|
|
36
|
+
// rcfile-noise analyze.js fixture script) keep working without import
|
|
37
|
+
// changes. Sprint 45 T4 removes this shim — new code should consume the
|
|
38
|
+
// adapter directly via require('./agent-adapters/claude').
|
|
29
39
|
const PATTERNS = {
|
|
30
40
|
claudeCode: {
|
|
31
|
-
prompt:
|
|
32
|
-
thinking:
|
|
33
|
-
editing:
|
|
34
|
-
tool:
|
|
35
|
-
idle:
|
|
41
|
+
prompt: claudeAdapter.patterns.prompt,
|
|
42
|
+
thinking: claudeAdapter.patterns.thinking,
|
|
43
|
+
editing: claudeAdapter.patterns.editing,
|
|
44
|
+
tool: claudeAdapter.patterns.tool,
|
|
45
|
+
idle: claudeAdapter.patterns.idle
|
|
36
46
|
},
|
|
37
47
|
geminiCli: {
|
|
38
48
|
prompt: /^gemini>\s/m,
|
|
@@ -86,7 +96,11 @@ const PATTERNS = {
|
|
|
86
96
|
// Sprint 40 T2: added mixed-case `Fatal` (mirrors `fatal` / `FATAL`) and
|
|
87
97
|
// the `npm ERR!` shape (special-cased outside the alternation because
|
|
88
98
|
// `!` is not a word character so `\b` after `npm ERR!` doesn't match).
|
|
89
|
-
|
|
99
|
+
// Sprint 44 T3: this regex is now owned by the Claude adapter
|
|
100
|
+
// (./agent-adapters/claude.js patterns.error). The shim below preserves
|
|
101
|
+
// the legacy PATTERNS.errorLineStart export — same regex object, so any
|
|
102
|
+
// existing reference equality (e.g. `=== PATTERNS.errorLineStart`) holds.
|
|
103
|
+
errorLineStart: claudeAdapter.patterns.error,
|
|
90
104
|
// Sprint 33: PATTERNS.error misses the most common Unix shell errors —
|
|
91
105
|
// `cat: /foo: No such file or directory`, `bash: foo: command not found`,
|
|
92
106
|
// `rm: cannot remove ...: Permission denied`. These have a colon-prefix
|
|
@@ -241,9 +255,18 @@ class Session {
|
|
|
241
255
|
}
|
|
242
256
|
|
|
243
257
|
_detectType(data) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
258
|
+
// Sprint 44 T3: registry-aware detection. detectAdapter() iterates
|
|
259
|
+
// AGENT_ADAPTERS in declaration order and returns the first hit by
|
|
260
|
+
// prompt regex OR command-string match. Sprint 44 lands Claude only
|
|
261
|
+
// (so this returns the Claude adapter or undefined); Sprint 45 adds
|
|
262
|
+
// Codex / Gemini / Grok adapters and the gemini fall-through below
|
|
263
|
+
// moves into gemini.js.
|
|
264
|
+
const adapter = detectAdapter(data, this.meta.command);
|
|
265
|
+
if (adapter) {
|
|
266
|
+
this.meta.type = adapter.sessionType;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (PATTERNS.geminiCli.prompt.test(data) || /gemini/i.test(this.meta.command)) {
|
|
247
270
|
this.meta.type = 'gemini';
|
|
248
271
|
} else if (
|
|
249
272
|
PATTERNS.pythonServer.uvicorn.test(data) ||
|
|
@@ -259,24 +282,21 @@ class Session {
|
|
|
259
282
|
const p = PATTERNS;
|
|
260
283
|
const oldStatus = this.meta.status;
|
|
261
284
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
break;
|
|
279
|
-
|
|
285
|
+
// Sprint 44 T3: claude-code status detection now lives in the Claude
|
|
286
|
+
// adapter's `statusFor(data)` method. Returns { status, statusDetail }
|
|
287
|
+
// on a match, null on no-change — preserves the original switch's
|
|
288
|
+
// "leave status untouched if no claude pattern fires" semantics.
|
|
289
|
+
// Other types (gemini, python-server, default shell) stay in-file
|
|
290
|
+
// until Sprint 45 migrates them.
|
|
291
|
+
const adapter = getAdapterForSessionType(this.meta.type);
|
|
292
|
+
if (adapter && typeof adapter.statusFor === 'function') {
|
|
293
|
+
const result = adapter.statusFor(data);
|
|
294
|
+
if (result && result.status) {
|
|
295
|
+
this.meta.status = result.status;
|
|
296
|
+
this.meta.statusDetail = result.statusDetail || '';
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
switch (this.meta.type) {
|
|
280
300
|
case 'gemini':
|
|
281
301
|
if (p.geminiCli.thinking.test(data)) {
|
|
282
302
|
this.meta.status = 'thinking';
|
|
@@ -309,6 +329,7 @@ class Session {
|
|
|
309
329
|
} else {
|
|
310
330
|
this.meta.status = 'active';
|
|
311
331
|
}
|
|
332
|
+
}
|
|
312
333
|
}
|
|
313
334
|
|
|
314
335
|
// Debounce status change events (3s) to avoid flooding RAG with active↔idle flaps
|
|
@@ -387,10 +408,20 @@ class Session {
|
|
|
387
408
|
// Claude Code's tool output frequently contains "error"/"Error" mid-line
|
|
388
409
|
// (grep matches, test results, log dumps). Use a line-anchored pattern
|
|
389
410
|
// for that session type so we don't flag content as failure.
|
|
390
|
-
|
|
391
|
-
|
|
411
|
+
//
|
|
412
|
+
// Sprint 44 T3: per-agent primary error pattern is now read off the
|
|
413
|
+
// adapter (`patterns.error` + `patternNames.error`). Falls back to the
|
|
414
|
+
// generic prose-shape PATTERNS.error when no adapter has claimed the
|
|
415
|
+
// session type. The Claude adapter's `patterns.error` IS the same regex
|
|
416
|
+
// object as PATTERNS.errorLineStart (the shim wires them together), so
|
|
417
|
+
// existing `=== PATTERNS.errorLineStart` reference checks still hold.
|
|
418
|
+
const adapter = getAdapterForSessionType(this.meta.type);
|
|
419
|
+
const primaryPattern = adapter && adapter.patterns && adapter.patterns.error
|
|
420
|
+
? adapter.patterns.error
|
|
392
421
|
: PATTERNS.error;
|
|
393
|
-
const primaryName =
|
|
422
|
+
const primaryName = adapter && adapter.patternNames && adapter.patternNames.error
|
|
423
|
+
? adapter.patternNames.error
|
|
424
|
+
: 'error';
|
|
394
425
|
// Sprint 33 fix: the structured patterns above miss `cat: /foo: No such
|
|
395
426
|
// file or directory` and friends — the most common Unix shell error
|
|
396
427
|
// shapes Josh hits day-to-day. Fall through to PATTERNS.shellError so
|
|
@@ -73,16 +73,54 @@ function listRumenMigrations() {
|
|
|
73
73
|
return tryNodeModules('@jhizzard/rumen');
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
// Resolve the parent directory containing the bundled Rumen Edge Function
|
|
77
|
+
// source. Sprint 43 T3: bundled-FIRST (matches listMnestraMigrations and
|
|
78
|
+
// listRumenMigrations since v0.6.8). The npm `@jhizzard/rumen` package's
|
|
79
|
+
// `files` array is `["dist", "migrations", "README.md", "LICENSE",
|
|
80
|
+
// "CHANGELOG.md"]` — it does NOT ship `supabase/functions/`. So the npm
|
|
81
|
+
// fallback only ever matters for someone who has installed `@jhizzard/rumen`
|
|
82
|
+
// from a local checkout (not the published tarball). Bundled-first prevents
|
|
83
|
+
// a stale local rumen install from shadowing the source TermDeck developed
|
|
84
|
+
// and tested against.
|
|
85
|
+
//
|
|
86
|
+
// Returns the directory whose immediate children are the function-name
|
|
87
|
+
// subdirectories (e.g., `rumen-tick/`, `graph-inference/`).
|
|
88
|
+
function rumenFunctionsRoot() {
|
|
89
|
+
const bundledRoot = path.join(SETUP_DIR, 'rumen', 'functions');
|
|
90
|
+
if (fs.existsSync(bundledRoot) && fs.readdirSync(bundledRoot).length > 0) {
|
|
91
|
+
return bundledRoot;
|
|
92
|
+
}
|
|
78
93
|
try {
|
|
79
94
|
const pkgJsonPath = require.resolve('@jhizzard/rumen/package.json', {
|
|
80
95
|
paths: [process.cwd(), SETUP_DIR]
|
|
81
96
|
});
|
|
82
|
-
const candidate = path.join(path.dirname(pkgJsonPath), 'supabase', 'functions'
|
|
97
|
+
const candidate = path.join(path.dirname(pkgJsonPath), 'supabase', 'functions');
|
|
83
98
|
if (fs.existsSync(candidate)) return candidate;
|
|
84
99
|
} catch (_err) { /* fallthrough */ }
|
|
85
|
-
return
|
|
100
|
+
return bundledRoot;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Enumerate the function-name subdirectories under the resolved Rumen
|
|
104
|
+
// functions root. Each entry must contain at least an `index.ts`. Sprint 43
|
|
105
|
+
// T3 bundled both `rumen-tick` and `graph-inference`.
|
|
106
|
+
function listRumenFunctions() {
|
|
107
|
+
const root = rumenFunctionsRoot();
|
|
108
|
+
if (!fs.existsSync(root)) return [];
|
|
109
|
+
return fs.readdirSync(root)
|
|
110
|
+
.filter((name) => {
|
|
111
|
+
const dir = path.join(root, name);
|
|
112
|
+
return fs.statSync(dir).isDirectory()
|
|
113
|
+
&& fs.existsSync(path.join(dir, 'index.ts'));
|
|
114
|
+
})
|
|
115
|
+
.sort();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Back-compat: pre-Sprint-43 callers expected a single path resolving to the
|
|
119
|
+
// `rumen-tick/` directory specifically. Delegates to rumenFunctionsRoot()
|
|
120
|
+
// + 'rumen-tick'. Prefer rumenFunctionsRoot() / listRumenFunctions() for new
|
|
121
|
+
// code that needs to operate over multiple functions.
|
|
122
|
+
function rumenFunctionDir() {
|
|
123
|
+
return path.join(rumenFunctionsRoot(), 'rumen-tick');
|
|
86
124
|
}
|
|
87
125
|
|
|
88
126
|
function readFile(filepath) {
|
|
@@ -92,6 +130,8 @@ function readFile(filepath) {
|
|
|
92
130
|
module.exports = {
|
|
93
131
|
listMnestraMigrations,
|
|
94
132
|
listRumenMigrations,
|
|
133
|
+
rumenFunctionsRoot,
|
|
134
|
+
listRumenFunctions,
|
|
95
135
|
rumenFunctionDir,
|
|
96
136
|
readFile
|
|
97
137
|
};
|