@jhizzard/termdeck 0.3.9 → 0.4.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/README.md CHANGED
@@ -161,7 +161,7 @@ Honest limits, stated upfront so the skeptic has nothing to chase:
161
161
  - **Not a replacement for reading docs.** It's the shortest path to a memory you already wrote. If the memory isn't there, the feature does nothing.
162
162
  - **Not fully local by default.** Tier 2+ reaches out to Supabase for storage and OpenAI for embeddings. Tier 1 is fully local. A fully-local Tier 2 (local Postgres + local embeddings) is on the roadmap.
163
163
  - **Not free forever.** Tier 2+ pays OpenAI fractions of a cent per memory for embeddings. Self-hosted embeddings via Ollama are on the roadmap.
164
- - **Not proven at scale.** v0.3.9, validated against 3,527 memories in one developer's production store. First full Rumen kickstart on 2026-04-15 processed 111 sessions into 111 insights in one pass. No multi-user data yet. Bug reports and issues welcome.
164
+ - **Not proven at scale.** v0.4.0, validated against 3,527 memories in one developer's production store. First full Rumen kickstart on 2026-04-15 processed 111 sessions into 111 insights in one pass. No multi-user data yet. Bug reports and issues welcome.
165
165
 
166
166
  ---
167
167
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhizzard/termdeck",
3
- "version": "0.3.9",
3
+ "version": "0.4.0",
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"
@@ -1,7 +1,8 @@
1
1
  /* Extracted from index.html 2026-04-15 — see git blame on index.html prior to commit UNCOMMITTED for history */
2
2
  // ===== TermDeck Client =====
3
3
  const API = window.location.origin;
4
- const WS_BASE = `ws://${window.location.host}/ws`;
4
+ const WS_PROTOCOL = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
5
+ const WS_BASE = `${WS_PROTOCOL}//${window.location.host}/ws`;
5
6
 
6
7
  // State
