@jhizzard/termdeck 0.10.0 → 0.10.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/client/public/app.js +42 -3
- package/packages/server/src/flashback-diag.js +51 -0
- package/packages/server/src/index.js +62 -3
- package/packages/server/src/mnestra-bridge/index.js +63 -9
- package/packages/server/src/session.js +95 -5
- package/packages/server/src/setup/mnestra-migrations/011_project_tag_backfill.sql +237 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Browser-based terminal multiplexer with metadata overlays, panel flashback memory recall, and AI-aware session management",
|
|
5
5
|
"bin": {
|
|
6
6
|
"termdeck": "./packages/cli/src/index.js"
|
|
@@ -237,6 +237,9 @@
|
|
|
237
237
|
case 'meta':
|
|
238
238
|
updatePanelMeta(id, msg.session.meta);
|
|
239
239
|
break;
|
|
240
|
+
case 'proactive_memory':
|
|
241
|
+
showProactiveToast(id, msg.hit);
|
|
242
|
+
break;
|
|
240
243
|
case 'exit':
|
|
241
244
|
updatePanelMeta(id, {
|
|
242
245
|
status: 'exited',
|
|
@@ -1245,6 +1248,9 @@
|
|
|
1245
1248
|
case 'meta':
|
|
1246
1249
|
updatePanelMeta(id, msg.session.meta);
|
|
1247
1250
|
break;
|
|
1251
|
+
case 'proactive_memory':
|
|
1252
|
+
showProactiveToast(id, msg.hit);
|
|
1253
|
+
break;
|
|
1248
1254
|
case 'exit':
|
|
1249
1255
|
updatePanelMeta(id, { status: 'exited', statusDetail: `Exited (${msg.exitCode})` });
|
|
1250
1256
|
const p = document.getElementById(`panel-${id}`);
|
|
@@ -1253,6 +1259,17 @@
|
|
|
1253
1259
|
case 'status_broadcast':
|
|
1254
1260
|
updateGlobalStats(msg.sessions);
|
|
1255
1261
|
break;
|
|
1262
|
+
case 'config_changed':
|
|
1263
|
+
// Sprint 40 T1: parity with the main panel WS handler. The
|
|
1264
|
+
// server broadcasts config_changed to ALL ws clients, including
|
|
1265
|
+
// reconnected sessions; previously the reconnect path silently
|
|
1266
|
+
// dropped these. Idempotent — safe to re-receive.
|
|
1267
|
+
if (msg.config) {
|
|
1268
|
+
state.config = { ...state.config, ...msg.config };
|
|
1269
|
+
if (typeof renderSettingsPanel === 'function') renderSettingsPanel();
|
|
1270
|
+
if (typeof updateRagIndicator === 'function') updateRagIndicator();
|
|
1271
|
+
}
|
|
1272
|
+
break;
|
|
1256
1273
|
}
|
|
1257
1274
|
} catch (err) { console.error('[client] reconnect ws message failed:', err); }
|
|
1258
1275
|
};
|
|
@@ -2509,12 +2526,29 @@
|
|
|
2509
2526
|
{
|
|
2510
2527
|
targets: ['#btn-status', '#btn-config'],
|
|
2511
2528
|
title: 'Status and config',
|
|
2512
|
-
body: `<strong>status</strong> opens a global-metrics modal (session counts by state, RAG mode, memory bridge). <strong>config</strong> shows your loaded project list and theme defaults
|
|
2529
|
+
body: `<strong>status</strong> opens a global-metrics modal (session counts by state, RAG mode, memory bridge). <strong>config</strong> shows your loaded project list and theme defaults — plus a live RAG-mode toggle (Sprint 36) that flips Flashback on/off without a server restart.`,
|
|
2530
|
+
},
|
|
2531
|
+
{
|
|
2532
|
+
targets: ['#btn-sprint', '#btn-graph'],
|
|
2533
|
+
title: 'Sprint runner and knowledge graph',
|
|
2534
|
+
body: `<strong>sprint</strong> opens the in-dashboard 4+1 sprint runner (Sprint 37): name the sprint, define T1–T4 lane goals, click kick off — TermDeck spawns four panels and injects boot prompts via the two-stage submit pattern automatically. Optional <strong>--isolation=worktree</strong> creates a git worktree per lane so concurrent edits can't stomp. <strong>graph</strong> opens the D3.js force-directed knowledge graph (Sprint 38) of your memory_items + memory_relationships in a new tab — click any node to open its memory in a drawer, filter by relationship type, search, zoom/pan.`,
|
|
2513
2535
|
},
|
|
2514
2536
|
{
|
|
2515
2537
|
targets: ['#btn-how', '#btn-help'],
|
|
2516
2538
|
title: 'How this works and help',
|
|
2517
|
-
body: `Click <strong>how this works</strong> any time to replay this tour. <strong>help</strong> opens the full TermDeck documentation in a new tab.`,
|
|
2539
|
+
body: `Click <strong>how this works</strong> any time to replay this tour. <strong>help</strong> opens the full TermDeck documentation in a new tab. The <strong>📖 Guide</strong> tab on the right edge of the screen — also opens with the <kbd>g</kbd> keyboard shortcut — is the always-on Orchestrator Guide (Sprint 37): nine sections covering the 4+1 sprint pattern, inject mandate, CLAUDE.md hierarchy, memory-first discipline, sprint discipline, restart-prompt rituals, scaffolding files, channel inject patterns. Search built in.`,
|
|
2540
|
+
},
|
|
2541
|
+
{
|
|
2542
|
+
target: '#guideRail',
|
|
2543
|
+
title: 'Right-rail Orchestrator Guide',
|
|
2544
|
+
body: `The <strong>📖 Guide</strong> rail is your orchestration cheat-sheet — collapsed by default, one click (or <kbd>g</kbd>) to expand. It auto-scrolls to the relevant section based on what you're focused on: clicking a terminal panel jumps the Guide to the 4+1 pattern; opening the project drawer jumps to CLAUDE.md hierarchy. Useful when you forget exactly how the two-stage submit pattern works at 2 AM in the middle of a sprint inject.`,
|
|
2545
|
+
fallback: '#btn-how',
|
|
2546
|
+
},
|
|
2547
|
+
{
|
|
2548
|
+
target: '#btnPreviewProject',
|
|
2549
|
+
title: 'Orchestration preview',
|
|
2550
|
+
body: `The <strong>preview</strong> button next to the project + button (Sprint 37) shows you exactly what <code>termdeck init --project <name></code> would create for the selected project — file tree, contents per file, expand-on-click. Read-only by default; optional generate button writes the scaffolding (CLAUDE.md, CONTRADICTIONS.md, project_facts.md, .claude/settings.json, docs/orchestration/, RESTART-PROMPT.md template). Lets you see-before-commit instead of running the CLI blind.`,
|
|
2551
|
+
fallback: '#btn-how',
|
|
2518
2552
|
},
|
|
2519
2553
|
{
|
|
2520
2554
|
target: '.panel-header',
|
|
@@ -2553,10 +2587,15 @@
|
|
|
2553
2587
|
title: 'Prompt bar',
|
|
2554
2588
|
body: `Type any command here to launch it as a new terminal — <kbd>claude code ~/myproject</kbd>, <kbd>python3 manage.py runserver</kbd>, <kbd>npm run dev</kbd>. Pick a project from the dropdown to auto-cd into its path and apply its default theme. <kbd>Ctrl+Shift+N</kbd> focuses this bar from anywhere.`,
|
|
2555
2589
|
},
|
|
2590
|
+
{
|
|
2591
|
+
target: null,
|
|
2592
|
+
title: 'Knowledge graph + memory inference',
|
|
2593
|
+
body: `Sprint 38 brought your <strong>memory_relationships</strong> table to life. The <strong>graph</strong> button (top toolbar) renders your memories as a force-directed network — supersedes / relates_to / contradicts / elaborates / caused_by / blocks / inspired_by / cross_project_link edges, color-coded, filterable. The Mnestra MCP server now exposes four new tools: <code>memory_link</code>, <code>memory_unlink</code>, <code>memory_related</code>, and <code>memory_recall_graph</code> — Claude Code can connect related memories explicitly, traverse N-hop neighborhoods, and recall via graph-aware re-ranking (vector_score × edge_weight × recency). Edges populate automatically from Joshua's private rag-system classifier; a nightly cron in Sprint 39+ will surface cross-project connections.`,
|
|
2594
|
+
},
|
|
2556
2595
|
{
|
|
2557
2596
|
target: null,
|
|
2558
2597
|
title: 'You are ready.',
|
|
2559
|
-
body: `That's every major surface. Click <strong>how this works</strong> in the top toolbar to replay this walkthrough. <strong>help</strong> opens the full docs. Questions, bugs, feedback: <a href="https://github.com/jhizzard/termdeck/issues" target="_blank" style="color:var(--tg-accent)">github.com/jhizzard/termdeck/issues</a>. Now launch something.`,
|
|
2598
|
+
body: `That's every major surface. Click <strong>how this works</strong> in the top toolbar to replay this walkthrough. <strong>help</strong> opens the full docs. Press <kbd>g</kbd> any time to crack open the Orchestrator Guide. Questions, bugs, feedback: <a href="https://github.com/jhizzard/termdeck/issues" target="_blank" style="color:var(--tg-accent)">github.com/jhizzard/termdeck/issues</a>. Now launch something.`,
|
|
2560
2599
|
},
|
|
2561
2600
|
];
|
|
2562
2601
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Flashback diagnostic ring buffer (Sprint 39 T1).
|
|
2
|
+
//
|
|
3
|
+
// Six decision points along the Flashback pipeline write structured events
|
|
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:
|
|
7
|
+
//
|
|
8
|
+
// log({ sessionId, event, ...fields }) — append one event
|
|
9
|
+
// snapshot({ sessionId?, eventType?, limit? }) — read back filtered tail
|
|
10
|
+
// _resetForTest() — test-only ring clear
|
|
11
|
+
//
|
|
12
|
+
// Event shape (all events): { ts, sessionId, event, ...event-specific fields }.
|
|
13
|
+
//
|
|
14
|
+
// Event types and their producers:
|
|
15
|
+
// pattern_match — session.js _detectErrors (PATTERNS.error /
|
|
16
|
+
// errorLineStart / shellError matched)
|
|
17
|
+
// error_detected — session.js _detectErrors at onErrorDetected
|
|
18
|
+
// entry, before rate-limit check
|
|
19
|
+
// rate_limit_blocked — session.js _detectErrors when 30s limiter rejects
|
|
20
|
+
// bridge_query — mnestra-bridge queryMnestra at call return
|
|
21
|
+
// bridge_result — mnestra-bridge queryMnestra at call return
|
|
22
|
+
// proactive_memory_emit — index.js onErrorDetected WS send block
|
|
23
|
+
//
|
|
24
|
+
// The route GET /api/flashback/diag (registered in index.js) returns
|
|
25
|
+
// snapshot() output as JSON for ad-hoc inspection by Joshua and consumption
|
|
26
|
+
// by T4's production-flow e2e test.
|
|
27
|
+
|
|
28
|
+
const RING_SIZE = 200;
|
|
29
|
+
|
|
30
|
+
let ring = [];
|
|
31
|
+
|
|
32
|
+
function log(event) {
|
|
33
|
+
ring.push({ ts: new Date().toISOString(), ...event });
|
|
34
|
+
if (ring.length > RING_SIZE) {
|
|
35
|
+
ring = ring.slice(-RING_SIZE);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function snapshot({ sessionId, eventType, limit = RING_SIZE } = {}) {
|
|
40
|
+
let out = ring;
|
|
41
|
+
if (sessionId) out = out.filter((e) => e.sessionId === sessionId);
|
|
42
|
+
if (eventType) out = out.filter((e) => e.event === eventType);
|
|
43
|
+
const cap = Math.max(1, Math.min(RING_SIZE, Number(limit) || RING_SIZE));
|
|
44
|
+
return out.slice(-cap);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function _resetForTest() {
|
|
48
|
+
ring = [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { log, snapshot, _resetForTest, RING_SIZE };
|
|
@@ -56,6 +56,7 @@ const { SessionManager } = require('./session');
|
|
|
56
56
|
const { initDatabase, logCommand, getSessionHistory, getProjectSessions } = require('./database');
|
|
57
57
|
const { RAGIntegration } = require('./rag');
|
|
58
58
|
const { createBridge } = require('./mnestra-bridge');
|
|
59
|
+
const flashbackDiag = require('./flashback-diag');
|
|
59
60
|
const { writeSessionLog } = require('./session-logger');
|
|
60
61
|
const { TranscriptWriter } = require('./transcripts');
|
|
61
62
|
const { createHealthHandler, runPreflight } = require('./preflight');
|
|
@@ -853,30 +854,69 @@ function createServer(config) {
|
|
|
853
854
|
question,
|
|
854
855
|
project: sess.meta.project,
|
|
855
856
|
searchAll: false,
|
|
857
|
+
cwd: sess.meta.cwd,
|
|
858
|
+
sessionId: sess.id,
|
|
856
859
|
sessionContext: {
|
|
857
860
|
type: sess.meta.type,
|
|
858
861
|
project: sess.meta.project,
|
|
862
|
+
cwd: sess.meta.cwd,
|
|
859
863
|
lastCommands: sess.meta.lastCommands.slice(-5),
|
|
860
864
|
status: 'errored'
|
|
861
865
|
}
|
|
862
866
|
}).then((result) => {
|
|
863
|
-
const
|
|
867
|
+
const memories = (result && result.memories) || [];
|
|
868
|
+
const count = memories.length;
|
|
864
869
|
console.log(`[flashback] query returned ${count} matches for session ${sess.id}`);
|
|
865
|
-
const hit =
|
|
870
|
+
const hit = memories[0];
|
|
871
|
+
const wsReadyState = sess.ws ? sess.ws.readyState : null;
|
|
866
872
|
if (!hit) {
|
|
867
873
|
console.log(`[flashback] no matches — skipping proactive_memory send for session ${sess.id}`);
|
|
874
|
+
flashbackDiag.log({
|
|
875
|
+
sessionId: sess.id,
|
|
876
|
+
event: 'proactive_memory_emit',
|
|
877
|
+
ws_ready_state: wsReadyState,
|
|
878
|
+
frame_size_bytes: 0,
|
|
879
|
+
result_count_in_frame: 0,
|
|
880
|
+
outcome: 'dropped_empty',
|
|
881
|
+
});
|
|
868
882
|
return;
|
|
869
883
|
}
|
|
870
884
|
if (sess.ws && sess.ws.readyState === 1) {
|
|
885
|
+
const frame = JSON.stringify({ type: 'proactive_memory', hit });
|
|
871
886
|
try {
|
|
872
|
-
sess.ws.send(
|
|
887
|
+
sess.ws.send(frame);
|
|
873
888
|
console.log(`[flashback] proactive_memory sent to session ${sess.id} (source_type=${hit.source_type}, project=${hit.project})`);
|
|
889
|
+
flashbackDiag.log({
|
|
890
|
+
sessionId: sess.id,
|
|
891
|
+
event: 'proactive_memory_emit',
|
|
892
|
+
ws_ready_state: 1,
|
|
893
|
+
frame_size_bytes: Buffer.byteLength(frame, 'utf8'),
|
|
894
|
+
result_count_in_frame: 1,
|
|
895
|
+
outcome: 'emitted',
|
|
896
|
+
});
|
|
874
897
|
} catch (err) {
|
|
875
898
|
console.error('[flashback] proactive_memory send failed:', err);
|
|
876
899
|
console.error('[ws] proactive_memory send failed:', err);
|
|
900
|
+
flashbackDiag.log({
|
|
901
|
+
sessionId: sess.id,
|
|
902
|
+
event: 'proactive_memory_emit',
|
|
903
|
+
ws_ready_state: 1,
|
|
904
|
+
frame_size_bytes: Buffer.byteLength(frame, 'utf8'),
|
|
905
|
+
result_count_in_frame: 1,
|
|
906
|
+
outcome: 'error',
|
|
907
|
+
error_message: err && err.message ? err.message : String(err),
|
|
908
|
+
});
|
|
877
909
|
}
|
|
878
910
|
} else {
|
|
879
911
|
console.log(`[flashback] ws not open for session ${sess.id} (readyState=${sess.ws ? sess.ws.readyState : 'null'}) — dropped hit`);
|
|
912
|
+
flashbackDiag.log({
|
|
913
|
+
sessionId: sess.id,
|
|
914
|
+
event: 'proactive_memory_emit',
|
|
915
|
+
ws_ready_state: wsReadyState,
|
|
916
|
+
frame_size_bytes: 0,
|
|
917
|
+
result_count_in_frame: count,
|
|
918
|
+
outcome: 'dropped_no_ws',
|
|
919
|
+
});
|
|
880
920
|
}
|
|
881
921
|
}).catch((err) => {
|
|
882
922
|
console.error(`[flashback] query failed for session ${sess.id}: ${err.message}`);
|
|
@@ -1347,6 +1387,23 @@ function createServer(config) {
|
|
|
1347
1387
|
});
|
|
1348
1388
|
});
|
|
1349
1389
|
|
|
1390
|
+
// GET /api/flashback/diag - Sprint 39 T1 diagnostic ring buffer.
|
|
1391
|
+
// Returns the last N Flashback decision-point events so Joshua can trigger
|
|
1392
|
+
// a real-shell error and read the timeline of which gate dropped the toast.
|
|
1393
|
+
// Optional filters: ?sessionId=<uuid>, ?eventType=pattern_match, ?limit=N
|
|
1394
|
+
// (capped at 200, the ring size).
|
|
1395
|
+
app.get('/api/flashback/diag', (req, res) => {
|
|
1396
|
+
const { sessionId, eventType } = req.query || {};
|
|
1397
|
+
const rawLimit = req.query && req.query.limit;
|
|
1398
|
+
const limit = rawLimit != null ? parseInt(rawLimit, 10) : undefined;
|
|
1399
|
+
const events = flashbackDiag.snapshot({
|
|
1400
|
+
sessionId: typeof sessionId === 'string' && sessionId.length ? sessionId : undefined,
|
|
1401
|
+
eventType: typeof eventType === 'string' && eventType.length ? eventType : undefined,
|
|
1402
|
+
limit: Number.isFinite(limit) && limit > 0 ? Math.min(limit, flashbackDiag.RING_SIZE) : undefined,
|
|
1403
|
+
});
|
|
1404
|
+
res.json({ count: events.length, events });
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1350
1407
|
// ==================== Transcript endpoints (Sprint 6 T3) ====================
|
|
1351
1408
|
|
|
1352
1409
|
// GET /api/transcripts/search - FTS across all sessions
|
|
@@ -1568,6 +1625,7 @@ function createServer(config) {
|
|
|
1568
1625
|
const sessionContext = session ? {
|
|
1569
1626
|
type: session.meta.type,
|
|
1570
1627
|
project: session.meta.project,
|
|
1628
|
+
cwd: session.meta.cwd,
|
|
1571
1629
|
lastCommands: session.meta.lastCommands.slice(-5),
|
|
1572
1630
|
status: session.meta.status
|
|
1573
1631
|
} : null;
|
|
@@ -1577,6 +1635,7 @@ function createServer(config) {
|
|
|
1577
1635
|
question,
|
|
1578
1636
|
project,
|
|
1579
1637
|
searchAll,
|
|
1638
|
+
cwd: session ? session.meta.cwd : undefined,
|
|
1580
1639
|
sessionContext
|
|
1581
1640
|
});
|
|
1582
1641
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const { spawn } = require('child_process');
|
|
12
12
|
const { resolveProjectName } = require('../rag');
|
|
13
|
+
const flashbackDiag = require('../flashback-diag');
|
|
13
14
|
|
|
14
15
|
function createBridge(config) {
|
|
15
16
|
const mode = config.rag?.mnestraMode || 'direct';
|
|
@@ -225,7 +226,7 @@ function createBridge(config) {
|
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
|
|
228
|
-
async function queryMnestra({ question, project, searchAll, sessionContext, cwd }) {
|
|
229
|
+
async function queryMnestra({ question, project, searchAll, sessionContext, cwd, sessionId }) {
|
|
229
230
|
// Flashback callers pass the session's project (from config.yaml). If that
|
|
230
231
|
// slot is empty — e.g. a session created without an explicit project — fall
|
|
231
232
|
// back to resolving the session's cwd against config.projects so queries
|
|
@@ -246,15 +247,68 @@ function createBridge(config) {
|
|
|
246
247
|
// out-of-repo session-end hook), the mismatch surfaces here at query time.
|
|
247
248
|
console.log(`[mnestra-bridge] query project=${effectiveProject ?? 'ALL'} source=${searchAll ? 'searchAll' : projectSource} mode=${mode}`);
|
|
248
249
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
250
|
+
const projectTagInFilter = searchAll ? null : (effectiveProject || null);
|
|
251
|
+
const t0 = Date.now();
|
|
252
|
+
let result;
|
|
253
|
+
let callError;
|
|
254
|
+
try {
|
|
255
|
+
switch (mode) {
|
|
256
|
+
case 'webhook':
|
|
257
|
+
result = await queryWebhook({ question, project: effectiveProject, searchAll });
|
|
258
|
+
break;
|
|
259
|
+
case 'mcp':
|
|
260
|
+
result = await queryMcp({ question, project: effectiveProject, searchAll });
|
|
261
|
+
break;
|
|
262
|
+
case 'direct':
|
|
263
|
+
default:
|
|
264
|
+
result = await queryDirect({ question, project: effectiveProject, searchAll });
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
} catch (err) {
|
|
268
|
+
callError = err;
|
|
257
269
|
}
|
|
270
|
+
const durationMs = Date.now() - t0;
|
|
271
|
+
|
|
272
|
+
// Sprint 39 T1 — bridge_query / bridge_result diag events. Emitted at
|
|
273
|
+
// queryMnestra's outer boundary so all three backends (direct, webhook,
|
|
274
|
+
// mcp) flow through one observability point. T3 reads project_tag_in_filter
|
|
275
|
+
// (the tag the bridge SENT to the RPC) and top_3_project_tags (the tags
|
|
276
|
+
// it GOT BACK) to confirm or refute the project-mismatch hypothesis.
|
|
277
|
+
flashbackDiag.log({
|
|
278
|
+
sessionId,
|
|
279
|
+
event: 'bridge_query',
|
|
280
|
+
project_tag_in_filter: projectTagInFilter,
|
|
281
|
+
query_text: typeof question === 'string' ? question.slice(0, 200) : '',
|
|
282
|
+
mode,
|
|
283
|
+
rpc_args: {
|
|
284
|
+
project: projectTagInFilter,
|
|
285
|
+
searchAll: !!searchAll,
|
|
286
|
+
project_source: searchAll ? 'searchAll' : projectSource,
|
|
287
|
+
},
|
|
288
|
+
duration_ms: durationMs,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const memories = (result && Array.isArray(result.memories)) ? result.memories : [];
|
|
292
|
+
const tagCounts = {};
|
|
293
|
+
for (const m of memories) {
|
|
294
|
+
const tag = m && m.project != null ? String(m.project) : '(null)';
|
|
295
|
+
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
|
296
|
+
}
|
|
297
|
+
const top3 = Object.entries(tagCounts)
|
|
298
|
+
.sort((a, b) => b[1] - a[1])
|
|
299
|
+
.slice(0, 3)
|
|
300
|
+
.map(([tag, count]) => ({ tag, count }));
|
|
301
|
+
|
|
302
|
+
flashbackDiag.log({
|
|
303
|
+
sessionId,
|
|
304
|
+
event: 'bridge_result',
|
|
305
|
+
result_count: memories.length,
|
|
306
|
+
error_message: callError ? (callError.message || String(callError)) : null,
|
|
307
|
+
top_3_project_tags: top3,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (callError) throw callError;
|
|
311
|
+
return result;
|
|
258
312
|
}
|
|
259
313
|
|
|
260
314
|
return { mode, queryMnestra };
|
|
@@ -14,6 +14,7 @@ const { v4: uuidv4 } = require('uuid');
|
|
|
14
14
|
const os = require('os');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { resolveTheme } = require('./theme-resolver');
|
|
17
|
+
const flashbackDiag = require('./flashback-diag');
|
|
17
18
|
|
|
18
19
|
// Strip ANSI escape codes for pattern matching
|
|
19
20
|
function stripAnsi(str) {
|
|
@@ -43,6 +44,13 @@ const PATTERNS = {
|
|
|
43
44
|
django: /Starting development server/,
|
|
44
45
|
httpServer: /Serving HTTP on/,
|
|
45
46
|
request: /(?:^|\s|")(GET|POST|PUT|DELETE|PATCH)\s+\S+.*?\s(\d{3})/m,
|
|
47
|
+
// Sprint 40 T2: HTTP 5xx response in a web-server log line is a real
|
|
48
|
+
// error condition for the application. Used as a python-server-typed
|
|
49
|
+
// fallback in _detectErrors when the prose-shape analyzers miss because
|
|
50
|
+
// the line carries no `Error:` keyword — just `"GET /foo HTTP/1.1" 503`.
|
|
51
|
+
// 5xx only (not 4xx, which are typically client-caused). The leading
|
|
52
|
+
// `(?:^|\s|")` mirrors `request` so colon-quoted log shapes still match.
|
|
53
|
+
serverError: /(?:^|\s|")(?:GET|POST|PUT|DELETE|PATCH)\s+\S+.*?\sHTTP\/\d(?:\.\d)?"?\s+5\d{2}\b/m,
|
|
46
54
|
// Port detection — matches any of:
|
|
47
55
|
// • "port NNNN" phrase (capture group 1)
|
|
48
56
|
// • URL with http/https scheme, optionally prefixed with "on " or "at "
|
|
@@ -65,11 +73,20 @@ const PATTERNS = {
|
|
|
65
73
|
// tools (cat, ls, cd, rm, etc.) report filesystem misses in plain English
|
|
66
74
|
// without ever emitting the ENOENT errno code. Flagged as a gap by Rumen's
|
|
67
75
|
// first production kickstart insight on 2026-04-15.
|
|
68
|
-
|
|
76
|
+
// Sprint 40 T2: added uppercase `ERROR:` (mirrors `Error:` / `error:` for
|
|
77
|
+
// case-symmetry — closes the stripAnsi-ERROR test fixture from Sprint 33)
|
|
78
|
+
// and Node errno-style colon-prefix shapes (`ENOENT:`, `EACCES:`,
|
|
79
|
+
// `ECONNREFUSED:`) so `ENOENT: no such file or directory` shapes from
|
|
80
|
+
// child-process error reporting fire without depending on the line ALSO
|
|
81
|
+
// containing the `No such file or directory` prose phrase.
|
|
82
|
+
error: /(?:^|\n)\s*(?:Error:\s+\S|error:\s+\S|ERROR:\s+\S|Traceback \(most recent call last\):|npm ERR!|error\[E\d+\]:|Uncaught Exception|Fatal:|ENOENT:\s+\S|EACCES:\s+\S|ECONNREFUSED:\s+\S)/m,
|
|
69
83
|
// Stricter line-anchored variant for Claude Code, whose tool output (grep
|
|
70
84
|
// results, test logs, file contents) routinely mentions "Error" mid-line
|
|
71
85
|
// without representing an actual failure of the agent itself.
|
|
72
|
-
|
|
86
|
+
// Sprint 40 T2: added mixed-case `Fatal` (mirrors `fatal` / `FATAL`) and
|
|
87
|
+
// the `npm ERR!` shape (special-cased outside the alternation because
|
|
88
|
+
// `!` is not a word character so `\b` after `npm ERR!` doesn't match).
|
|
89
|
+
errorLineStart: /^\s*(?:(?:error|Error|ERROR|exception|Exception|Traceback|fatal|Fatal|FATAL|segmentation fault|panic|EACCES|ECONNREFUSED|ENOENT|command not found|undefined reference|cannot find module|failed with exit code|No such file or directory|Permission denied)\b|npm ERR!)/m,
|
|
73
90
|
// Sprint 33: PATTERNS.error misses the most common Unix shell errors —
|
|
74
91
|
// `cat: /foo: No such file or directory`, `bash: foo: command not found`,
|
|
75
92
|
// `rm: cannot remove ...: Permission denied`. These have a colon-prefix
|
|
@@ -77,7 +94,27 @@ const PATTERNS = {
|
|
|
77
94
|
// mentioning the same words. Each branch requires either the colon-prefix
|
|
78
95
|
// structure or a stand-alone anchored keyword. Validated against an
|
|
79
96
|
// adversarial prose suite (see tests/analyzer-error-fixtures.test.js).
|
|
80
|
-
|
|
97
|
+
//
|
|
98
|
+
// Sprint 39 T2: separated `command not found` from the other phrases. The
|
|
99
|
+
// unified branch was matching rcfile-noise lines emitted by version
|
|
100
|
+
// managers during shell startup — most notably:
|
|
101
|
+
// `pyenv: pyenv-virtualenv-init: command not found in path`
|
|
102
|
+
// …which has the colon-prefix-with-`command not found` shape but with a
|
|
103
|
+
// descriptive suffix (` in path`) rather than ending the line. The pyenv
|
|
104
|
+
// case confirms the strong rcfile-noise hypothesis for pyenv users: their
|
|
105
|
+
// shell startup burns the 30s onErrorDetected rate limit before the user
|
|
106
|
+
// can type their first command. The dedicated `command not found` branch
|
|
107
|
+
// below requires the keyword to be either:
|
|
108
|
+
// • followed by `:` (the zsh `command not found: <cmd>` form), or
|
|
109
|
+
// • at end-of-line (the bash `<sh>: <cmd>: command not found` form).
|
|
110
|
+
// Suffixes like ` in path`, ` in $PATH`, ` (compinit)` are silenced as
|
|
111
|
+
// rcfile noise.
|
|
112
|
+
// Trade-off: custom command_not_found_handler output that adds a comma-
|
|
113
|
+
// separated "did you mean X" suggestion is silenced — those are cosmetic
|
|
114
|
+
// suggestions, not the error itself, which the user already saw fire.
|
|
115
|
+
// See tests/rcfile-noise.test.js and tests/analyzer-error-fixtures.test.js
|
|
116
|
+
// for the locked corpus.
|
|
117
|
+
shellError: /(?:^|\n)(?:[^\n]*:\s+(?:.*?:\s+)?(?:No such file or directory|Permission denied|Is a directory|Not a directory)\b|[^\n]*:\s+(?:.*?:\s+)?command not found(?::|\s*(?:[\r\n]|$))|[^\n]*?\(\d+\)\s+Could not resolve host\b|\s*ModuleNotFoundError:\s+\S|\s*Segmentation fault\b|\s*fatal:\s+\S)/m
|
|
81
118
|
};
|
|
82
119
|
|
|
83
120
|
class Session {
|
|
@@ -350,14 +387,44 @@ class Session {
|
|
|
350
387
|
// Claude Code's tool output frequently contains "error"/"Error" mid-line
|
|
351
388
|
// (grep matches, test results, log dumps). Use a line-anchored pattern
|
|
352
389
|
// for that session type so we don't flag content as failure.
|
|
353
|
-
const
|
|
390
|
+
const primaryPattern = this.meta.type === 'claude-code'
|
|
354
391
|
? PATTERNS.errorLineStart
|
|
355
392
|
: PATTERNS.error;
|
|
393
|
+
const primaryName = this.meta.type === 'claude-code' ? 'errorLineStart' : 'error';
|
|
356
394
|
// Sprint 33 fix: the structured patterns above miss `cat: /foo: No such
|
|
357
395
|
// file or directory` and friends — the most common Unix shell error
|
|
358
396
|
// shapes Josh hits day-to-day. Fall through to PATTERNS.shellError so
|
|
359
397
|
// the analyzer flips status='errored' and Flashback can fire.
|
|
360
|
-
|
|
398
|
+
const primaryMatch = clean.match(primaryPattern);
|
|
399
|
+
const shellMatch = !primaryMatch ? clean.match(PATTERNS.shellError) : null;
|
|
400
|
+
// Sprint 40 T2: HTTP 5xx fallback for python-server sessions. The prose
|
|
401
|
+
// analyzers miss `"GET /foo HTTP/1.1" 503 -` because it carries no
|
|
402
|
+
// `Error:` keyword — but the response IS the error signal for an
|
|
403
|
+
// HTTP-server session. Gated on session type to avoid flagging 5xx
|
|
404
|
+
// status codes that legitimately appear in unrelated content (e.g. a
|
|
405
|
+
// shell that just printed a copy of an HTTP log).
|
|
406
|
+
const serverMatch = (!primaryMatch && !shellMatch && this.meta.type === 'python-server')
|
|
407
|
+
? clean.match(PATTERNS.pythonServer.serverError)
|
|
408
|
+
: null;
|
|
409
|
+
if (!primaryMatch && !shellMatch && !serverMatch) return;
|
|
410
|
+
|
|
411
|
+
// Sprint 39 T1 — pattern_match diag event. Emitted on every PATTERNS hit,
|
|
412
|
+
// including ones that get rate-limited downstream. T2 reads these to
|
|
413
|
+
// measure the rcfile-noise false-positive rate against real shell output.
|
|
414
|
+
const matchedSrc = primaryMatch || shellMatch || serverMatch;
|
|
415
|
+
const matchedLine = (matchedSrc && typeof matchedSrc[0] === 'string')
|
|
416
|
+
? matchedSrc[0].replace(/^\n+/, '').slice(0, 200)
|
|
417
|
+
: '';
|
|
418
|
+
const matchedPattern = primaryMatch
|
|
419
|
+
? primaryName
|
|
420
|
+
: (shellMatch ? 'shellError' : 'serverError');
|
|
421
|
+
flashbackDiag.log({
|
|
422
|
+
sessionId: this.id,
|
|
423
|
+
event: 'pattern_match',
|
|
424
|
+
pattern: matchedPattern,
|
|
425
|
+
matched_line: matchedLine,
|
|
426
|
+
output_chunk_size: clean.length,
|
|
427
|
+
});
|
|
361
428
|
|
|
362
429
|
const oldStatus = this.meta.status;
|
|
363
430
|
this.meta.status = 'errored';
|
|
@@ -371,7 +438,30 @@ class Session {
|
|
|
371
438
|
|
|
372
439
|
// Server-side rate limit: at most one error_detected event every 30s per session
|
|
373
440
|
const now = Date.now();
|
|
441
|
+
const remainingMs = this._lastErrorFireAt
|
|
442
|
+
? Math.max(0, 30000 - (now - this._lastErrorFireAt))
|
|
443
|
+
: 0;
|
|
444
|
+
|
|
445
|
+
// Sprint 39 T1 — error_detected diag event, before the rate-limit gate.
|
|
446
|
+
// The (error_detected count − rate_limit_blocked count) is the number of
|
|
447
|
+
// errors that actually got dispatched to onErrorDetected. T2/T3 use this
|
|
448
|
+
// to spot rcfile noise burning the rate-limit window before real errors.
|
|
449
|
+
flashbackDiag.log({
|
|
450
|
+
sessionId: this.id,
|
|
451
|
+
event: 'error_detected',
|
|
452
|
+
error_text: matchedLine,
|
|
453
|
+
rate_limit_remaining_ms: remainingMs,
|
|
454
|
+
last_emit_at: this._lastErrorFireAt
|
|
455
|
+
? new Date(this._lastErrorFireAt).toISOString()
|
|
456
|
+
: null,
|
|
457
|
+
});
|
|
458
|
+
|
|
374
459
|
if (now - this._lastErrorFireAt < 30000) {
|
|
460
|
+
flashbackDiag.log({
|
|
461
|
+
sessionId: this.id,
|
|
462
|
+
event: 'rate_limit_blocked',
|
|
463
|
+
rate_limit_remaining_ms: remainingMs,
|
|
464
|
+
});
|
|
375
465
|
console.log(`[flashback] error detected in session ${this.id} but rate-limited (${Math.round((30000 - (now - this._lastErrorFireAt)) / 1000)}s left)`);
|
|
376
466
|
return;
|
|
377
467
|
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
-- Sprint 39 T3 — chopin-nashville project-tag backfill.
|
|
2
|
+
--
|
|
3
|
+
-- Why this exists:
|
|
4
|
+
-- memory_items rows tagged project='chopin-nashville' are ~96% polluted
|
|
5
|
+
-- with content from other projects (termdeck, mnestra, rumen, podium, pvb,
|
|
6
|
+
-- dor). Root cause is the harness session-end hook
|
|
7
|
+
-- (~/.claude/hooks/memory-session-end.js, OUT OF THIS REPO): its
|
|
8
|
+
-- PROJECT_MAP iteration tests /ChopinNashville/i first and there are no
|
|
9
|
+
-- entries for termdeck/mnestra/rumen/podium/dor — so any session whose
|
|
10
|
+
-- cwd lives under ~/Documents/Graciella/ChopinNashville/... falls into
|
|
11
|
+
-- chopin-nashville, including the entire TermDeck checkout (which lives at
|
|
12
|
+
-- ChopinNashville/SideHustles/TermDeck/termdeck) and Podium (which lives at
|
|
13
|
+
-- ChopinNashville/2026/ChopinInBohemia/podium).
|
|
14
|
+
--
|
|
15
|
+
-- This migration heals the historical rows. The forward-fix to the harness
|
|
16
|
+
-- hook is Joshua's responsibility (out-of-repo file) and is NOT covered
|
|
17
|
+
-- here — without it, new mis-tagged rows will continue to be written until
|
|
18
|
+
-- he extends PROJECT_MAP with the missing project entries.
|
|
19
|
+
--
|
|
20
|
+
-- What this migration does NOT do:
|
|
21
|
+
-- - Does NOT touch mnestra_session_memory / mnestra_project_memory / etc.
|
|
22
|
+
-- (legacy rag-events tables; different write path; separate cleanup).
|
|
23
|
+
-- - Does NOT consolidate duplicate project tags like 'gorgias' vs
|
|
24
|
+
-- 'gorgias-ticket-monitor', 'pvb' vs 'PVB', or 'mnestra' vs 'engram'.
|
|
25
|
+
-- Those are visible in `SELECT project, count(*) FROM memory_items GROUP
|
|
26
|
+
-- BY project` but they're a separate cleanup pass.
|
|
27
|
+
-- - Does NOT touch the ~898 "other/uncertain" chopin-nashville rows that
|
|
28
|
+
-- don't carry an unambiguous project keyword. A future sprint can run an
|
|
29
|
+
-- LLM-classification pass; for this migration, conservative wins.
|
|
30
|
+
--
|
|
31
|
+
-- Heuristic — content keyword bucketing:
|
|
32
|
+
-- The migration runs UPDATEs sequentially. Earlier buckets claim ambiguous
|
|
33
|
+
-- multi-project rows first; later buckets only see rows that no earlier
|
|
34
|
+
-- bucket has already re-tagged. Order is by bucket size (largest first):
|
|
35
|
+
--
|
|
36
|
+
-- 1. termdeck / mnestra — keywords: termdeck, mnestra, "4+1 sprint"
|
|
37
|
+
-- 2. rumen — keyword: rumen
|
|
38
|
+
-- 3. podium — keyword: podium
|
|
39
|
+
-- 4. pvb — keywords: PVB, petvetbid, pet vet bid
|
|
40
|
+
-- 5. dor / openclaw — TIGHTENED:
|
|
41
|
+
-- word-boundary uppercase DOR (rules out
|
|
42
|
+
-- "dormant", "vendored", "indoor", etc.),
|
|
43
|
+
-- plus path/identifier markers and
|
|
44
|
+
-- openclaw substring.
|
|
45
|
+
--
|
|
46
|
+
-- Spot-check baseline (T3 audit, 2026-04-27):
|
|
47
|
+
-- termdeck/mnestra: 130 rows, all 6 sampled were true positives (TermDeck
|
|
48
|
+
-- server code, Mnestra wizard, sprint orchestration).
|
|
49
|
+
-- rumen: 92 rows, all 6 sampled were true positives.
|
|
50
|
+
-- podium: 58 rows, all 6 sampled were true positives.
|
|
51
|
+
-- pvb: 7 rows, 1 of those overlaps with mnestra ("Mnestra
|
|
52
|
+
-- repo … petvetbid project") and gets claimed by bucket 1.
|
|
53
|
+
-- dor (tightened): 3 rows after tightening from 6 — the original
|
|
54
|
+
-- `%dor%` ILIKE pattern caught false positives like
|
|
55
|
+
-- "dormant", "vendored". Final 3 rows are all true
|
|
56
|
+
-- DOR/OpenClaw mentions.
|
|
57
|
+
-- chopin-nashville total: 1,169 rows. Legitimate-signal baseline (rows
|
|
58
|
+
-- matching Acceptd / NICPC / Bohemia / laureate /
|
|
59
|
+
-- applicant / competition / repertoire keywords): 71.
|
|
60
|
+
--
|
|
61
|
+
-- Idempotence:
|
|
62
|
+
-- Every UPDATE is gated by `WHERE project = 'chopin-nashville'`. After the
|
|
63
|
+
-- first run, those rows have a different project tag, so re-running this
|
|
64
|
+
-- migration is a no-op (zero rows updated per bucket). RAISE NOTICE on a
|
|
65
|
+
-- re-run will print zeros, which is the expected idempotent signal.
|
|
66
|
+
--
|
|
67
|
+
-- Application:
|
|
68
|
+
-- THIS MIGRATION IS NOT EXECUTED BY THE LANE THAT WROTE IT. Orchestrator
|
|
69
|
+
-- reviews the RAISE NOTICE counts after applying. Apply via the bundled
|
|
70
|
+
-- migration runner at packages/server/src/setup/migration-runner.js (which
|
|
71
|
+
-- uses node-postgres client.query — psql metacommands like \gset are NOT
|
|
72
|
+
-- available, so the count probes use GET DIAGNOSTICS ROW_COUNT inside DO
|
|
73
|
+
-- blocks). Manual fallback: `psql "$DATABASE_URL" -f 011_project_tag_backfill.sql`.
|
|
74
|
+
|
|
75
|
+
BEGIN;
|
|
76
|
+
|
|
77
|
+
-- ============================================================
|
|
78
|
+
-- AUDIT BEFORE
|
|
79
|
+
-- ============================================================
|
|
80
|
+
DO $$
|
|
81
|
+
DECLARE
|
|
82
|
+
before_chopin int;
|
|
83
|
+
before_termdeck int;
|
|
84
|
+
before_rumen int;
|
|
85
|
+
before_podium int;
|
|
86
|
+
before_pvb int;
|
|
87
|
+
before_dor int;
|
|
88
|
+
BEGIN
|
|
89
|
+
SELECT count(*) INTO before_chopin FROM memory_items WHERE project = 'chopin-nashville';
|
|
90
|
+
SELECT count(*) INTO before_termdeck FROM memory_items WHERE project = 'termdeck';
|
|
91
|
+
SELECT count(*) INTO before_rumen FROM memory_items WHERE project = 'rumen';
|
|
92
|
+
SELECT count(*) INTO before_podium FROM memory_items WHERE project = 'podium';
|
|
93
|
+
SELECT count(*) INTO before_pvb FROM memory_items WHERE project = 'pvb';
|
|
94
|
+
SELECT count(*) INTO before_dor FROM memory_items WHERE project = 'dor';
|
|
95
|
+
RAISE NOTICE '[011-backfill] BEFORE chopin-nashville=% termdeck=% rumen=% podium=% pvb=% dor=%',
|
|
96
|
+
before_chopin, before_termdeck, before_rumen, before_podium, before_pvb, before_dor;
|
|
97
|
+
END $$;
|
|
98
|
+
|
|
99
|
+
-- ============================================================
|
|
100
|
+
-- BUCKET 1 — TermDeck / Mnestra (claims multi-project mentions first)
|
|
101
|
+
-- ============================================================
|
|
102
|
+
DO $$
|
|
103
|
+
DECLARE
|
|
104
|
+
rows_updated int;
|
|
105
|
+
BEGIN
|
|
106
|
+
UPDATE memory_items SET project = 'termdeck'
|
|
107
|
+
WHERE project = 'chopin-nashville'
|
|
108
|
+
AND (
|
|
109
|
+
content ILIKE '%termdeck%'
|
|
110
|
+
OR content ILIKE '%mnestra%'
|
|
111
|
+
OR content ILIKE '%4+1 sprint%'
|
|
112
|
+
);
|
|
113
|
+
GET DIAGNOSTICS rows_updated = ROW_COUNT;
|
|
114
|
+
RAISE NOTICE '[011-backfill] bucket 1 (termdeck/mnestra): % rows re-tagged', rows_updated;
|
|
115
|
+
END $$;
|
|
116
|
+
|
|
117
|
+
-- ============================================================
|
|
118
|
+
-- BUCKET 2 — Rumen
|
|
119
|
+
-- ============================================================
|
|
120
|
+
DO $$
|
|
121
|
+
DECLARE
|
|
122
|
+
rows_updated int;
|
|
123
|
+
BEGIN
|
|
124
|
+
UPDATE memory_items SET project = 'rumen'
|
|
125
|
+
WHERE project = 'chopin-nashville'
|
|
126
|
+
AND content ILIKE '%rumen%';
|
|
127
|
+
GET DIAGNOSTICS rows_updated = ROW_COUNT;
|
|
128
|
+
RAISE NOTICE '[011-backfill] bucket 2 (rumen): % rows re-tagged', rows_updated;
|
|
129
|
+
END $$;
|
|
130
|
+
|
|
131
|
+
-- ============================================================
|
|
132
|
+
-- BUCKET 3 — Podium
|
|
133
|
+
-- ============================================================
|
|
134
|
+
DO $$
|
|
135
|
+
DECLARE
|
|
136
|
+
rows_updated int;
|
|
137
|
+
BEGIN
|
|
138
|
+
UPDATE memory_items SET project = 'podium'
|
|
139
|
+
WHERE project = 'chopin-nashville'
|
|
140
|
+
AND content ILIKE '%podium%';
|
|
141
|
+
GET DIAGNOSTICS rows_updated = ROW_COUNT;
|
|
142
|
+
RAISE NOTICE '[011-backfill] bucket 3 (podium): % rows re-tagged', rows_updated;
|
|
143
|
+
END $$;
|
|
144
|
+
|
|
145
|
+
-- ============================================================
|
|
146
|
+
-- BUCKET 4 — PVB (case-insensitive PVB / petvetbid markers)
|
|
147
|
+
-- ============================================================
|
|
148
|
+
DO $$
|
|
149
|
+
DECLARE
|
|
150
|
+
rows_updated int;
|
|
151
|
+
BEGIN
|
|
152
|
+
UPDATE memory_items SET project = 'pvb'
|
|
153
|
+
WHERE project = 'chopin-nashville'
|
|
154
|
+
AND (
|
|
155
|
+
content ILIKE '%PVB%'
|
|
156
|
+
OR content ILIKE '%petvetbid%'
|
|
157
|
+
OR content ILIKE '%pet vet bid%'
|
|
158
|
+
);
|
|
159
|
+
GET DIAGNOSTICS rows_updated = ROW_COUNT;
|
|
160
|
+
RAISE NOTICE '[011-backfill] bucket 4 (pvb): % rows re-tagged', rows_updated;
|
|
161
|
+
END $$;
|
|
162
|
+
|
|
163
|
+
-- ============================================================
|
|
164
|
+
-- BUCKET 5 — DOR / OpenClaw (TIGHTENED — word boundary + identifiers)
|
|
165
|
+
--
|
|
166
|
+
-- Original briefing heuristic was `content ILIKE '%dor%'`, which produced a
|
|
167
|
+
-- ~33% false-positive rate (matched "dormant", "vendored", "indoor", etc.).
|
|
168
|
+
-- T3 audit tightened to:
|
|
169
|
+
-- • POSIX word boundary `\mDOR\M` — case-sensitive uppercase only, so
|
|
170
|
+
-- "dormant" / "DormHall" / "vendor" / "indoor" no longer match.
|
|
171
|
+
-- • path/identifier markers: /DOR/, ~/Documents/DOR, dor.config,
|
|
172
|
+
-- "Rust LLM gateway" (DOR's tagline).
|
|
173
|
+
-- • openclaw substring (OpenClaw is the slack-channel automation product
|
|
174
|
+
-- that lives next to DOR in Joshua's stack).
|
|
175
|
+
-- ============================================================
|
|
176
|
+
DO $$
|
|
177
|
+
DECLARE
|
|
178
|
+
rows_updated int;
|
|
179
|
+
BEGIN
|
|
180
|
+
UPDATE memory_items SET project = 'dor'
|
|
181
|
+
WHERE project = 'chopin-nashville'
|
|
182
|
+
AND (
|
|
183
|
+
content ~ '\mDOR\M'
|
|
184
|
+
OR content ILIKE '%/DOR/%'
|
|
185
|
+
OR content ILIKE '%~/Documents/DOR%'
|
|
186
|
+
OR content ILIKE '%dor.config%'
|
|
187
|
+
OR content ILIKE '%Rust LLM gateway%'
|
|
188
|
+
OR content ILIKE '%openclaw%'
|
|
189
|
+
);
|
|
190
|
+
GET DIAGNOSTICS rows_updated = ROW_COUNT;
|
|
191
|
+
RAISE NOTICE '[011-backfill] bucket 5 (dor): % rows re-tagged', rows_updated;
|
|
192
|
+
END $$;
|
|
193
|
+
|
|
194
|
+
-- ============================================================
|
|
195
|
+
-- AUDIT AFTER
|
|
196
|
+
-- ============================================================
|
|
197
|
+
DO $$
|
|
198
|
+
DECLARE
|
|
199
|
+
after_chopin int;
|
|
200
|
+
after_termdeck int;
|
|
201
|
+
after_rumen int;
|
|
202
|
+
after_podium int;
|
|
203
|
+
after_pvb int;
|
|
204
|
+
after_dor int;
|
|
205
|
+
BEGIN
|
|
206
|
+
SELECT count(*) INTO after_chopin FROM memory_items WHERE project = 'chopin-nashville';
|
|
207
|
+
SELECT count(*) INTO after_termdeck FROM memory_items WHERE project = 'termdeck';
|
|
208
|
+
SELECT count(*) INTO after_rumen FROM memory_items WHERE project = 'rumen';
|
|
209
|
+
SELECT count(*) INTO after_podium FROM memory_items WHERE project = 'podium';
|
|
210
|
+
SELECT count(*) INTO after_pvb FROM memory_items WHERE project = 'pvb';
|
|
211
|
+
SELECT count(*) INTO after_dor FROM memory_items WHERE project = 'dor';
|
|
212
|
+
RAISE NOTICE '[011-backfill] AFTER chopin-nashville=% termdeck=% rumen=% podium=% pvb=% dor=%',
|
|
213
|
+
after_chopin, after_termdeck, after_rumen, after_podium, after_pvb, after_dor;
|
|
214
|
+
RAISE NOTICE '[011-backfill] If apply succeeds and chopin-nashville count is around the legitimate baseline (~71 rows match competition/laureate/applicant/Acceptd/NICPC/Bohemia/repertoire keywords as of T3 audit), the migration succeeded. The ~898 rows that remain under chopin-nashville without a clear keyword signal are deliberate — a future LLM-classification pass can address them if needed.';
|
|
215
|
+
END $$;
|
|
216
|
+
|
|
217
|
+
COMMIT;
|
|
218
|
+
|
|
219
|
+
-- ============================================================
|
|
220
|
+
-- POST-APPLY: optional verification queries (NOT part of the migration).
|
|
221
|
+
-- Run separately to confirm Flashback against project='termdeck' now hits
|
|
222
|
+
-- the re-tagged rows.
|
|
223
|
+
-- ============================================================
|
|
224
|
+
--
|
|
225
|
+
-- 1. Tag distribution after migration:
|
|
226
|
+
-- SELECT project, count(*) FROM memory_items GROUP BY project ORDER BY count(*) DESC LIMIT 20;
|
|
227
|
+
--
|
|
228
|
+
-- 2. Confirm no chopin-nashville rows match obvious termdeck/rumen keywords:
|
|
229
|
+
-- SELECT count(*) FROM memory_items
|
|
230
|
+
-- WHERE project='chopin-nashville'
|
|
231
|
+
-- AND (content ILIKE '%termdeck%' OR content ILIKE '%rumen%' OR content ILIKE '%podium%');
|
|
232
|
+
-- -- Expected: 0
|
|
233
|
+
--
|
|
234
|
+
-- 3. Confirm Flashback project-bound test corpus (>= 5 termdeck-tagged rows
|
|
235
|
+
-- matching the canonical probe question):
|
|
236
|
+
-- SELECT count(*) FROM memory_items
|
|
237
|
+
-- WHERE project='termdeck' AND content ILIKE '%shell error%';
|