Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions server/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -1283,4 +1283,31 @@ const stmts = {
),
};

// --- Ignore-Filter: Sessions mit bestimmten cwd nicht persistieren ---------
// Einbahn-Konsolidierungs-/Probe-Sessions (z. B. `claude --print` mit cwd=/tmp,
// von der claude-code-api gespawnt) sollen das Dashboard nicht zumüllen. Greift
// zentral für ALLE Insert-Pfade (Hooks, REST-POST, Backfill), da alle
// stmts.insertSession.run(...) nutzen. cwd ist das 4. Argument
// (id, name, status, cwd, model, metadata). Opt-in über MONITOR_IGNORE_CWD
// (kommagetrennt, exakter cwd-Match); Default leer = Filter AUS (damit Tests
// und Upstream-Verhalten unverändert bleiben). Aktivierung via systemd-Unit.
const IGNORED_SESSION_CWDS = new Set(
(process.env.MONITOR_IGNORE_CWD ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean)
);
if (IGNORED_SESSION_CWDS.size > 0) {
const _origInsertSession = stmts.insertSession;
stmts.insertSession = {
run: (...args) => {
const cwd = args[3];
if (cwd && IGNORED_SESSION_CWDS.has(cwd)) {
return { changes: 0, lastInsertRowid: 0 };
}
return _origInsertSession.run(...args);
},
};
}
Comment on lines +1300 to +1311

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Critical Correctness Issues

  1. Foreign Key Constraint Violations:
    The database schema enforces foreign key constraints (FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE) on agents, events, token_usage, and workflows tables, and foreign_keys = ON is enabled on line 127. If we skip inserting a session but subsequently attempt to insert agents, events, token usage, or workflows referencing that session ID, SQLite will throw a SqliteError: FOREIGN KEY constraint failed error, crashing the server or hook handler.

  2. Interface Preservation:
    Replacing stmts.insertSession with a plain object { run: ... } strips away all other properties and methods of the Statement object (such as get, all, iterate, etc.). Using a Proxy preserves the Statement prototype and all other methods/properties, ensuring full compatibility.

Solution

Track ignored session IDs in a Set and use a Proxy to intercept all write statements referencing session_id to safely skip execution for ignored sessions.

const IGNORED_SESSION_IDS = new Set();

if (IGNORED_SESSION_CWDS.size > 0) {
  const wrapStatement = (stmt, checkFn) => {
    if (!stmt) return stmt;
    return new Proxy(stmt, {
      get(target, prop, receiver) {
        if (prop === "run") {
          return (...args) => {
            if (checkFn(...args)) {
              return { changes: 0, lastInsertRowid: 0 };
            }
            return target.run(...args);
          };
        }
        const value = Reflect.get(target, prop, receiver);
        return typeof value === "function" ? value.bind(target) : value;
      },
    });
  };

  stmts.insertSession = new Proxy(stmts.insertSession, {
    get(target, prop, receiver) {
      if (prop === "run") {
        return (...args) => {
          const id = args[0];
          const cwd = args[3];
          if (cwd && IGNORED_SESSION_CWDS.has(cwd)) {
            if (id) IGNORED_SESSION_IDS.add(id);
            return { changes: 0, lastInsertRowid: 0 };
          }
          return target.run(...args);
        };
      }
      const value = Reflect.get(target, prop, receiver);
      return typeof value === "function" ? value.bind(target) : value;
    },
  });

  stmts.insertAgent = wrapStatement(stmts.insertAgent, (...args) => IGNORED_SESSION_IDS.has(args[1]));
  stmts.insertEvent = wrapStatement(stmts.insertEvent, (...args) => IGNORED_SESSION_IDS.has(args[0]));
  stmts.upsertTokenUsage = wrapStatement(stmts.upsertTokenUsage, (...args) => IGNORED_SESSION_IDS.has(args[0]));
  stmts.replaceTokenUsage = wrapStatement(stmts.replaceTokenUsage, (...args) => IGNORED_SESSION_IDS.has(args[0]));
  stmts.upsertWorkflow = wrapStatement(stmts.upsertWorkflow, (...args) => IGNORED_SESSION_IDS.has(args[1]));
  stmts.updateSession = wrapStatement(stmts.updateSession, (...args) => IGNORED_SESSION_IDS.has(args[4]));
  stmts.reactivateSession = wrapStatement(stmts.reactivateSession, (...args) => IGNORED_SESSION_IDS.has(args[0]));
  stmts.updateSessionModel = wrapStatement(stmts.updateSessionModel, (...args) => IGNORED_SESSION_IDS.has(args[1]));
  stmts.touchSession = wrapStatement(stmts.touchSession, (...args) => IGNORED_SESSION_IDS.has(args[0]));
  stmts.setSessionTranscriptPath = wrapStatement(stmts.setSessionTranscriptPath, (...args) => IGNORED_SESSION_IDS.has(args[1]));
  stmts.setSessionAwaitingInput = wrapStatement(stmts.setSessionAwaitingInput, (...args) => IGNORED_SESSION_IDS.has(args[1]));
  stmts.clearSessionAwaitingInput = wrapStatement(stmts.clearSessionAwaitingInput, (...args) => IGNORED_SESSION_IDS.has(args[0]));
  stmts.clearSessionAgentsAwaitingInput = wrapStatement(stmts.clearSessionAgentsAwaitingInput, (...args) => IGNORED_SESSION_IDS.has(args[0]));
  stmts.insertAlertEvent = wrapStatement(stmts.insertAlertEvent, (...args) => IGNORED_SESSION_IDS.has(args[3]));
}


module.exports = { db, stmts, DB_PATH, DEFAULT_PRICING };
Loading