7
8
  const state = {
@@ -133,6 +133,11 @@ function createServer(config) {
133
133
  // ==================== REST API ====================
134
134
 
135
135
  // GET /api/health - preflight health checks (Sprint 6 T1, wired by T3)
136
+ // SECURITY NOTE: Returns operational detail (memory counts, DB latency, project paths,
137
+ // RAG breaker state). Intentional for local-first use — TermDeck binds to 127.0.0.1 by
138
+ // default and the CLI guardrail blocks beyond-localhost binds without explicit opt-in.
139
+ // For any non-loopback deployment (Sprint 18+ remote story), gate this route behind auth
140
+ // or scope the response to a minimal {status, version} payload.
136
141
  app.get('/api/health', createHealthHandler(config));
137
142
 
138
143
  // GET /api/sessions - list all active sessions
@@ -59,8 +59,12 @@ class RAGIntegration {
59
59
  };
60
60
 
61
61
  // Circuit breaker: track consecutive 404s per table name.
62
- // After 3 consecutive 404s, disable pushes to that table until restart.
63
- this._circuitBreaker = new Map(); // table -> { count: number, open: boolean }
62
+ // After 3 consecutive 404s, open the breaker. The breaker auto-transitions
63
+ // to half-open after 5 minutes, allowing one retry attempt. A successful
64
+ // retry fully resets the breaker; a failed retry re-opens it for another
65
+ // 5-minute backoff window.
66
+ this._circuitBreaker = new Map(); // table -> { count, open, openedAt, halfOpen }
67
+ this._halfOpenDelayMs = 5 * 60 * 1000;
64
68
 
65
69
  if (this.enabled) {
66
70
  this._startSync();
@@ -142,22 +146,34 @@ class RAGIntegration {
142
146
  }, this._projectFor(session));
143
147
  }
144
148
 
145
- // Circuit breaker check — returns true if pushes to this table are disabled
149
+ // Circuit breaker check — returns true if pushes to this table are disabled.
150
+ // Has a side effect: when the 5-minute half-open window has elapsed, flips
151
+ // the breaker to half-open and permits one retry attempt through.
146
152
  _isCircuitOpen(table) {
147
153
  const state = this._circuitBreaker.get(table);
148
- return !!(state && state.open);
154
+ if (!state || !state.open) return false;
155
+ if (state.halfOpen) return true; // retry already in flight — block concurrent pushes
156
+
157
+ const elapsed = Date.now() - (state.openedAt || 0);
158
+ if (elapsed >= this._halfOpenDelayMs) {
159
+ state.halfOpen = true;
160
+ console.log(`[rag] circuit breaker half-open for ${table}, retrying`);
161
+ return false; // allow one attempt through
162
+ }
163
+ return true;
149
164
  }
150
165
 
151
166
  // Record a 404 for a table; opens the breaker after 3 consecutive hits
152
167
  _record404(table) {
153
168
  let state = this._circuitBreaker.get(table);
154
169
  if (!state) {
155
- state = { count: 0, open: false };
170
+ state = { count: 0, open: false, openedAt: null, halfOpen: false };
156
171
  this._circuitBreaker.set(table, state);
157
172
  }
158
173
  state.count += 1;
159
174
  if (state.count >= 3 && !state.open) {
160
175
  state.open = true;
176
+ state.openedAt = Date.now();
161
177
  console.warn(`[rag] circuit breaker open for ${table} — disabling pushes (table may not exist in Supabase)`);
162
178
  }
163
179
  }
@@ -208,8 +224,14 @@ class RAGIntegration {
208
224
  // Success — reset any accumulated 404 count for this table
209
225
  this._resetCircuit(table);
210
226
  } catch (err) {
211
- // Log at warn (not error) to reduce noise — the circuit breaker handles persistence
212
- if (!this._isCircuitOpen(table)) {
227
+ const state = this._circuitBreaker.get(table);
228
+ if (state && state.halfOpen) {
229
+ // Half-open retry failed — re-open for another 5-minute backoff window
230
+ state.halfOpen = false;
231
+ state.openedAt = Date.now();
232
+ console.warn(`[rag] circuit breaker re-opened for ${table} after half-open retry failed`);
233
+ } else if (!state || !state.open) {
234
+ // Log at warn (not error) to reduce noise — the circuit breaker handles persistence
213
235
  console.warn('[rag] push to', table, 'failed:', err.message);
214
236
  }
215
237
  throw err; // Propagate to caller so sync loop knows this event failed
@@ -46,6 +46,8 @@ class TranscriptWriter {
46
46
  // Lazy pool
47
47
  this._pool = null;
48
48
  this._poolFailed = false;
49
+ this._poolFailedAt = 0;
50
+ this._poolRetryMs = 30_000;
49
51
 
50
52
  // Start flush timer
51
53
  this._timer = null;
@@ -56,7 +58,13 @@ class TranscriptWriter {
56
58
 
57
59
  // Lazy-init pg.Pool (same pattern as getRumenPool in index.js)
58
60
  _getPool() {
59
- if (this._pool || this._poolFailed) return this._pool;
61
+ if (this._pool) return this._pool;
62
+ if (this._poolFailed) {
63
+ if (Date.now() - this._poolFailedAt < this._poolRetryMs) return null;
64
+ console.warn('[transcript] retrying pool creation after 30s cooldown');
65
+ this._poolFailed = false;
66
+ this._poolFailedAt = 0;
67
+ }
60
68
  if (!pg || !this._databaseUrl) return null;
61
69
  try {
62
70
  this._pool = new pg.Pool({
@@ -72,6 +80,7 @@ class TranscriptWriter {
72
80
  } catch (err) {
73
81
  console.error('[transcript] pool creation failed:', err.message);
74
82
  this._poolFailed = true;
83
+ this._poolFailedAt = Date.now();
75
84
  return null;
76
85
  }
77
86
  }