@jhizzard/termdeck 1.8.1 → 1.10.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 +2 -1
- package/packages/cli/assets/supervise/com.jhizzard.termdeck-supervise.plist +38 -0
- package/packages/cli/assets/supervise/termdeck-supervise.service +27 -0
- package/packages/cli/assets/supervise/termdeck-supervise.sh +146 -0
- package/packages/cli/assets/supervise/termdeck-supervise.timer +14 -0
- package/packages/cli/src/doctor.js +11 -0
- package/packages/cli/src/index.js +15 -2
- package/packages/cli/src/init-bridge.js +1270 -0
- package/packages/cli/src/init-mnestra.js +104 -13
- package/packages/cli/src/init-rumen.js +7 -0
- package/packages/cli/src/init.js +1 -0
- package/packages/client/public/app.js +135 -9
- package/packages/client/public/index.html +1 -0
- package/packages/client/public/input-guard.js +192 -0
- package/packages/client/public/style.css +63 -0
- package/packages/server/src/agent-adapters/web-chat-grok.js +22 -22
- package/packages/server/src/health.js +21 -3
- package/packages/server/src/index.js +6 -1
- package/packages/server/src/preflight.js +14 -5
- package/packages/server/src/setup/rumen/functions/inbox-promote/index.ts +105 -0
- package/packages/server/src/setup/rumen/functions/inbox-promote/tsconfig.json +14 -0
- package/packages/server/src/setup/supabase-url.js +101 -1
- package/packages/stack-installer/assets/hooks/README.md +25 -15
- package/packages/stack-installer/assets/hooks/memory-pre-compact.js +35 -7
- package/packages/stack-installer/assets/hooks/memory-session-end.js +121 -27
|
@@ -1956,6 +1956,69 @@
|
|
|
1956
1956
|
to { opacity: 1; transform: translateY(0); }
|
|
1957
1957
|
}
|
|
1958
1958
|
|
|
1959
|
+
/* ===== Input-guard toast (Sprint 73 T3, termdeck#12) =====
|
|
1960
|
+
Same chrome as .proactive-toast but red-accented (it reports suppressed
|
|
1961
|
+
input, not a memory hit) and not cursor-pointer (clicking it does
|
|
1962
|
+
nothing; actions are the explicit buttons). */
|
|
1963
|
+
.input-guard-toast {
|
|
1964
|
+
position: absolute;
|
|
1965
|
+
right: 10px;
|
|
1966
|
+
bottom: 44px;
|
|
1967
|
+
max-width: 320px;
|
|
1968
|
+
padding: 8px 10px 8px 12px;
|
|
1969
|
+
background: rgba(23, 15, 15, 0.95);
|
|
1970
|
+
border: 1px solid var(--tg-accent-dim);
|
|
1971
|
+
border-left: 3px solid #f7768e;
|
|
1972
|
+
border-radius: var(--tg-radius-sm);
|
|
1973
|
+
box-shadow: 0 6px 18px rgba(0,0,0,0.35);
|
|
1974
|
+
color: var(--tg-text);
|
|
1975
|
+
font-size: 11px;
|
|
1976
|
+
z-index: 26;
|
|
1977
|
+
animation: toast-in 0.18s ease;
|
|
1978
|
+
}
|
|
1979
|
+
.input-guard-toast .t-title {
|
|
1980
|
+
font-size: 10px;
|
|
1981
|
+
text-transform: uppercase;
|
|
1982
|
+
letter-spacing: 0.4px;
|
|
1983
|
+
color: #f7768e;
|
|
1984
|
+
margin-bottom: 3px;
|
|
1985
|
+
}
|
|
1986
|
+
.input-guard-toast .t-body {
|
|
1987
|
+
font-size: 11px;
|
|
1988
|
+
line-height: 1.35;
|
|
1989
|
+
color: var(--tg-text);
|
|
1990
|
+
}
|
|
1991
|
+
.input-guard-toast .t-meta {
|
|
1992
|
+
margin-top: 4px;
|
|
1993
|
+
font-size: 9px;
|
|
1994
|
+
color: var(--tg-text-dim);
|
|
1995
|
+
}
|
|
1996
|
+
.input-guard-toast .t-dismiss {
|
|
1997
|
+
position: absolute;
|
|
1998
|
+
top: 2px;
|
|
1999
|
+
right: 4px;
|
|
2000
|
+
background: none;
|
|
2001
|
+
border: none;
|
|
2002
|
+
color: var(--tg-text-dim);
|
|
2003
|
+
cursor: pointer;
|
|
2004
|
+
font-size: 12px;
|
|
2005
|
+
padding: 0 4px;
|
|
2006
|
+
}
|
|
2007
|
+
.input-guard-toast .t-dismiss:hover { color: var(--tg-text); }
|
|
2008
|
+
.input-guard-toast .t-send-anyway {
|
|
2009
|
+
margin-top: 6px;
|
|
2010
|
+
padding: 2px 8px;
|
|
2011
|
+
background: none;
|
|
2012
|
+
border: 1px solid #f7768e;
|
|
2013
|
+
border-radius: var(--tg-radius-sm);
|
|
2014
|
+
color: #f7768e;
|
|
2015
|
+
font-size: 10px;
|
|
2016
|
+
cursor: pointer;
|
|
2017
|
+
}
|
|
2018
|
+
.input-guard-toast .t-send-anyway:hover {
|
|
2019
|
+
background: rgba(247, 118, 142, 0.15);
|
|
2020
|
+
}
|
|
2021
|
+
|
|
1959
2022
|
/* ===== Flashback modal (Sprint 16 T2) ===== */
|
|
1960
2023
|
.flashback-modal {
|
|
1961
2024
|
display: none;
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
// Grok's reasoning model, which the CLI rejects (`reasoningEffort` → HTTP 400;
|
|
14
14
|
// see grok-models.js). Same provider, different runtime + different cost
|
|
15
15
|
// realization, so it is a separate `sessionType:'web-chat'`. Provenance is
|
|
16
|
-
// tagged `sourceAgent:'grok'`
|
|
17
|
-
//
|
|
16
|
+
// tagged `sourceAgent:'grok-web'` (Sprint 73 T1 — see the "source_agent
|
|
17
|
+
// attribution" section), distinguishing web rows from Grok-CLI rows in Mnestra.
|
|
18
18
|
//
|
|
19
19
|
// ── The one hard constraint: NO node-pty, NO on-disk transcript ──────────────
|
|
20
20
|
// There is no PTY stream and no conversation file on disk. The server seam
|
|
@@ -39,22 +39,22 @@
|
|
|
39
39
|
// error) — index.js does not route web-chat text through `_detectErrors`.
|
|
40
40
|
//
|
|
41
41
|
// ── source_agent attribution ─────────────────────────────────────────────────
|
|
42
|
-
// `sourceAgent:'grok'` (
|
|
43
|
-
//
|
|
44
|
-
// surface
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
42
|
+
// `sourceAgent:'grok-web'` (Sprint 73 T1 — flips the Sprint 72 ORCH zero-touch
|
|
43
|
+
// decision that shipped 'grok' to keep that sprint off the release-sensitive
|
|
44
|
+
// hook surface). Web and CLI Grok rows are now distinguishable in Mnestra:
|
|
45
|
+
// onPanelClose/periodic emit `adapter.sourceAgent || adapter.name`, the bundled
|
|
46
|
+
// hook allow-lists 'grok-web' (stamp v4, plus a `web-chat-grok` registry-name
|
|
47
|
+
// alias as the agy→antigravity-style safety net), and mnestra's source_agents
|
|
48
|
+
// enum + recall filter gain 'grok-web' via migration 024 (Sprint 74 T1 —
|
|
49
|
+
// ATOMIC release partner; neither side ships without the other, else rows are
|
|
50
|
+
// unfilterable or, on a stale installed hook, coerced to 'claude').
|
|
51
51
|
//
|
|
52
|
-
// Byte-floor
|
|
53
|
-
// transcripts < 5 KB unless sessionType is
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
52
|
+
// Byte-floor (shipped with the flip, hook v4): the bundled hook skips
|
|
53
|
+
// transcripts < 5 KB unless the sessionType is exempted. Our materialized
|
|
54
|
+
// envelope is compact — synthesized turn content, no JSONL metadata bloat;
|
|
55
|
+
// 48/49 live Sprint-72 envelopes were <5 KB — so 'web-chat' is exempted
|
|
56
|
+
// alongside 'antigravity', gated on parsed content (≥1 assistant turn)
|
|
57
|
+
// instead of raw bytes.
|
|
58
58
|
//
|
|
59
59
|
// Contract — see ./claude.js header for the full annotated adapter shape.
|
|
60
60
|
|
|
@@ -211,11 +211,11 @@ function bootPromptTemplate(lane = {}, sprint = {}) {
|
|
|
211
211
|
const webChatGrokAdapter = {
|
|
212
212
|
name: 'web-chat-grok',
|
|
213
213
|
sessionType: 'web-chat',
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
sourceAgent: 'grok',
|
|
214
|
+
// Sprint 73 T1 — distinct web provenance (was 'grok', the Sprint 72 ORCH
|
|
215
|
+
// zero-touch decision). Pairs ATOMICALLY with hook v4 (ALLOWED_SOURCE_AGENTS
|
|
216
|
+
// + byte-floor exemption) and mnestra migration 024 (Sprint 74 T1). See the
|
|
217
|
+
// "source_agent attribution" header for the full rationale.
|
|
218
|
+
sourceAgent: 'grok-web',
|
|
219
219
|
// Sprint 50 T3 — human-readable label for launcher buttons + panel headers.
|
|
220
220
|
displayName: 'Grok (Web)',
|
|
221
221
|
// Provider URL the CDP driver navigates the dedicated-profile tab to on
|
|
@@ -64,6 +64,21 @@
|
|
|
64
64
|
|
|
65
65
|
const http = require('http');
|
|
66
66
|
const https = require('https');
|
|
67
|
+
// Sprint 75 T2 (part C): endpoint-shape classifier — could-not-connect
|
|
68
|
+
// envelopes name the IPv6-only direct endpoint as the likely cause when
|
|
69
|
+
// DATABASE_URL has that shape. Warn-only suffix; never changes categories.
|
|
70
|
+
const { classifyDbEndpoint } = require('./setup/supabase-url');
|
|
71
|
+
|
|
72
|
+
// Suffix appended to connect-failure details when DATABASE_URL is the
|
|
73
|
+
// IPv6-only direct endpoint (db.<project-ref>.supabase.co — AAAA-only DNS).
|
|
74
|
+
function directEndpointSuffix(databaseUrl) {
|
|
75
|
+
try {
|
|
76
|
+
if (classifyDbEndpoint(databaseUrl).kind === 'direct') {
|
|
77
|
+
return ' (DATABASE_URL is the IPv6-only db.<project-ref> direct endpoint — on IPv4-only hosts pg clients hang until a pool/connect timeout; use the Shared Pooler URL)';
|
|
78
|
+
}
|
|
79
|
+
} catch (_e) { /* warn-only */ }
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
67
82
|
|
|
68
83
|
const TTL_SECONDS = 30;
|
|
69
84
|
const TTL_MS = TTL_SECONDS * 1000;
|
|
@@ -276,9 +291,10 @@ async function runPgChecks({ databaseUrl, _pgClient }) {
|
|
|
276
291
|
} else {
|
|
277
292
|
// URL set but connect failed → classify by code (timeout vs unreachable).
|
|
278
293
|
const cat = classifyDbFailure(connectEnvelope || {});
|
|
279
|
-
const why = connectEnvelope && connectEnvelope.error
|
|
294
|
+
const why = (connectEnvelope && connectEnvelope.error
|
|
280
295
|
? `could not connect to Postgres using DATABASE_URL — ${connectEnvelope.error}`
|
|
281
|
-
: 'could not connect to Postgres using DATABASE_URL'
|
|
296
|
+
: 'could not connect to Postgres using DATABASE_URL')
|
|
297
|
+
+ directEndpointSuffix(databaseUrl);
|
|
282
298
|
pushPgUnavailableChecks(checks, 'mnestra-pg', cat, why, 'pg unavailable — connect failed');
|
|
283
299
|
}
|
|
284
300
|
return checks;
|
|
@@ -479,7 +495,9 @@ async function checkRumenPool(config, options) {
|
|
|
479
495
|
return warnCheck('rumen-pool', CATEGORIES.DEPENDENCY_DOWN, 'SELECT 1 returned unexpected result');
|
|
480
496
|
} catch (err) {
|
|
481
497
|
const cat = classifyDbFailure(err);
|
|
482
|
-
|
|
498
|
+
const detail = (err && err.message ? err.message : String(err))
|
|
499
|
+
+ directEndpointSuffix(dbUrl);
|
|
500
|
+
return warnCheck('rumen-pool', cat, detail);
|
|
483
501
|
} finally {
|
|
484
502
|
try { await pool.end(); } catch (_e) { /* ignore */ }
|
|
485
503
|
}
|
|
@@ -3467,8 +3467,13 @@ function validateSupabase(url, key) {
|
|
|
3467
3467
|
|
|
3468
3468
|
function validateOpenAI(key) {
|
|
3469
3469
|
return new Promise((resolve) => {
|
|
3470
|
+
// Probe with the EXACT request shape the bundled hooks use in production
|
|
3471
|
+
// (session-end v5: 3-large @ dimensions:1536, recall-parity with mnestra)
|
|
3472
|
+
// so a passing preflight means the real capture pipeline's call works —
|
|
3473
|
+
// not some other model the account may gate differently.
|
|
3470
3474
|
const payload = JSON.stringify({
|
|
3471
|
-
model: 'text-embedding-3-
|
|
3475
|
+
model: 'text-embedding-3-large',
|
|
3476
|
+
dimensions: 1536,
|
|
3472
3477
|
input: 'termdeck setup test'
|
|
3473
3478
|
});
|
|
3474
3479
|
const req = https.request({
|
|
@@ -10,6 +10,9 @@ const http = require('http');
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const os = require('os');
|
|
12
12
|
const path = require('path');
|
|
13
|
+
// Sprint 75 T2 (part C): endpoint-shape classifier — used to explain
|
|
14
|
+
// connect failures against the IPv6-only direct endpoint. Warn-only.
|
|
15
|
+
const { classifyDbEndpoint } = require('./setup/supabase-url');
|
|
13
16
|
|
|
14
17
|
// Cache preflight results for 60s
|
|
15
18
|
let _cachedResult = null;
|
|
@@ -352,10 +355,16 @@ async function runPreflight(config) {
|
|
|
352
355
|
name: 'rumen_recent', passed: false,
|
|
353
356
|
detail: `check failed — ${err.message}`,
|
|
354
357
|
})),
|
|
355
|
-
checkDatabase().catch((err) =>
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
358
|
+
checkDatabase().catch((err) => {
|
|
359
|
+
// Sprint 75 T2 (part C): a connect failure against the IPv6-only
|
|
360
|
+
// direct endpoint (db.<project-ref>.supabase.co) on an IPv4-only
|
|
361
|
+
// host presents as a timeout — name the likely cause in the detail.
|
|
362
|
+
let detail = `connection failed — ${err.message}`;
|
|
363
|
+
if (classifyDbEndpoint(process.env.DATABASE_URL).kind === 'direct') {
|
|
364
|
+
detail += ' (DATABASE_URL is the IPv6-only db.<project-ref> direct endpoint — on IPv4-only hosts pg clients hang until a pool/connect timeout; use the Shared Pooler URL)';
|
|
365
|
+
}
|
|
366
|
+
return { name: 'database_url', passed: false, detail };
|
|
367
|
+
}),
|
|
359
368
|
checkProjectPaths(config).catch((err) => ({
|
|
360
369
|
name: 'project_paths', passed: false,
|
|
361
370
|
detail: `check failed — ${err.message}`,
|
|
@@ -413,7 +422,7 @@ const REMEDIATION = {
|
|
|
413
422
|
mnestra_reachable: 'Start Mnestra with `mnestra serve`',
|
|
414
423
|
mnestra_has_memories: 'Run `mnestra ingest` to populate the memory store',
|
|
415
424
|
rumen_recent: 'Check Rumen Edge Function deployment or run `termdeck init --rumen`',
|
|
416
|
-
database_url: 'Set DATABASE_URL in ~/.termdeck/secrets.env',
|
|
425
|
+
database_url: 'Set DATABASE_URL in ~/.termdeck/secrets.env (IPv4-only hosts: use the Shared Pooler URL)',
|
|
417
426
|
project_paths: 'Fix paths in ~/.termdeck/config.yaml → projects',
|
|
418
427
|
shell_sanity: 'Check $SHELL and your login profile (~/.zshrc or ~/.bashrc)',
|
|
419
428
|
graph_health: 'Run T2 inference cron or apply migrations 009/010 to populate edges',
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Rumen Sprint 76 — inbox-promote Supabase Edge Function entry point.
|
|
2
|
+
//
|
|
3
|
+
// Drains Mnestra's memory_inbox quarantine: web-chat proposals (written via
|
|
4
|
+
// the bridge's memory_propose channel into engram migration 026's table) are
|
|
5
|
+
// promoted to canonical memory_items or rejected with an audit trail. One
|
|
6
|
+
// promotion pass per invocation; see src/promote.ts in @jhizzard/rumen for
|
|
7
|
+
// the gate sequence (caps -> source whitelist -> rate cap -> dedup ->
|
|
8
|
+
// kitchen-vs-recipe) and the doctrine amendment notes.
|
|
9
|
+
//
|
|
10
|
+
// Sibling of rumen-tick by design (NOT a step inside it): budget isolation
|
|
11
|
+
// (the tick already spends its wall-clock on the insight cycle), independent
|
|
12
|
+
// cadence, and failure isolation. Same thin-wrapper pattern: the npm:
|
|
13
|
+
// specifier freezes the package version at DEPLOY time — upgrading
|
|
14
|
+
// @jhizzard/rumen does nothing until this function is redeployed (the
|
|
15
|
+
// Sprint 66 Brad-Rumen-zero lesson).
|
|
16
|
+
//
|
|
17
|
+
// IMPORTANT: This file targets the Deno runtime, NOT Node. It will not
|
|
18
|
+
// compile under the root tsconfig.json — it is intentionally excluded.
|
|
19
|
+
// A sibling tsconfig.json in this directory keeps the types sane for
|
|
20
|
+
// editors, but the canonical build target is Deno's own type checker
|
|
21
|
+
// (`deno check`) and Supabase's `supabase functions deploy`.
|
|
22
|
+
//
|
|
23
|
+
// Deployment (ORCH at sprint close — deployable, NOT deployed from a lane):
|
|
24
|
+
// supabase functions deploy inbox-promote
|
|
25
|
+
// supabase secrets set DATABASE_URL="$DATABASE_URL" # Shared Pooler IPv4 URL
|
|
26
|
+
// supabase secrets set OPENAI_API_KEY="$OPENAI_API_KEY" # dedup-gate embeddings (3-large@1536)
|
|
27
|
+
// supabase secrets set ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" # kitchen-vs-recipe Haiku gate
|
|
28
|
+
// # Optional tuning (defaults shown):
|
|
29
|
+
// supabase secrets set RUMEN_PROMOTE_BATCH=25
|
|
30
|
+
// supabase secrets set RUMEN_PROMOTE_RATE_CAP_24H=50
|
|
31
|
+
// supabase secrets set RUMEN_PROMOTE_MAX_ATTEMPTS=5
|
|
32
|
+
// supabase secrets set RUMEN_PROMOTE_CLAIM_LEASE_MINUTES=10
|
|
33
|
+
//
|
|
34
|
+
// Both model keys are REQUIRED: without them the pass skips (HTTP 503 below)
|
|
35
|
+
// rather than claiming rows it cannot gate — config absence must not burn
|
|
36
|
+
// promotion attempts across the inbox.
|
|
37
|
+
//
|
|
38
|
+
// Triggered on a schedule by pg_cron — see migrations/003_pg_cron_inbox_promote.sql.
|
|
39
|
+
|
|
40
|
+
// @ts-ignore Deno std import resolved at runtime.
|
|
41
|
+
import { serve } from 'https://deno.land/std@0.224.0/http/server.ts';
|
|
42
|
+
// @ts-ignore npm specifier resolved at runtime. Version is stamped at
|
|
43
|
+
// publish/deploy time by ORCH at sprint close — must be >= 0.6.0, the first
|
|
44
|
+
// version exporting promoteInbox.
|
|
45
|
+
import { promoteInbox, createPoolFromUrl } from 'npm:@jhizzard/rumen@0.6.0';
|
|
46
|
+
|
|
47
|
+
// @ts-ignore Deno global available at runtime.
|
|
48
|
+
declare const Deno: { env: { get: (k: string) => string | undefined } };
|
|
49
|
+
|
|
50
|
+
serve(async (_req: Request) => {
|
|
51
|
+
// Same fallback as rumen-tick: Supabase auto-injects SUPABASE_DB_URL.
|
|
52
|
+
const url = Deno.env.get('DATABASE_URL') ?? Deno.env.get('SUPABASE_DB_URL');
|
|
53
|
+
if (!url) {
|
|
54
|
+
console.error('[rumen-promote] DATABASE_URL / SUPABASE_DB_URL not set in Edge Function secrets');
|
|
55
|
+
return new Response(
|
|
56
|
+
JSON.stringify({ ok: false, error: 'DATABASE_URL not set' }),
|
|
57
|
+
{ status: 500, headers: { 'Content-Type': 'application/json' } },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const pool = createPoolFromUrl(url);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
console.log('[rumen-promote] edge function pass starting');
|
|
65
|
+
const summary = await promoteInbox(pool);
|
|
66
|
+
|
|
67
|
+
if (summary.skipped_reason) {
|
|
68
|
+
// Config-level skip (missing model key, rate-accounting failure):
|
|
69
|
+
// surface as a non-200 so pg_cron/operator dashboards notice, but the
|
|
70
|
+
// inbox is untouched and simply drains on a later pass.
|
|
71
|
+
console.error('[rumen-promote] pass skipped: ' + summary.skipped_reason);
|
|
72
|
+
return new Response(
|
|
73
|
+
JSON.stringify({ ok: false, skipped: summary.skipped_reason, summary }),
|
|
74
|
+
{ status: 503, headers: { 'Content-Type': 'application/json' } },
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(
|
|
79
|
+
'[rumen-promote] edge function pass complete claimed=' +
|
|
80
|
+
summary.claimed +
|
|
81
|
+
' promoted=' +
|
|
82
|
+
summary.promoted +
|
|
83
|
+
' rejected=' +
|
|
84
|
+
summary.rejected,
|
|
85
|
+
);
|
|
86
|
+
// Row-level failures are fail-soft by design — the pass itself succeeded.
|
|
87
|
+
return new Response(
|
|
88
|
+
JSON.stringify({ ok: true, summary }),
|
|
89
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
90
|
+
);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
93
|
+
console.error('[rumen-promote] edge function pass threw:', err);
|
|
94
|
+
return new Response(
|
|
95
|
+
JSON.stringify({ ok: false, error: message }),
|
|
96
|
+
{ status: 500, headers: { 'Content-Type': 'application/json' } },
|
|
97
|
+
);
|
|
98
|
+
} finally {
|
|
99
|
+
try {
|
|
100
|
+
await pool.end();
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error('[rumen-promote] pool.end() failed:', err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"allowImportingTsExtensions": false,
|
|
11
|
+
"types": []
|
|
12
|
+
},
|
|
13
|
+
"include": ["index.ts"]
|
|
14
|
+
}
|
|
@@ -186,6 +186,104 @@ function normalizeDatabaseUrl(url) {
|
|
|
186
186
|
return { url: u.toString(), modified: true };
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// ── DATABASE_URL endpoint-shape classification (Sprint 75 T2) ──────────────
|
|
190
|
+
//
|
|
191
|
+
// Ported from engram src/db-endpoint.ts (Sprint 74 T2 — Brad's Dell R730
|
|
192
|
+
// field report, 2026-06-09). Supabase's direct endpoint
|
|
193
|
+
// `db.<project-ref>.supabase.co` — which also hosts the Dedicated Pooler on
|
|
194
|
+
// :6543 — publishes ONLY an AAAA record. On a host without IPv6 (many CI
|
|
195
|
+
// runners and VPSes) pg clients don't fail fast; they hang until a pool
|
|
196
|
+
// timeout. The IPv4-compatible alternative is the Shared Pooler:
|
|
197
|
+
//
|
|
198
|
+
// postgres://postgres.<project-ref>:<pw>@aws-<n>-<region>.pooler.supabase.com:6543/postgres
|
|
199
|
+
//
|
|
200
|
+
// This classifier lets every DATABASE_URL ingress warn BEFORE the first
|
|
201
|
+
// hang. It never rewrites or rejects anything — `looksLikePostgresUrl`
|
|
202
|
+
// stays the blocking validator; direct URLs remain accepted because
|
|
203
|
+
// IPv6-capable hosts use them legitimately. Warn ≠ reject.
|
|
204
|
+
|
|
205
|
+
const LOCAL_DB_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', '[::1]']);
|
|
206
|
+
|
|
207
|
+
// Classify a raw DATABASE_URL string by endpoint family. Returns
|
|
208
|
+
// { kind, host?, port?, username?, poolerUserMismatch? } where kind is one of:
|
|
209
|
+
// 'absent' — nothing usable provided
|
|
210
|
+
// 'invalid' — set but not parseable as postgres:// / postgresql://
|
|
211
|
+
// 'direct' — db.<project-ref>.supabase.co|in (IPv6-only: AAAA, no A
|
|
212
|
+
// record; covers BOTH :5432 direct and :6543 Dedicated
|
|
213
|
+
// Pooler — same hostname, same IPv4 unreachability)
|
|
214
|
+
// 'shared-pooler' — *.pooler.supabase.com (IPv4-compatible)
|
|
215
|
+
// 'local' — loopback/local Postgres
|
|
216
|
+
// 'other' — self-hosted, RDS, IPv6 literal, … (no Supabase concerns)
|
|
217
|
+
// poolerUserMismatch is true when the host is the Shared Pooler but the
|
|
218
|
+
// username lacks the mandatory `.<project-ref>` suffix — the documented
|
|
219
|
+
// "Tenant or user not found" failure.
|
|
220
|
+
function classifyDbEndpoint(raw) {
|
|
221
|
+
if (raw === undefined || raw === null || typeof raw !== 'string') {
|
|
222
|
+
return { kind: 'absent' };
|
|
223
|
+
}
|
|
224
|
+
const trimmed = stripSurroundingQuotes(raw.trim());
|
|
225
|
+
if (trimmed === '') return { kind: 'absent' };
|
|
226
|
+
|
|
227
|
+
let u;
|
|
228
|
+
try {
|
|
229
|
+
u = new URL(trimmed);
|
|
230
|
+
} catch (_err) {
|
|
231
|
+
return { kind: 'invalid' };
|
|
232
|
+
}
|
|
233
|
+
if (u.protocol !== 'postgres:' && u.protocol !== 'postgresql:') {
|
|
234
|
+
return { kind: 'invalid' };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Normalize: lowercase, drop a trailing FQDN dot.
|
|
238
|
+
const host = u.hostname.toLowerCase().replace(/\.$/, '');
|
|
239
|
+
let username = '';
|
|
240
|
+
try {
|
|
241
|
+
username = decodeURIComponent(u.username);
|
|
242
|
+
} catch (_err) {
|
|
243
|
+
username = u.username;
|
|
244
|
+
}
|
|
245
|
+
const base = { host, port: u.port, username };
|
|
246
|
+
|
|
247
|
+
if (LOCAL_DB_HOSTS.has(host)) return { kind: 'local', ...base };
|
|
248
|
+
|
|
249
|
+
if (/^db\.[a-z0-9-]+\.supabase\.(co|in)$/.test(host)) {
|
|
250
|
+
return { kind: 'direct', ...base };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (host.endsWith('.pooler.supabase.com')) {
|
|
254
|
+
// Shared Pooler logins are `postgres.<project-ref>` — a dotless
|
|
255
|
+
// username means the URL was hand-assembled from direct-connection
|
|
256
|
+
// parts and will fail with "Tenant or user not found".
|
|
257
|
+
const poolerUserMismatch = username !== '' && !username.includes('.');
|
|
258
|
+
return { kind: 'shared-pooler', ...base, poolerUserMismatch };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return { kind: 'other', ...base };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Warning lines for a classification — [] when there is nothing to say.
|
|
265
|
+
// Wording kept byte-similar to engram's doctor probe messages so grep /
|
|
266
|
+
// troubleshooting stays consistent across the stack. Print-only: callers
|
|
267
|
+
// write these to stdout after a PASSING validation and never change exit
|
|
268
|
+
// codes on their account.
|
|
269
|
+
function directEndpointWarningLines(classification) {
|
|
270
|
+
if (!classification || typeof classification !== 'object') return [];
|
|
271
|
+
if (classification.kind === 'direct') {
|
|
272
|
+
return [
|
|
273
|
+
'⚠ this is the IPv6-only endpoint (db.<project-ref>.supabase.co — AAAA-only DNS, no IPv4)',
|
|
274
|
+
'on IPv4-only hosts pg clients hang until a pool/connect timeout',
|
|
275
|
+
'IPv4-safe: Connect modal → Transaction pooler → toggle ON "Use IPv4 connection (Shared Pooler)"',
|
|
276
|
+
'postgres://postgres.<project-ref>:<password>@aws-<n>-<region>.pooler.supabase.com:6543/postgres'
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
if (classification.kind === 'shared-pooler' && classification.poolerUserMismatch) {
|
|
280
|
+
return [
|
|
281
|
+
`⚠ Shared Pooler host but username "${classification.username}" — pooler logins must be postgres.<project-ref>; fails with "Tenant or user not found"`
|
|
282
|
+
];
|
|
283
|
+
}
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
|
|
189
287
|
// Mask all but the last 4 chars of a secret for logging.
|
|
190
288
|
function maskSecret(value) {
|
|
191
289
|
if (!value || typeof value !== 'string') return '';
|
|
@@ -202,5 +300,7 @@ module.exports = {
|
|
|
202
300
|
isTransactionPoolerUrl,
|
|
203
301
|
normalizeDatabaseUrl,
|
|
204
302
|
maskSecret,
|
|
205
|
-
stripSurroundingQuotes
|
|
303
|
+
stripSurroundingQuotes,
|
|
304
|
+
classifyDbEndpoint,
|
|
305
|
+
directEndpointWarningLines
|
|
206
306
|
};
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
The `@jhizzard/termdeck-stack` installer can drop `memory-session-end.js`
|
|
4
4
|
into `~/.claude/hooks/` and wire it into `~/.claude/settings.json` under
|
|
5
|
-
`hooks.
|
|
6
|
-
yes.
|
|
5
|
+
`hooks.SessionEnd`. The installer prompts you before doing this; default
|
|
6
|
+
is yes. (Early versions wired `hooks.Stop`, which fires every assistant
|
|
7
|
+
turn — the wizard migrates that to `SessionEnd` automatically.)
|
|
7
8
|
|
|
8
9
|
## What the hook does
|
|
9
10
|
|
|
10
|
-
On every Claude Code session close, Claude Code fires its `
|
|
11
|
-
with a JSON payload on stdin:
|
|
11
|
+
On every Claude Code session close, Claude Code fires its `SessionEnd`
|
|
12
|
+
hook with a JSON payload on stdin:
|
|
12
13
|
|
|
13
14
|
```json
|
|
14
15
|
{ "transcript_path": "/path/to/session.jsonl", "cwd": "/path/where/you/were/working", "session_id": "..." }
|
|
@@ -17,20 +18,28 @@ with a JSON payload on stdin:
|
|
|
17
18
|
The hook:
|
|
18
19
|
|
|
19
20
|
1. Skips transcripts smaller than 5 KB (no signal in tiny sessions —
|
|
20
|
-
override via `TERMDECK_HOOK_MIN_BYTES`).
|
|
21
|
+
override via `TERMDECK_HOOK_MIN_BYTES`). Compact-envelope session
|
|
22
|
+
types (`antigravity`, `web-chat`) are exempt from the byte floor and
|
|
23
|
+
gate on parsed content instead (≥ 1 assistant turn).
|
|
21
24
|
2. Validates env vars (`SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`,
|
|
22
25
|
`OPENAI_API_KEY`); if any are missing, logs the missing list and
|
|
23
26
|
exits cleanly without blocking the session close.
|
|
24
27
|
3. Detects the project from `cwd` against a built-in regex table; falls
|
|
25
|
-
back to `"global"` when nothing matches.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
back to `"global"` when nothing matches. The hook ships with a
|
|
29
|
+
default most-specific-first table — see "Customizing the project
|
|
30
|
+
map" below to extend it with your own entries.
|
|
28
31
|
4. Builds a coarse session summary from the last ~30 messages of the
|
|
29
32
|
transcript (~7 KB cap to stay inside OpenAI's embedding-input
|
|
30
33
|
budget).
|
|
31
|
-
5. Embeds the summary via OpenAI `text-embedding-3-
|
|
34
|
+
5. Embeds the summary via OpenAI `text-embedding-3-large` at
|
|
35
|
+
`dimensions: 1536` — deliberately identical to Mnestra's recall-query
|
|
36
|
+
embedder, so rows and queries share one vector space (rows embedded
|
|
37
|
+
with any other model rank as semantic noise in hybrid search).
|
|
32
38
|
6. POSTs **one row** to Supabase `/rest/v1/memory_items` with
|
|
33
|
-
`source_type='session_summary'
|
|
39
|
+
`source_type='session_summary'` (stamped
|
|
40
|
+
`metadata.embedding_model='text-embedding-3-large@1536'`), plus a
|
|
41
|
+
companion upsert to `/rest/v1/memory_sessions` keyed on
|
|
42
|
+
`session_id`.
|
|
34
43
|
7. Logs every step to `~/.claude/hooks/memory-hook.log`.
|
|
35
44
|
|
|
36
45
|
The hook is **fail-soft**: any error (network, parse, env-var-missing,
|
|
@@ -70,9 +79,9 @@ If any of the three is missing the log line will name them:
|
|
|
70
79
|
|
|
71
80
|
## Customizing the project map
|
|
72
81
|
|
|
73
|
-
The hook ships with
|
|
74
|
-
session lands under `project: 'global'`
|
|
75
|
-
your own:
|
|
82
|
+
The hook ships with a default `PROJECT_MAP` (most-specific-first); a
|
|
83
|
+
session lands under `project: 'global'` only when no entry matches its
|
|
84
|
+
`cwd`. To add your own entries:
|
|
76
85
|
|
|
77
86
|
1. Open `~/.claude/hooks/memory-session-end.js` after the installer
|
|
78
87
|
has dropped it.
|
|
@@ -146,8 +155,8 @@ before overwriting; choose accordingly.
|
|
|
146
155
|
Two options:
|
|
147
156
|
|
|
148
157
|
1. Edit `~/.claude/settings.json` and remove the entry under
|
|
149
|
-
`hooks.
|
|
150
|
-
file in place; it simply won't fire.
|
|
158
|
+
`hooks.SessionEnd` that references `memory-session-end.js`. Leave
|
|
159
|
+
the file in place; it simply won't fire.
|
|
151
160
|
2. Or delete `~/.claude/hooks/memory-session-end.js` AND remove the
|
|
152
161
|
`settings.json` entry. (Removing only the file leaves a broken
|
|
153
162
|
`command` in settings — Claude Code will log a missing-file error
|
|
@@ -162,6 +171,7 @@ re-prompt to install. Decline at the prompt to stay opted out.
|
|
|
162
171
|
|---|---|
|
|
163
172
|
| `TERMDECK_HOOK_DEBUG=1` | Verbose `[debug]` lines in the log |
|
|
164
173
|
| `TERMDECK_HOOK_MIN_BYTES=10000` | Override the 5 KB skip threshold |
|
|
174
|
+
| `TERMDECK_HOOK_MIN_MESSAGES=5` | Override the parsed-message floor (default 1) |
|
|
165
175
|
|
|
166
176
|
## Log file
|
|
167
177
|
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TermDeck pre-compact memory hook (Mnestra-direct, no rag-system dependency).
|
|
3
3
|
*
|
|
4
|
+
* @termdeck/stack-installer-hook v2
|
|
5
|
+
*
|
|
6
|
+
* ^ Stamp lives at the TOP of the docblock — both readers scan only the first
|
|
7
|
+
* 4096 bytes (Sprint 73 T1 hit this on the session-end hook when its
|
|
8
|
+
* changelog grew past 4 KB and the stamp fell out of the window, silently
|
|
9
|
+
* disabling every refresh path). Keep it above the fold.
|
|
10
|
+
*
|
|
4
11
|
* Vendored into ~/.claude/hooks/memory-pre-compact.js by @jhizzard/termdeck-stack.
|
|
5
12
|
* Wired into ~/.claude/settings.json under hooks.PreCompact — fires BEFORE
|
|
6
13
|
* Claude Code compacts conversation context, capturing the in-flight session
|
|
@@ -32,7 +39,9 @@
|
|
|
32
39
|
* - Load ~/.termdeck/secrets.env on env-var gaps (Sprint 47.5 discipline).
|
|
33
40
|
* - Parse transcript via the adapter parser exported by
|
|
34
41
|
* memory-session-end.js (Sprint 38 module-export contract; no duplication).
|
|
35
|
-
* - Embed via
|
|
42
|
+
* - Embed via the session-end hook's embedText (text-embedding-3-large at
|
|
43
|
+
* dimensions:1536 since v5 there — recall-parity with mnestra's query
|
|
44
|
+
* embedder; this hook has NO embed call of its own).
|
|
36
45
|
* - POST ONE row to /rest/v1/memory_items with
|
|
37
46
|
* source_type='pre_compact_snapshot', category='workflow'.
|
|
38
47
|
*
|
|
@@ -43,18 +52,27 @@
|
|
|
43
52
|
* fail-soft.
|
|
44
53
|
*
|
|
45
54
|
* Version stamp (Sprint 64 T3.2 — initial cut):
|
|
46
|
-
* The marker `@termdeck/stack-installer-hook v<N>`
|
|
47
|
-
* stack-installer's installPreCompactHook
|
|
48
|
-
* --yes) and `termdeck init --mnestra`
|
|
49
|
-
* step)
|
|
55
|
+
* The marker `@termdeck/stack-installer-hook v<N>` at the TOP of this
|
|
56
|
+
* docblock is read by both stack-installer's installPreCompactHook
|
|
57
|
+
* (version-aware overwrite under --yes) and `termdeck init --mnestra`
|
|
58
|
+
* (refreshBundledPreCompactHookIfNewer step) — both scan only the first
|
|
59
|
+
* 4096 bytes. Bump the integer whenever a change here should overwrite an
|
|
50
60
|
* already-installed copy. Comment-only tweaks do not need a bump.
|
|
51
61
|
*
|
|
52
|
-
*
|
|
62
|
+
* v2 (Sprint 73 T1, ORCH handoff — embedding recall-parity marker):
|
|
63
|
+
* - Snapshot rows now stamp metadata.embedding_model with the marker
|
|
64
|
+
* exported by the session-end hook (v5: 'text-embedding-3-large@1536')
|
|
65
|
+
* — Sprint 74 T3's re-embed backfill keys idempotency on it. The marker
|
|
66
|
+
* is stamped ONLY when the loaded helpers export it: an older installed
|
|
67
|
+
* session-end (still embedding 3-small) exports none, the row stays
|
|
68
|
+
* unmarked, and the backfill correctly re-embeds it — a false marker on
|
|
69
|
+
* a mis-embedded row would permanently hide it from repair.
|
|
53
70
|
*
|
|
54
71
|
* Required env vars (validated at entry, after the secrets.env fallback):
|
|
55
72
|
* - SUPABASE_URL e.g. https://<project-ref>.supabase.co
|
|
56
73
|
* - SUPABASE_SERVICE_ROLE_KEY service-role key (needs INSERT on memory_items)
|
|
57
|
-
* - OPENAI_API_KEY sk-... for
|
|
74
|
+
* - OPENAI_API_KEY sk-... for the embed model (see embedText in
|
|
75
|
+
* the session-end hook — 3-large@1536 since v5)
|
|
58
76
|
*
|
|
59
77
|
* Optional:
|
|
60
78
|
* - TERMDECK_HOOK_DEBUG=1 verbose logging
|
|
@@ -126,6 +144,7 @@ async function postPreCompactSnapshot({
|
|
|
126
144
|
content, embedding,
|
|
127
145
|
project, sessionId,
|
|
128
146
|
sourceAgent,
|
|
147
|
+
embeddingModelMarker,
|
|
129
148
|
}) {
|
|
130
149
|
try {
|
|
131
150
|
const res = await fetch(`${supabaseUrl}/rest/v1/memory_items`, {
|
|
@@ -144,6 +163,12 @@ async function postPreCompactSnapshot({
|
|
|
144
163
|
project,
|
|
145
164
|
source_session_id: sessionId || null,
|
|
146
165
|
source_agent: sourceAgent,
|
|
166
|
+
// v2 — backfill-idempotency marker, present ONLY when the loaded
|
|
167
|
+
// helpers export one (i.e. the embed actually ran on that model).
|
|
168
|
+
// See the v2 header note for the stale-helpers rationale.
|
|
169
|
+
...(embeddingModelMarker
|
|
170
|
+
? { metadata: { embedding_model: embeddingModelMarker } }
|
|
171
|
+
: {}),
|
|
147
172
|
}),
|
|
148
173
|
});
|
|
149
174
|
if (!res.ok) {
|
|
@@ -240,6 +265,9 @@ async function processPreCompactPayload(input, helpers) {
|
|
|
240
265
|
supabaseUrl: env.supabaseUrl,
|
|
241
266
|
supabaseKey: env.supabaseKey,
|
|
242
267
|
content, embedding, project, sessionId, sourceAgent,
|
|
268
|
+
// Marker travels with the embedder: undefined on a pre-v5 session-end
|
|
269
|
+
// hook (3-small embeds → row stays unmarked → backfill repairs it).
|
|
270
|
+
embeddingModelMarker: helpers.EMBEDDING_MODEL_MARKER || null,
|
|
243
271
|
});
|
|
244
272
|
|
|
245
273
|
if (ok) {
|