@jhizzard/termdeck 1.1.0 → 1.1.1
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 +57 -0
- package/packages/server/src/index.js +19 -1
- package/packages/server/src/setup/migrations.js +27 -1
- package/packages/server/src/setup/mnestra-migrations/021_project_tag_canonicalize_claimguard.sql +175 -0
- package/packages/server/src/setup/mnestra-migrations/022_source_agent_backfill.sql +182 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
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"
|
|
@@ -129,6 +129,12 @@
|
|
|
129
129
|
// Sprint 37 T1: orchestrator Guide right-rail. Lazy — fetches the doc
|
|
130
130
|
// on first expand to keep page load light.
|
|
131
131
|
setupGuideRail();
|
|
132
|
+
|
|
133
|
+
// 2026-05-08 hotfix: document-level capture-phase image-paste handler.
|
|
134
|
+
// Intercepts Cmd+V image data before xterm-helper-textarea consumes it
|
|
135
|
+
// (xterm reads only text/plain, drops images silently). See comment on
|
|
136
|
+
// setupGlobalImagePaste() near uploadFilesAndType() for details.
|
|
137
|
+
setupGlobalImagePaste();
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
// ===== Drag/drop reorder of PTY panels (Sprint 42 T4) =====
|
|
@@ -275,6 +281,57 @@
|
|
|
275
281
|
}
|
|
276
282
|
}
|
|
277
283
|
|
|
284
|
+
// Document-level capture-phase image paste handler.
|
|
285
|
+
//
|
|
286
|
+
// The Sprint 59 per-panel `paste` listener at line 218 is bubble-phase, but
|
|
287
|
+
// xterm.js@5.5.0's hidden helper-textarea has its own `paste` handler that
|
|
288
|
+
// reads only `clipboardData.getData('text/plain')`. Image data lives in
|
|
289
|
+
// `clipboardData.items` with `kind: 'file'` and never reaches xterm's
|
|
290
|
+
// text path — and the panel-level bubble-phase handler runs after xterm's,
|
|
291
|
+
// by which point xterm has already returned (silently dropping the image).
|
|
292
|
+
// Net: pre-fix, Cmd+V'ing a screenshot into a focused TermDeck panel did
|
|
293
|
+
// nothing. Joshua reported this on 2026-05-08 (post-v1.1.0 upgrade).
|
|
294
|
+
//
|
|
295
|
+
// Fix: document-level listener with `{capture: true}` runs in capture
|
|
296
|
+
// phase BEFORE the event reaches xterm-helper-textarea. If the event
|
|
297
|
+
// target is inside a `.term-panel` AND the clipboard contains image
|
|
298
|
+
// files, we preventDefault + stopPropagation (so xterm + the bubble-phase
|
|
299
|
+
// panel handler don't see it) and route through `uploadFilesAndType`.
|
|
300
|
+
// For text paste (no image files) we let the event continue normally.
|
|
301
|
+
//
|
|
302
|
+
// Idempotent: setupGlobalImagePaste() is called once from init().
|
|
303
|
+
let _globalImagePasteSetup = false;
|
|
304
|
+
function setupGlobalImagePaste() {
|
|
305
|
+
if (_globalImagePasteSetup) return;
|
|
306
|
+
_globalImagePasteSetup = true;
|
|
307
|
+
document.addEventListener('paste', (e) => {
|
|
308
|
+
const target = e.target;
|
|
309
|
+
if (!(target instanceof Element)) return;
|
|
310
|
+
const panel = target.closest('.term-panel');
|
|
311
|
+
if (!panel) return;
|
|
312
|
+
const items = (e.clipboardData && e.clipboardData.items) || [];
|
|
313
|
+
const blobs = [];
|
|
314
|
+
for (const item of items) {
|
|
315
|
+
if (item.kind === 'file' && item.type && item.type.startsWith('image/')) {
|
|
316
|
+
const blob = item.getAsFile();
|
|
317
|
+
if (blob) blobs.push(blob);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (blobs.length === 0) return;
|
|
321
|
+
e.preventDefault();
|
|
322
|
+
e.stopPropagation();
|
|
323
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
324
|
+
const named = blobs.map((b, i) => {
|
|
325
|
+
const ext = (b.type.split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '');
|
|
326
|
+
const name = b.name && b.name.length > 0
|
|
327
|
+
? b.name
|
|
328
|
+
: `pasted-${ts}${blobs.length > 1 ? '-' + i : ''}.${ext}`;
|
|
329
|
+
return new File([b], name, { type: b.type });
|
|
330
|
+
});
|
|
331
|
+
uploadFilesAndType(panel, named);
|
|
332
|
+
}, { capture: true });
|
|
333
|
+
}
|
|
334
|
+
|
|
278
335
|
// ===== Create Terminal Panel =====
|
|
279
336
|
function createTerminalPanel(sessionData) {
|
|
280
337
|
const id = sessionData.id;
|
|
@@ -16,7 +16,25 @@ const { createCachedLookup, createFailureLogger } = require('./rumen-pool-resili
|
|
|
16
16
|
// Conditional imports (graceful fallback if not installed yet)
|
|
17
17
|
let pty, Database, pg;
|
|
18
18
|
try { pty = require('@homebridge/node-pty-prebuilt-multiarch'); } catch { pty = null; }
|
|
19
|
-
try {
|
|
19
|
+
try {
|
|
20
|
+
Database = require('better-sqlite3');
|
|
21
|
+
} catch (err) {
|
|
22
|
+
// Brad Heath 2026-05-11: distinguish a native-ABI mismatch (Node upgraded
|
|
23
|
+
// after install) from "package not installed yet." ABI mismatch leaves
|
|
24
|
+
// Database=null and cascades into a null-handle storm downstream that
|
|
25
|
+
// masquerades as "Mnestra unreachable / DB timeout" in health probes.
|
|
26
|
+
// Fail fast with the actionable rebuild hint instead.
|
|
27
|
+
const msg = err && err.message ? String(err.message) : '';
|
|
28
|
+
if (err && err.code === 'ERR_DLOPEN_FAILED' && /NODE_MODULE_VERSION/.test(msg)) {
|
|
29
|
+
console.error('[db] better-sqlite3 native ABI mismatch (Node was upgraded after install).');
|
|
30
|
+
console.error('[db] TermDeck cannot serve memory features without a working SQLite.');
|
|
31
|
+
console.error('[db] Fix:');
|
|
32
|
+
console.error(' cd "$(npm root -g)/@jhizzard/termdeck" && npm rebuild better-sqlite3');
|
|
33
|
+
console.error('[db] Then restart TermDeck. Aborting.');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
Database = null;
|
|
37
|
+
}
|
|
20
38
|
try { pg = require('pg'); } catch { pg = null; }
|
|
21
39
|
|
|
22
40
|
// Module-level singleton Postgres pool for rumen_insights (petvetbid DB).
|
|
@@ -110,7 +110,33 @@ const MIGRATION_PROBES = Object.freeze({
|
|
|
110
110
|
'018_rumen_processed_at.sql':
|
|
111
111
|
"select 1 from information_schema.columns where table_schema='public' and table_name='memory_sessions' and column_name='rumen_processed_at'",
|
|
112
112
|
'019_security_hardening.sql':
|
|
113
|
-
"select 1 from pg_proc p, unnest(coalesce(p.proconfig,'{}'::text[])) c where p.proname='memory_hybrid_search' and c like 'search_path=%' and c like '%extensions%'"
|
|
113
|
+
"select 1 from pg_proc p, unnest(coalesce(p.proconfig,'{}'::text[])) c where p.proname='memory_hybrid_search' and c like 'search_path=%' and c like '%extensions%'",
|
|
114
|
+
// 021 canonicalizes legacy gorgias / gorgias-ticket-monitor project tags to
|
|
115
|
+
// claimguard. Probe is NOT-EXISTS-shaped: returns 1 row when both legacy
|
|
116
|
+
// tags carry zero rows (021's effects are in place OR the install never had
|
|
117
|
+
// legacy data). Returns 0 rows when at least one legacy tag still has rows
|
|
118
|
+
// (021 has not yet run). False-positive backfill costs nothing because the
|
|
119
|
+
// migration's UPDATE is gated on `project IN ('gorgias', 'gorgias-ticket-monitor')`
|
|
120
|
+
// so a re-apply against an already-canonicalized corpus is a 0-row no-op.
|
|
121
|
+
// Sprint 62 T2 added this; 020 is bootstrap-special-cased and intentionally
|
|
122
|
+
// absent from MIGRATION_PROBES.
|
|
123
|
+
'021_project_tag_canonicalize_claimguard.sql':
|
|
124
|
+
"select 1 where not exists (select 1 from memory_items where project in ('gorgias', 'gorgias-ticket-monitor'))",
|
|
125
|
+
// 022 backfills source_agent for the rows where the writer is inferable from
|
|
126
|
+
// row shape (Predicate A: decision/bug_fix/architecture/preference/code_context
|
|
127
|
+
// → 'claude'; Predicate B: fact rows with source_session_id → 'claude';
|
|
128
|
+
// Predicate D: document_chunk → 'orchestrator'). Predicate C (fact rows
|
|
129
|
+
// with no session and no path) is intentionally NOT backfilled — see the
|
|
130
|
+
// migration body for the provenance-preservation rationale. Probe is
|
|
131
|
+
// NOT-EXISTS-shaped over the A/B/D row-set: returns 1 when those targets
|
|
132
|
+
// all have source_agent set (022's effects in place), 0 when any A/B/D
|
|
133
|
+
// target still has NULL (022 has not yet run). Excludes Predicate C from
|
|
134
|
+
// the probe predicate so the residual NULL slice doesn't keep the probe
|
|
135
|
+
// false forever. False-positive backfill costs nothing because the
|
|
136
|
+
// migration body is gated on `source_agent IS NULL` and a re-apply against
|
|
137
|
+
// an already-tagged corpus is a 0-row no-op. Sprint 62 T3.
|
|
138
|
+
'022_source_agent_backfill.sql':
|
|
139
|
+
"select 1 where not exists (select 1 from memory_items where source_agent is null and (source_type in ('decision','bug_fix','architecture','preference','code_context') or (source_type='fact' and source_session_id is not null) or source_type='document_chunk'))"
|
|
114
140
|
});
|
|
115
141
|
|
|
116
142
|
// Sprint 61 T2 — self-transactional detection.
|
package/packages/server/src/setup/mnestra-migrations/021_project_tag_canonicalize_claimguard.sql
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
-- 021_project_tag_canonicalize_claimguard.sql
|
|
2
|
+
-- Sprint 62 T2 — finishes the gorgias / gorgias-ticket-monitor → claimguard
|
|
3
|
+
-- rename that migration 012 (Sprint 41 T2) explicitly scoped out.
|
|
4
|
+
--
|
|
5
|
+
-- Why this exists:
|
|
6
|
+
-- Same project (the ClaimGuard repo at ~/Documents/Unagi/gorgias-ticket-monitor)
|
|
7
|
+
-- was tagged three ways across history. As of 2026-05-08:
|
|
8
|
+
-- - 'claimguard' ~29 rows (newest tag, written by the
|
|
9
|
+
-- post-Sprint-41 PROJECT_MAP)
|
|
10
|
+
-- - 'gorgias-ticket-monitor' ~245 rows (mid tag, the on-disk dir name)
|
|
11
|
+
-- - 'gorgias' ~541 rows (oldest tag, pre-Sprint-41)
|
|
12
|
+
--
|
|
13
|
+
-- Migration 012's §"What this migration does NOT do" called out the merge
|
|
14
|
+
-- as a separate cleanup pass:
|
|
15
|
+
--
|
|
16
|
+
-- - Does NOT consolidate duplicate tags like 'gorgias' vs
|
|
17
|
+
-- 'gorgias-ticket-monitor', 'pvb' vs 'PVB', or 'mnestra' vs 'engram'.
|
|
18
|
+
-- Visible in `SELECT project, count(*) FROM memory_items GROUP BY
|
|
19
|
+
-- project` but a separate cleanup pass.
|
|
20
|
+
--
|
|
21
|
+
-- That separate pass is 021. Sprint 21 T2's earlier rename plan never
|
|
22
|
+
-- landed; Sprint 35's harness-hook fix addressed the upstream PROJECT_MAP
|
|
23
|
+
-- so new rows tag correctly, and Sprint 62 T2 (this migration) closes the
|
|
24
|
+
-- historical-corpus gap so memory_recall(project="claimguard") returns the
|
|
25
|
+
-- full ~815-row history rather than just the post-Sprint-41 tail.
|
|
26
|
+
--
|
|
27
|
+
-- The companion T2 invariant test at
|
|
28
|
+
-- termdeck/tests/project-tag-invariant.test.js currently skips the claimguard
|
|
29
|
+
-- invariant via `deferredToSprint35`; with 021 applied that invariant would
|
|
30
|
+
-- pass cleanly if un-deferred. Un-deferring is out of T2's lane (test edits
|
|
31
|
+
-- are owned by orchestrator close-out).
|
|
32
|
+
--
|
|
33
|
+
-- Why the *project*-column merge and not a content-keyword rebucket: rows
|
|
34
|
+
-- already-tagged 'gorgias' or 'gorgias-ticket-monitor' carry definitive
|
|
35
|
+
-- project provenance — the row is from the ClaimGuard project by virtue of
|
|
36
|
+
-- the writer's prior tag, regardless of content keywords. We are not
|
|
37
|
+
-- inferring; we are renaming an exact-match tag set that the SOURCE-BRIEF
|
|
38
|
+
-- and 012's prologue both confirm refer to the same on-disk codebase.
|
|
39
|
+
--
|
|
40
|
+
-- Idempotence:
|
|
41
|
+
-- The UPDATE is gated by `WHERE project IN ('gorgias','gorgias-ticket-monitor')`.
|
|
42
|
+
-- After the first apply those rows carry project='claimguard', so a re-run
|
|
43
|
+
-- matches zero rows — RAISE NOTICE prints 0 and the migration succeeds. The
|
|
44
|
+
-- bundled migration runner (packages/server/src/setup/migration-runner.js)
|
|
45
|
+
-- also checksums applied migrations into mnestra_migrations (table from
|
|
46
|
+
-- 020) and skips re-application by filename, so the in-runner path is
|
|
47
|
+
-- idempotent at two layers.
|
|
48
|
+
--
|
|
49
|
+
-- RLS posture:
|
|
50
|
+
-- memory_items has RLS enabled (per migration 019 security hardening), but
|
|
51
|
+
-- service_role bypasses RLS. The migration runner authenticates as
|
|
52
|
+
-- service_role via DATABASE_URL, so the UPDATE lands without policy
|
|
53
|
+
-- changes. This migration does NOT touch policies or roles.
|
|
54
|
+
--
|
|
55
|
+
-- Reversibility:
|
|
56
|
+
-- Down-migration is documented at the bottom (commented). Splitting the
|
|
57
|
+
-- merged set back into three is destructive — once project='claimguard'
|
|
58
|
+
-- replaces the prior values, the row provenance for which tag it ORIGINALLY
|
|
59
|
+
-- carried is gone (no audit column tracks pre-image). Reversal requires
|
|
60
|
+
-- restore from a pg_dump snapshot taken before the migration was applied.
|
|
61
|
+
-- Do NOT attempt heuristic reversal.
|
|
62
|
+
--
|
|
63
|
+
-- Application:
|
|
64
|
+
-- Applied via the bundled migration runner using node-postgres
|
|
65
|
+
-- client.query(). DO blocks + GET DIAGNOSTICS ROW_COUNT (no psql
|
|
66
|
+
-- metacommands — \gset / \echo / etc are not supported in client.query).
|
|
67
|
+
-- Manual fallback: `psql "$DATABASE_URL" -f 021_project_tag_canonicalize_claimguard.sql`.
|
|
68
|
+
|
|
69
|
+
BEGIN;
|
|
70
|
+
|
|
71
|
+
-- ============================================================
|
|
72
|
+
-- AUDIT BEFORE
|
|
73
|
+
-- ============================================================
|
|
74
|
+
DO $$
|
|
75
|
+
DECLARE
|
|
76
|
+
before_claimguard int;
|
|
77
|
+
before_gorgias int;
|
|
78
|
+
before_gorgias_ticket_monitor int;
|
|
79
|
+
before_total_three int;
|
|
80
|
+
BEGIN
|
|
81
|
+
SELECT count(*) INTO before_claimguard
|
|
82
|
+
FROM public.memory_items WHERE project = 'claimguard';
|
|
83
|
+
SELECT count(*) INTO before_gorgias
|
|
84
|
+
FROM public.memory_items WHERE project = 'gorgias';
|
|
85
|
+
SELECT count(*) INTO before_gorgias_ticket_monitor
|
|
86
|
+
FROM public.memory_items WHERE project = 'gorgias-ticket-monitor';
|
|
87
|
+
before_total_three := before_claimguard + before_gorgias + before_gorgias_ticket_monitor;
|
|
88
|
+
RAISE NOTICE '[021-canonicalize] BEFORE claimguard=% gorgias=% gorgias-ticket-monitor=% (sum=%)',
|
|
89
|
+
before_claimguard, before_gorgias, before_gorgias_ticket_monitor, before_total_three;
|
|
90
|
+
END $$;
|
|
91
|
+
|
|
92
|
+
-- ============================================================
|
|
93
|
+
-- CANONICALIZE — gorgias + gorgias-ticket-monitor → claimguard
|
|
94
|
+
--
|
|
95
|
+
-- Single-statement UPDATE on the project column. No content scoping required:
|
|
96
|
+
-- the source tags refer unambiguously to the ClaimGuard project per Sprint 41
|
|
97
|
+
-- T2's analysis (012's prologue) and the SOURCE-BRIEF for Sprint 62 §1.
|
|
98
|
+
-- ============================================================
|
|
99
|
+
DO $$
|
|
100
|
+
DECLARE
|
|
101
|
+
affected_count integer;
|
|
102
|
+
BEGIN
|
|
103
|
+
UPDATE public.memory_items
|
|
104
|
+
SET project = 'claimguard'
|
|
105
|
+
WHERE project IN ('gorgias', 'gorgias-ticket-monitor');
|
|
106
|
+
GET DIAGNOSTICS affected_count = ROW_COUNT;
|
|
107
|
+
RAISE NOTICE '[021-canonicalize] canonicalized % memory_items rows (gorgias + gorgias-ticket-monitor) -> claimguard',
|
|
108
|
+
affected_count;
|
|
109
|
+
END $$;
|
|
110
|
+
|
|
111
|
+
-- ============================================================
|
|
112
|
+
-- AUDIT AFTER + CONSERVATION CHECK
|
|
113
|
+
-- ============================================================
|
|
114
|
+
DO $$
|
|
115
|
+
DECLARE
|
|
116
|
+
after_claimguard int;
|
|
117
|
+
after_gorgias int;
|
|
118
|
+
after_gorgias_ticket_monitor int;
|
|
119
|
+
BEGIN
|
|
120
|
+
SELECT count(*) INTO after_claimguard
|
|
121
|
+
FROM public.memory_items WHERE project = 'claimguard';
|
|
122
|
+
SELECT count(*) INTO after_gorgias
|
|
123
|
+
FROM public.memory_items WHERE project = 'gorgias';
|
|
124
|
+
SELECT count(*) INTO after_gorgias_ticket_monitor
|
|
125
|
+
FROM public.memory_items WHERE project = 'gorgias-ticket-monitor';
|
|
126
|
+
RAISE NOTICE '[021-canonicalize] AFTER claimguard=% gorgias=% gorgias-ticket-monitor=%',
|
|
127
|
+
after_claimguard, after_gorgias, after_gorgias_ticket_monitor;
|
|
128
|
+
IF after_gorgias <> 0 OR after_gorgias_ticket_monitor <> 0 THEN
|
|
129
|
+
RAISE EXCEPTION
|
|
130
|
+
'[021-canonicalize] post-apply invariant violated: expected zero rows in gorgias / gorgias-ticket-monitor, got gorgias=% gorgias-ticket-monitor=%',
|
|
131
|
+
after_gorgias, after_gorgias_ticket_monitor;
|
|
132
|
+
END IF;
|
|
133
|
+
END $$;
|
|
134
|
+
|
|
135
|
+
COMMIT;
|
|
136
|
+
|
|
137
|
+
-- ============================================================
|
|
138
|
+
-- POST-APPLY: verification queries (NOT part of the migration; run separately
|
|
139
|
+
-- to confirm the merge took, the invariant tests stay green, and the recall
|
|
140
|
+
-- path returns the full history). Each query is safe to run repeatedly.
|
|
141
|
+
-- ============================================================
|
|
142
|
+
--
|
|
143
|
+
-- 1. Tag distribution after migration — claimguard should be the only
|
|
144
|
+
-- bucket among the three; gorgias / gorgias-ticket-monitor should be 0:
|
|
145
|
+
-- SELECT project, count(*) FROM public.memory_items
|
|
146
|
+
-- WHERE project IN ('claimguard', 'gorgias', 'gorgias-ticket-monitor')
|
|
147
|
+
-- GROUP BY project ORDER BY project;
|
|
148
|
+
--
|
|
149
|
+
-- 2. Confirm no orphan rows remain under either legacy tag (these should
|
|
150
|
+
-- return 0):
|
|
151
|
+
-- SELECT count(*) FROM public.memory_items
|
|
152
|
+
-- WHERE project IN ('gorgias', 'gorgias-ticket-monitor');
|
|
153
|
+
--
|
|
154
|
+
-- 3. Spot-check that the merged set carries content from all three
|
|
155
|
+
-- historical eras (look for varied dates, varied source_types):
|
|
156
|
+
-- SELECT date_trunc('week', created_at) AS week, count(*)
|
|
157
|
+
-- FROM public.memory_items
|
|
158
|
+
-- WHERE project = 'claimguard'
|
|
159
|
+
-- GROUP BY 1 ORDER BY 1;
|
|
160
|
+
--
|
|
161
|
+
-- 4. Confirm the project-tag invariant test for claimguard would now pass
|
|
162
|
+
-- if un-deferred (rows whose content matches gorgias-ticket-monitor or
|
|
163
|
+
-- Unagi/ identifiers should be top-tagged claimguard):
|
|
164
|
+
-- SELECT project, count(*) FROM public.memory_items
|
|
165
|
+
-- WHERE content ILIKE '%gorgias-ticket-monitor%'
|
|
166
|
+
-- OR content ILIKE '%Unagi/%'
|
|
167
|
+
-- GROUP BY project ORDER BY count(*) DESC LIMIT 5;
|
|
168
|
+
--
|
|
169
|
+
-- DOWN-MIGRATION (manual, NOT auto-applied):
|
|
170
|
+
-- Splitting the merged set back into three is non-trivial (no source-of-
|
|
171
|
+
-- truth on which rows were originally which tag — provenance is lost when
|
|
172
|
+
-- the UPDATE replaces the project string). If a roll-back is needed,
|
|
173
|
+
-- restore from a pg_dump taken before this migration was applied. Do NOT
|
|
174
|
+
-- attempt to reverse via heuristic — the row provenance is destroyed by
|
|
175
|
+
-- the merge.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
-- 022_source_agent_backfill.sql
|
|
2
|
+
-- Sprint 62 T3 (TermDeck) — backfill source_agent for pre-Sprint-50 NULL rows
|
|
3
|
+
-- where the writer can be inferred from row shape, NOT from content content-marker
|
|
4
|
+
-- inspection. Mnestra 0.4.9 (release-pending; orchestrator bumps at sprint close).
|
|
5
|
+
--
|
|
6
|
+
-- Why this exists:
|
|
7
|
+
-- Sprint 50 introduced source_agent (migration 015). Pre-Sprint-50 rows
|
|
8
|
+
-- have source_agent IS NULL and are silently excluded from filtered
|
|
9
|
+
-- memory_recall queries (per the recall tool's docstring: "NULL-source-
|
|
10
|
+
-- agent rows ... are excluded when this filter is set" — see
|
|
11
|
+
-- src/recall.ts:165-169).
|
|
12
|
+
--
|
|
13
|
+
-- 2026-05-08 production probe: 6,381 of 6,483 active memory_items rows
|
|
14
|
+
-- (~98%) have source_agent IS NULL — far above the SOURCE-BRIEF estimate
|
|
15
|
+
-- of "3,000+". Filtered recall has been blind to most of the corpus for
|
|
16
|
+
-- roughly the entire post-Sprint-50 window.
|
|
17
|
+
--
|
|
18
|
+
-- Migration 015 already backfilled session_summary NULL rows -> 'claude'
|
|
19
|
+
-- (015 lines 48-51), so the NULL universe today is exclusively non-
|
|
20
|
+
-- session_summary types. This migration closes the slice where the
|
|
21
|
+
-- writer can be inferred from row shape (architectural / schema /
|
|
22
|
+
-- structural evidence), and deliberately leaves the remaining slice
|
|
23
|
+
-- NULL — to be reached via the additive include_null_source recall
|
|
24
|
+
-- flag rather than by speculative attribution.
|
|
25
|
+
--
|
|
26
|
+
-- Design principle: row-shape attribution, not content-marker attribution.
|
|
27
|
+
-- The original SOURCE-BRIEF proposed content-marker predicates (ILIKE
|
|
28
|
+
-- '%[T-CODEX]%' etc). Sampling proved this unsafe: 100% of NULL rows
|
|
29
|
+
-- matching codex/gemini/grok markers are Claude *describing* those
|
|
30
|
+
-- agents, never authored by them. Marker == "row mentions agent",
|
|
31
|
+
-- not "row authored by agent".
|
|
32
|
+
--
|
|
33
|
+
-- Instead, this migration attributes by the (source_type, has_path,
|
|
34
|
+
-- has_session) tuple — schema-level fingerprints that map 1:1 to the
|
|
35
|
+
-- writer architecture, and that 50+ randomly-sampled rows confirm.
|
|
36
|
+
--
|
|
37
|
+
-- Predicate plan (each with explicit evidence chain):
|
|
38
|
+
--
|
|
39
|
+
-- A. NULL + source_type IN (decision, bug_fix, architecture, preference,
|
|
40
|
+
-- code_context) -> 'claude'.
|
|
41
|
+
-- Architectural evidence: pre-Sprint-50, only Claude shipped a
|
|
42
|
+
-- memory_remember client. The mcp__memory__memory_remember and
|
|
43
|
+
-- mcp__mnestra__memory_remember surfaces both ran exclusively in
|
|
44
|
+
-- Claude sessions. Codex/Gemini/Grok memory_remember capabilities
|
|
45
|
+
-- did not exist until the Sprint 51 per-agent MCP wiring (see
|
|
46
|
+
-- memory: "MCP server wiring patterns for Codex, Gemini, and Grok
|
|
47
|
+
-- CLIs (verified 2026-05-04 ... follow-up to Sprint 51.6's "Codex
|
|
48
|
+
-- MCP not wired" gap)"). All NULL rows of these source_types are
|
|
49
|
+
-- pre-Sprint-50 and therefore architecturally Claude.
|
|
50
|
+
-- Schema fingerprint: 100% of these rows have source_file_path IS NULL
|
|
51
|
+
-- AND source_session_id IS NULL — bare memory_remember shape.
|
|
52
|
+
-- Sample confirmation: 28-row sample showed 100% Claude-summary writing
|
|
53
|
+
-- pattern (project context, dated entries, file:line evidence — the
|
|
54
|
+
-- recognizable Claude memory_remember signature).
|
|
55
|
+
-- Expected count: 560.
|
|
56
|
+
--
|
|
57
|
+
-- B. NULL + source_type='fact' + source_session_id IS NOT NULL -> 'claude'.
|
|
58
|
+
-- Schema evidence: source_session_id is a Claude session UUID format
|
|
59
|
+
-- (matches the existing claude/session_summary tagged rows; same
|
|
60
|
+
-- shape: has_path=false, has_session=true). The Claude SessionEnd
|
|
61
|
+
-- hook is the only writer that populates source_session_id with a
|
|
62
|
+
-- Claude UUID. Other writers either set source_file_path (rag-extractor)
|
|
63
|
+
-- or leave both NULL (bare memory_remember).
|
|
64
|
+
-- Expected count: 4,587.
|
|
65
|
+
--
|
|
66
|
+
-- D. NULL + source_type='document_chunk' -> 'orchestrator'.
|
|
67
|
+
-- Structural evidence: 951/951 rows have source_file_path set + JSONB
|
|
68
|
+
-- metadata containing chunkIndex + heading keys — unmistakable
|
|
69
|
+
-- rag-system batch-chunker output. The chunker is not an LLM session;
|
|
70
|
+
-- 'orchestrator' is the appropriate non-LLM tag per the source_agent
|
|
71
|
+
-- enum (claude|codex|gemini|grok|orchestrator).
|
|
72
|
+
-- Path buckets:
|
|
73
|
+
-- 513 rows ~/.gemini/antigravity/scratch/* (Gemini scratch docs the
|
|
74
|
+
-- rag-extractor ingested — Gemini wrote the source MD,
|
|
75
|
+
-- but the rag-extractor wrote the row.)
|
|
76
|
+
-- 429 rows ~/Documents/* (project docs ingested directly).
|
|
77
|
+
-- 9 rows ~/.claude/projects/*/memory/MEMORY.md (auto-memory MD
|
|
78
|
+
-- ingested by the rag-extractor).
|
|
79
|
+
-- All four buckets are extractor-written, not LLM-written. The
|
|
80
|
+
-- original document author is preserved in source_file_path; the
|
|
81
|
+
-- row writer is the extractor.
|
|
82
|
+
-- Expected count: 951.
|
|
83
|
+
--
|
|
84
|
+
-- Predicate deliberately NOT applied (response to T4-CODEX 20:43 ET concern):
|
|
85
|
+
-- C. NULL + source_type='fact' + source_session_id IS NULL +
|
|
86
|
+
-- source_file_path IS NULL.
|
|
87
|
+
-- These 283 rows are bare memory_remember calls without session
|
|
88
|
+
-- attribution. Sampling (10 rows) showed 100% Claude content pattern,
|
|
89
|
+
-- but they lack the schema fingerprint that makes A/B/D structurally
|
|
90
|
+
-- definitive — there is no architectural lock that PREVENTS a
|
|
91
|
+
-- non-Claude writer from producing this shape (e.g., a manual psql
|
|
92
|
+
-- insert, a non-MCP REST call, or an early rag-extractor variant
|
|
93
|
+
-- that omitted source_file_path).
|
|
94
|
+
-- Migration 015 lines 24-30 explicitly preserved provenance
|
|
95
|
+
-- uncertainty for non-session_summary historical rows; broad
|
|
96
|
+
-- attribution here would erase that bright line. Per T4-CODEX
|
|
97
|
+
-- AUDIT-CONCERN (Sprint 62, 20:43 ET), these rows stay NULL and
|
|
98
|
+
-- are reached via the additive include_null_source recall path
|
|
99
|
+
-- added in src/recall.ts under this same sprint.
|
|
100
|
+
-- Residual NULL after this migration: 283 rows = 4.4% of corpus.
|
|
101
|
+
-- Acceptance target: <5%. Met.
|
|
102
|
+
--
|
|
103
|
+
-- Total backfill: 6,098 rows (A + B + D). Acceptance: residual NULL < 5%
|
|
104
|
+
-- of corpus (4.4% expected; well under threshold).
|
|
105
|
+
--
|
|
106
|
+
-- What this migration deliberately does NOT do:
|
|
107
|
+
-- * Touch session_summary rows (015 already attributed those).
|
|
108
|
+
-- * Touch already-tagged rows (every UPDATE is gated by source_agent IS NULL).
|
|
109
|
+
-- * Use content-marker predicates (sampling proved unreliable; markers
|
|
110
|
+
-- describe agents, not authors).
|
|
111
|
+
-- * Backfill the inferential-only slice (Predicate C, see above).
|
|
112
|
+
--
|
|
113
|
+
-- Idempotent: every UPDATE has WHERE source_agent IS NULL, so re-running
|
|
114
|
+
-- is a no-op on already-tagged rows. Safe to re-apply.
|
|
115
|
+
--
|
|
116
|
+
-- Reversibility: this migration tags rows but does not modify content,
|
|
117
|
+
-- type, or any other column. To revert (in a future migration), run:
|
|
118
|
+
-- UPDATE public.memory_items
|
|
119
|
+
-- SET source_agent = NULL
|
|
120
|
+
-- WHERE source_agent IN ('claude', 'orchestrator')
|
|
121
|
+
-- AND created_at < '2026-05-09'
|
|
122
|
+
-- AND source_type != 'session_summary'; -- preserve 015's backfill
|
|
123
|
+
--
|
|
124
|
+
-- RLS posture (per global CLAUDE.md RLS hygiene gates 1-5): this is a
|
|
125
|
+
-- DO block, not a CREATE FUNCTION. Runs as the migration runner's role
|
|
126
|
+
-- (service_role, which bypasses RLS). search_path is set explicitly to
|
|
127
|
+
-- defend against schema-shadow attacks during execution. No new policies,
|
|
128
|
+
-- no new function executable surface.
|
|
129
|
+
|
|
130
|
+
set search_path = public, pg_catalog;
|
|
131
|
+
|
|
132
|
+
do $$
|
|
133
|
+
declare
|
|
134
|
+
pred_a integer := 0;
|
|
135
|
+
pred_b integer := 0;
|
|
136
|
+
pred_d integer := 0;
|
|
137
|
+
remaining integer;
|
|
138
|
+
total_rows integer;
|
|
139
|
+
begin
|
|
140
|
+
-- Predicate A: structural attribution by source_type for non-fact, non-document_chunk
|
|
141
|
+
-- types. Architectural lock: pre-Sprint-50 only Claude shipped a memory_remember
|
|
142
|
+
-- client. NULL rows of these types are therefore unambiguously Claude.
|
|
143
|
+
update public.memory_items
|
|
144
|
+
set source_agent = 'claude'
|
|
145
|
+
where source_agent is null
|
|
146
|
+
and source_type in ('decision', 'bug_fix', 'architecture', 'preference', 'code_context');
|
|
147
|
+
get diagnostics pred_a = row_count;
|
|
148
|
+
|
|
149
|
+
-- Predicate B: fact rows with Claude-session attribution. source_session_id
|
|
150
|
+
-- is the Claude SessionEnd hook's UUID; same shape as the existing tagged
|
|
151
|
+
-- claude/session_summary rows.
|
|
152
|
+
update public.memory_items
|
|
153
|
+
set source_agent = 'claude'
|
|
154
|
+
where source_agent is null
|
|
155
|
+
and source_type = 'fact'
|
|
156
|
+
and source_session_id is not null;
|
|
157
|
+
get diagnostics pred_b = row_count;
|
|
158
|
+
|
|
159
|
+
-- Predicate D: rag-system document chunks -> 'orchestrator' (non-LLM batch writer).
|
|
160
|
+
-- All 951 rows carry source_file_path + chunkIndex/heading metadata — the
|
|
161
|
+
-- rag-extractor's deterministic fingerprint.
|
|
162
|
+
update public.memory_items
|
|
163
|
+
set source_agent = 'orchestrator'
|
|
164
|
+
where source_agent is null
|
|
165
|
+
and source_type = 'document_chunk';
|
|
166
|
+
get diagnostics pred_d = row_count;
|
|
167
|
+
|
|
168
|
+
select count(*) into remaining
|
|
169
|
+
from public.memory_items
|
|
170
|
+
where source_agent is null;
|
|
171
|
+
|
|
172
|
+
select count(*) into total_rows from public.memory_items;
|
|
173
|
+
|
|
174
|
+
raise notice '[022] backfill complete: A(claude/typed)=% B(claude/fact+session)=% D(orchestrator/doc_chunk)=% remaining_null=% / % total (acceptance: <5%%)',
|
|
175
|
+
pred_a, pred_b, pred_d, remaining, total_rows;
|
|
176
|
+
raise notice '[022] residual NULL = bare memory_remember fact rows (no session, no path); reach via include_null_source recall flag';
|
|
177
|
+
end$$;
|
|
178
|
+
|
|
179
|
+
-- Refresh the column comment to reflect 015 + 022 together as the partial-
|
|
180
|
+
-- backfill story, and document the residual + the recall flag escape hatch.
|
|
181
|
+
comment on column public.memory_items.source_agent is
|
|
182
|
+
'Agent that produced this memory: claude|codex|gemini|grok|orchestrator|NULL. Populated at write time by per-agent SessionEnd writers from Sprint 50 onward. Pre-Sprint-50 NULL rows backfilled by migration 015 (session_summary -> claude) and migration 022 (decision/bug_fix/architecture/preference/code_context -> claude; fact w/ source_session_id -> claude; document_chunk -> orchestrator). Residual NULL = bare-call fact rows without session or path attribution; intentionally preserved per migration 015''s provenance bright line. Reach those via memory_recall include_null_source=true.';
|