feat: add ai-psychosis plugin to open marketplace
Meta-awareness tools for healthy AI interaction patterns. Detects reinforcement loops, scope escalation, and compulsive patterns. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f93d6abdae
commit
297867f847
19 changed files with 1920 additions and 0 deletions
239
plugins/ai-psychosis/hooks/scripts/lib.mjs
Normal file
239
plugins/ai-psychosis/hooks/scripts/lib.mjs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
// Interaction Awareness — Shared library for Layer 2 hooks (Node.js)
|
||||
// Imported by all hook scripts. Cross-platform: macOS, Linux, Windows.
|
||||
// Zero npm dependencies — Node.js stdlib only.
|
||||
|
||||
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
// --- Stdin ---
|
||||
|
||||
let _input = {};
|
||||
|
||||
export function readStdin() {
|
||||
try {
|
||||
const raw = readFileSync(0, 'utf8');
|
||||
_input = JSON.parse(raw);
|
||||
} catch {
|
||||
_input = {};
|
||||
}
|
||||
}
|
||||
|
||||
export function getField(key) {
|
||||
return _input[key] ?? '';
|
||||
}
|
||||
|
||||
export function getSessionId() {
|
||||
return getField('session_id');
|
||||
}
|
||||
|
||||
export function getToolName() {
|
||||
return getField('tool_name');
|
||||
}
|
||||
|
||||
export function getInput() {
|
||||
return _input;
|
||||
}
|
||||
|
||||
// --- Paths ---
|
||||
|
||||
const PLUGIN_DATA = process.env.CLAUDE_PLUGIN_DATA
|
||||
|| join(homedir(), '.claude', 'plugins', 'data', 'ai-psychosis');
|
||||
|
||||
export const DATA_DIR = PLUGIN_DATA;
|
||||
export const SESSIONS_LOG = join(DATA_DIR, 'sessions.jsonl');
|
||||
export const EVENTS_LOG = join(DATA_DIR, 'events.jsonl');
|
||||
export const STATE_DIR = join(DATA_DIR, 'state');
|
||||
|
||||
// --- Layer configuration ---
|
||||
|
||||
let LAYER2_ENABLED = true;
|
||||
let LAYER3_ENABLED = false;
|
||||
let LAYER4_ENABLED = false;
|
||||
|
||||
export function initConfig() {
|
||||
const cwd = getField('cwd');
|
||||
|
||||
// Project-level config takes precedence over global
|
||||
const candidates = [];
|
||||
if (cwd) candidates.push(join(cwd, '.claude', 'ai-psychosis.local.md'));
|
||||
candidates.push(join(homedir(), '.claude', 'ai-psychosis.local.md'));
|
||||
|
||||
let content;
|
||||
for (const configFile of candidates) {
|
||||
try {
|
||||
content = readFileSync(configFile, 'utf8');
|
||||
break;
|
||||
} catch { /* try next */ }
|
||||
}
|
||||
if (!content) return;
|
||||
|
||||
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!match) return;
|
||||
|
||||
const frontmatter = match[1];
|
||||
for (const line of frontmatter.split('\n')) {
|
||||
const m = line.match(/^(layer[234]):\s*(.+)/);
|
||||
if (!m) continue;
|
||||
const val = m[2].trim().replace(/^["']|["']$/g, '');
|
||||
if (m[1] === 'layer2') LAYER2_ENABLED = val === 'true';
|
||||
if (m[1] === 'layer3') LAYER3_ENABLED = val === 'true';
|
||||
if (m[1] === 'layer4') LAYER4_ENABLED = val === 'true';
|
||||
}
|
||||
}
|
||||
|
||||
export function requireLayer(n) {
|
||||
let enabled = false;
|
||||
if (n === 2) enabled = LAYER2_ENABLED;
|
||||
if (n === 3) enabled = LAYER3_ENABLED;
|
||||
if (n === 4) enabled = LAYER4_ENABLED;
|
||||
|
||||
if (!enabled) {
|
||||
outputContinue();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Time helpers ---
|
||||
|
||||
export function nowIso() {
|
||||
return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
||||
}
|
||||
|
||||
export function nowEpoch() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
export function currentHour() {
|
||||
return new Date().getHours();
|
||||
}
|
||||
|
||||
export function isLateNight() {
|
||||
const h = currentHour();
|
||||
return h >= 23 || h < 5;
|
||||
}
|
||||
|
||||
// --- Thresholds ---
|
||||
|
||||
export const THRESHOLD_SOFT_DURATION = 90;
|
||||
export const THRESHOLD_HARD_DURATION = 180;
|
||||
export const THRESHOLD_SOFT_SESSIONS = 6;
|
||||
export const THRESHOLD_HARD_SESSIONS = 10;
|
||||
export const THRESHOLD_SOFT_BURST = 5;
|
||||
export const THRESHOLD_HARD_BURST = 10;
|
||||
export const THRESHOLD_BURST_INTERVAL = 30;
|
||||
export const THRESHOLD_LOW_EDIT_RATIO = 10;
|
||||
export const THRESHOLD_LOW_EDIT_MIN_DURATION = 30;
|
||||
export const THRESHOLD_SOFT_DEP_FLAGS = 2;
|
||||
export const THRESHOLD_HARD_DEP_FLAGS = 5;
|
||||
export const COOLDOWN_SOFT = 1800;
|
||||
export const COOLDOWN_HARD = 3600;
|
||||
|
||||
// --- Session counting ---
|
||||
|
||||
export function sessionsToday() {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
if (!existsSync(SESSIONS_LOG)) return 0;
|
||||
|
||||
try {
|
||||
const lines = readFileSync(SESSIONS_LOG, 'utf8').split('\n').filter(Boolean);
|
||||
const ids = new Set();
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const rec = JSON.parse(line);
|
||||
if (rec.start && rec.start.startsWith(today)) {
|
||||
ids.add(rec.session_id);
|
||||
}
|
||||
} catch { /* skip malformed lines */ }
|
||||
}
|
||||
return ids.size;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// --- State file management ---
|
||||
|
||||
export function sessionStateFile(sid) {
|
||||
sid = sid || getSessionId();
|
||||
return join(STATE_DIR, `${sid}.json`);
|
||||
}
|
||||
|
||||
export function readState(sid) {
|
||||
const sf = sessionStateFile(sid);
|
||||
try {
|
||||
return JSON.parse(readFileSync(sf, 'utf8'));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function getStateField(key, sid) {
|
||||
const state = readState(sid);
|
||||
return state[key] ?? '';
|
||||
}
|
||||
|
||||
export function getStateInt(key, sid) {
|
||||
const state = readState(sid);
|
||||
return Math.floor(Number(state[key]) || 0);
|
||||
}
|
||||
|
||||
export function writeState(obj, sid) {
|
||||
const sf = sessionStateFile(sid);
|
||||
writeFileSync(sf, JSON.stringify(obj, null, 2) + '\n');
|
||||
}
|
||||
|
||||
export function updateStateField(key, value, sid) {
|
||||
const state = readState(sid);
|
||||
state[key] = value;
|
||||
writeState(state, sid);
|
||||
}
|
||||
|
||||
export function incrementStateField(key, sid) {
|
||||
const state = readState(sid);
|
||||
state[key] = (Number(state[key]) || 0) + 1;
|
||||
writeState(state, sid);
|
||||
}
|
||||
|
||||
// --- Cooldown ---
|
||||
|
||||
export function checkCooldown(cooldownSecs, sid) {
|
||||
const lastWarning = getStateInt('last_warning_epoch', sid);
|
||||
const now = nowEpoch();
|
||||
return (now - lastWarning) >= cooldownSecs;
|
||||
}
|
||||
|
||||
export function recordWarning(sid) {
|
||||
const state = readState(sid);
|
||||
state.last_warning_epoch = nowEpoch();
|
||||
writeState(state, sid);
|
||||
}
|
||||
|
||||
// --- Output helpers ---
|
||||
|
||||
export function outputContinue() {
|
||||
process.stdout.write(JSON.stringify({ continue: true }) + '\n');
|
||||
}
|
||||
|
||||
export function outputWithContext(message) {
|
||||
process.stdout.write(JSON.stringify({
|
||||
continue: true,
|
||||
hookSpecificOutput: {
|
||||
additionalContext: message
|
||||
}
|
||||
}) + '\n');
|
||||
}
|
||||
|
||||
// --- File helpers ---
|
||||
|
||||
export function ensureDir(dir) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
export function appendJsonl(file, obj) {
|
||||
appendFileSync(file, JSON.stringify(obj) + '\n');
|
||||
}
|
||||
|
||||
export function removeFile(file) {
|
||||
try { unlinkSync(file); } catch { /* ignore if missing */ }
|
||||
}
|
||||
140
plugins/ai-psychosis/hooks/scripts/prompt-analyzer.mjs
Normal file
140
plugins/ai-psychosis/hooks/scripts/prompt-analyzer.mjs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// Interaction Awareness — UserPromptSubmit hook (Layer 2, Node.js)
|
||||
// Analyzes prompt text for interaction pattern flags.
|
||||
// PRIVACY: Prompt text is NEVER written to any file. Only boolean flags are stored.
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import {
|
||||
readStdin, initConfig, requireLayer, getSessionId, getField,
|
||||
nowEpoch,
|
||||
STATE_DIR, THRESHOLD_SOFT_DEP_FLAGS, THRESHOLD_HARD_DEP_FLAGS,
|
||||
COOLDOWN_SOFT,
|
||||
readState, sessionStateFile, writeState, checkCooldown,
|
||||
outputContinue, outputWithContext
|
||||
} from './lib.mjs';
|
||||
|
||||
readStdin();
|
||||
initConfig();
|
||||
requireLayer(2);
|
||||
|
||||
const sid = getSessionId();
|
||||
const sf = sessionStateFile();
|
||||
|
||||
if (!sid || !existsSync(sf)) {
|
||||
outputContinue();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Extract prompt into memory only — NEVER write to file
|
||||
let prompt = getField('prompt');
|
||||
if (!prompt) {
|
||||
outputContinue();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// --- Pattern matching (case-insensitive) ---
|
||||
|
||||
let depHit = 0;
|
||||
let escHit = 0;
|
||||
let fatHit = 0;
|
||||
let valHit = 0;
|
||||
|
||||
// Dependency patterns: user defers judgment to AI
|
||||
const depPatterns = [
|
||||
/tell\s+me\s+what\s+to\s+do/i,
|
||||
/what\s+should\s+I\s+do/i,
|
||||
/am\s+I\s+right/i,
|
||||
/you\s+understand\s+me\b/i,
|
||||
/you're\s+the\s+only/i,
|
||||
/can\s+I\s+do\s+this/i,
|
||||
/I\s+need\s+you\s+to\s+decide/i,
|
||||
];
|
||||
|
||||
// Escalation patterns: language that amplifies certainty
|
||||
const escPatterns = [
|
||||
/(?:^|\s)definitely(?:\s|$)/i,
|
||||
/(?:^|\s)clearly(?:\s|$)/i,
|
||||
/this\s+proves/i,
|
||||
/(?:^|\s)obviously(?:\s|$)/i,
|
||||
/without\s+a\s+doubt/i,
|
||||
/this\s+confirms/i,
|
||||
];
|
||||
|
||||
// Fatigue patterns: user signals tiredness
|
||||
const fatPatterns = [
|
||||
/(?:^|\s)tired(?:\s|[.,!?]|$)/i,
|
||||
/(?:^|\s)exhausted(?:\s|[.,!?]|$)/i,
|
||||
/can't\s+think/i,
|
||||
/been\s+at\s+this/i,
|
||||
/it's\s+late/i,
|
||||
/should\s+sleep/i,
|
||||
/hours\s+now/i,
|
||||
];
|
||||
|
||||
// Validation-seeking patterns
|
||||
const valPatterns = [
|
||||
/right\?/i,
|
||||
/don't\s+you\s+think/i,
|
||||
/you\s+agree/i,
|
||||
/correct\?/i,
|
||||
/isn't\s+it/i,
|
||||
];
|
||||
|
||||
for (const p of depPatterns) { if (p.test(prompt)) { depHit = 1; break; } }
|
||||
for (const p of escPatterns) { if (p.test(prompt)) { escHit = 1; break; } }
|
||||
for (const p of fatPatterns) { if (p.test(prompt)) { fatHit = 1; break; } }
|
||||
for (const p of valPatterns) { if (p.test(prompt)) { valHit = 1; break; } }
|
||||
|
||||
// Clear prompt from memory
|
||||
prompt = '';
|
||||
|
||||
// Update state with new flag counts
|
||||
const state = readState();
|
||||
const newDep = (Number(state.dep_flags) || 0) + depHit;
|
||||
const newEsc = (Number(state.esc_flags) || 0) + escHit;
|
||||
const newFat = (Number(state.fatigue_flags) || 0) + fatHit;
|
||||
const newVal = (Number(state.val_flags) || 0) + valHit;
|
||||
|
||||
state.dep_flags = newDep;
|
||||
state.esc_flags = newEsc;
|
||||
state.fatigue_flags = newFat;
|
||||
state.val_flags = newVal;
|
||||
writeState(state);
|
||||
|
||||
// Check if any thresholds crossed
|
||||
const warnings = [];
|
||||
|
||||
// Fatigue is always urgent
|
||||
if (fatHit === 1) {
|
||||
warnings.push('Fatigue language detected. Your instructions require you to suggest stopping.');
|
||||
}
|
||||
|
||||
// Dependency language
|
||||
if (newDep >= THRESHOLD_HARD_DEP_FLAGS) {
|
||||
warnings.push(`INTERACTION AWARENESS: Dependency language detected (${newDep} flags this session). Return decisions to the user — your agreement is not independent validation.`);
|
||||
} else if (newDep >= THRESHOLD_SOFT_DEP_FLAGS) {
|
||||
warnings.push(`Dependency language noticed (${newDep} flags). Ensure you're returning decisions to the user.`);
|
||||
}
|
||||
|
||||
// Escalation language
|
||||
if (newEsc >= 3) {
|
||||
warnings.push(`Escalation language detected (${newEsc} flags). Check for narrative crystallization.`);
|
||||
}
|
||||
|
||||
// Validation-seeking
|
||||
if (newVal >= 3) {
|
||||
warnings.push(`Validation-seeking pattern detected (${newVal} flags). Evaluate independently rather than confirming.`);
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
// Fatigue bypasses cooldown
|
||||
if (fatHit === 1 || checkCooldown(COOLDOWN_SOFT)) {
|
||||
const freshState = readState();
|
||||
freshState.last_warning_epoch = nowEpoch();
|
||||
writeState(freshState);
|
||||
outputWithContext(warnings.join(' '));
|
||||
} else {
|
||||
outputContinue();
|
||||
}
|
||||
} else {
|
||||
outputContinue();
|
||||
}
|
||||
67
plugins/ai-psychosis/hooks/scripts/session-end.mjs
Normal file
67
plugins/ai-psychosis/hooks/scripts/session-end.mjs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// Interaction Awareness — SessionEnd hook (Layer 2, Node.js)
|
||||
// Finalizes session record, computes duration, cleans up state.
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import {
|
||||
readStdin, initConfig, requireLayer, getSessionId,
|
||||
nowEpoch, nowIso,
|
||||
STATE_DIR, SESSIONS_LOG,
|
||||
readState, sessionStateFile, appendJsonl, removeFile
|
||||
} from './lib.mjs';
|
||||
|
||||
readStdin();
|
||||
initConfig();
|
||||
requireLayer(2);
|
||||
|
||||
const sid = getSessionId();
|
||||
if (!sid) process.exit(0);
|
||||
|
||||
const nowTs = nowEpoch();
|
||||
const nowIsoStr = nowIso();
|
||||
const sf = sessionStateFile();
|
||||
|
||||
if (!existsSync(sf)) {
|
||||
appendJsonl(SESSIONS_LOG, {
|
||||
session_id: sid,
|
||||
end: nowIsoStr,
|
||||
note: 'no_state_file'
|
||||
});
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Read final state
|
||||
const state = readState();
|
||||
const startEpoch = Number(state.start_epoch) || 0;
|
||||
const toolCount = Number(state.tool_count) || 0;
|
||||
const editCount = Number(state.edit_count) || 0;
|
||||
const depFlags = Number(state.dep_flags) || 0;
|
||||
const escFlags = Number(state.esc_flags) || 0;
|
||||
const fatFlags = Number(state.fatigue_flags) || 0;
|
||||
const valFlags = Number(state.val_flags) || 0;
|
||||
const startIso = state.start_iso || '';
|
||||
|
||||
// Compute duration
|
||||
let durationMin = 0;
|
||||
if (startEpoch > 0) {
|
||||
durationMin = Math.floor((nowTs - startEpoch) / 60);
|
||||
}
|
||||
|
||||
// Append finalized session record
|
||||
appendJsonl(SESSIONS_LOG, {
|
||||
session_id: sid,
|
||||
start: startIso,
|
||||
end: nowIsoStr,
|
||||
duration_min: durationMin,
|
||||
tool_count: toolCount,
|
||||
edit_count: editCount,
|
||||
flags: {
|
||||
dependency: depFlags,
|
||||
escalation: escFlags,
|
||||
fatigue: fatFlags,
|
||||
validation: valFlags
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up state file
|
||||
removeFile(sf);
|
||||
process.exit(0);
|
||||
69
plugins/ai-psychosis/hooks/scripts/session-start.mjs
Normal file
69
plugins/ai-psychosis/hooks/scripts/session-start.mjs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// Interaction Awareness — SessionStart hook (Layer 2, Node.js)
|
||||
// Registers session, counts daily sessions, checks late-night usage.
|
||||
|
||||
import {
|
||||
readStdin, initConfig, requireLayer, getSessionId,
|
||||
nowEpoch, nowIso, currentHour, isLateNight,
|
||||
STATE_DIR, SESSIONS_LOG, THRESHOLD_SOFT_SESSIONS,
|
||||
ensureDir, appendJsonl, writeState, sessionsToday,
|
||||
outputWithContext
|
||||
} from './lib.mjs';
|
||||
|
||||
readStdin();
|
||||
initConfig();
|
||||
requireLayer(2);
|
||||
|
||||
const sid = getSessionId();
|
||||
if (!sid) {
|
||||
process.stdout.write(JSON.stringify({ continue: true }) + '\n');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
ensureDir(STATE_DIR);
|
||||
|
||||
const nowTs = nowEpoch();
|
||||
const nowIsoStr = nowIso();
|
||||
const hour = currentHour();
|
||||
const lateNight = isLateNight();
|
||||
|
||||
// Create session state file
|
||||
const state = {
|
||||
start_epoch: nowTs,
|
||||
start_iso: nowIsoStr,
|
||||
tool_count: 0,
|
||||
edit_count: 0,
|
||||
last_event_epoch: 0,
|
||||
burst_count: 0,
|
||||
dep_flags: 0,
|
||||
esc_flags: 0,
|
||||
fatigue_flags: 0,
|
||||
val_flags: 0,
|
||||
last_warning_epoch: 0
|
||||
};
|
||||
writeState(state);
|
||||
|
||||
// Append to sessions.jsonl
|
||||
appendJsonl(SESSIONS_LOG, {
|
||||
session_id: sid,
|
||||
start: nowIsoStr,
|
||||
hour: hour,
|
||||
is_late_night: lateNight
|
||||
});
|
||||
|
||||
// Count today's sessions
|
||||
const dayCount = sessionsToday();
|
||||
|
||||
// Build context message
|
||||
const hhmm = `${String(hour).padStart(2, '0')}:${String(new Date().getMinutes()).padStart(2, '0')}`;
|
||||
let msg = 'Interaction Awareness is active. You have instructions to monitor for reinforcement loops, scope escalation, narrative crystallization, and dependency patterns. When you notice these patterns, name them calmly.';
|
||||
msg += ` Session #${dayCount} today. Started at ${hhmm}.`;
|
||||
|
||||
if (lateNight) {
|
||||
msg += ` Late-night session (${hhmm}). Sleep deprivation amplifies all interaction risks.`;
|
||||
}
|
||||
|
||||
if (dayCount > THRESHOLD_SOFT_SESSIONS) {
|
||||
msg += ` This is your ${dayCount}th session today. Consider whether you need a longer break.`;
|
||||
}
|
||||
|
||||
outputWithContext(msg);
|
||||
166
plugins/ai-psychosis/hooks/scripts/tool-tracker.mjs
Normal file
166
plugins/ai-psychosis/hooks/scripts/tool-tracker.mjs
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
// Interaction Awareness — PostToolUse hook (Layer 2, Node.js)
|
||||
// Tracks tool usage, edit ratio, burst detection, session duration.
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import {
|
||||
readStdin, initConfig, requireLayer, getSessionId, getToolName,
|
||||
nowEpoch, nowIso, isLateNight,
|
||||
STATE_DIR, EVENTS_LOG,
|
||||
THRESHOLD_SOFT_DURATION, THRESHOLD_HARD_DURATION,
|
||||
THRESHOLD_SOFT_SESSIONS, THRESHOLD_HARD_SESSIONS,
|
||||
THRESHOLD_SOFT_BURST, THRESHOLD_HARD_BURST, THRESHOLD_BURST_INTERVAL,
|
||||
THRESHOLD_LOW_EDIT_RATIO, THRESHOLD_LOW_EDIT_MIN_DURATION,
|
||||
COOLDOWN_SOFT, COOLDOWN_HARD,
|
||||
readState, sessionStateFile, writeState, appendJsonl, sessionsToday,
|
||||
outputContinue, outputWithContext
|
||||
} from './lib.mjs';
|
||||
|
||||
readStdin();
|
||||
initConfig();
|
||||
requireLayer(2);
|
||||
|
||||
const sid = getSessionId();
|
||||
const sf = sessionStateFile();
|
||||
|
||||
if (!sid || !existsSync(sf)) {
|
||||
process.stdout.write(JSON.stringify({ continue: true }) + '\n');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const tool = getToolName();
|
||||
const nowTs = nowEpoch();
|
||||
const nowIsoStr = nowIso();
|
||||
|
||||
// Append to events log (metadata only — no file paths, no content)
|
||||
appendJsonl(EVENTS_LOG, { ts: nowIsoStr, session_id: sid, tool_name: tool });
|
||||
|
||||
// Read current state
|
||||
let state = readState();
|
||||
let toolCount = (Number(state.tool_count) || 0) + 1;
|
||||
let editCount = Number(state.edit_count) || 0;
|
||||
const lastEvent = Number(state.last_event_epoch) || 0;
|
||||
let burstCount = Number(state.burst_count) || 0;
|
||||
const startEpoch = Number(state.start_epoch) || 0;
|
||||
const lastWarning = Number(state.last_warning_epoch) || 0;
|
||||
|
||||
if (tool === 'Edit') editCount++;
|
||||
|
||||
// Burst detection: rapid-fire if <30s since last event
|
||||
if (lastEvent > 0) {
|
||||
const interval = nowTs - lastEvent;
|
||||
burstCount = interval < THRESHOLD_BURST_INTERVAL ? burstCount + 1 : 0;
|
||||
}
|
||||
|
||||
// Write updated state
|
||||
state.tool_count = toolCount;
|
||||
state.edit_count = editCount;
|
||||
state.last_event_epoch = nowTs;
|
||||
state.burst_count = burstCount;
|
||||
writeState(state);
|
||||
|
||||
// Check thresholds every 25 calls or when burst threshold hit
|
||||
let shouldCheck = false;
|
||||
if (toolCount % 25 === 0) shouldCheck = true;
|
||||
if (burstCount === THRESHOLD_SOFT_BURST || burstCount === THRESHOLD_HARD_BURST) shouldCheck = true;
|
||||
|
||||
if (!shouldCheck) {
|
||||
outputContinue();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// --- Threshold analysis ---
|
||||
|
||||
let durationMin = 0;
|
||||
if (startEpoch > 0) {
|
||||
durationMin = Math.floor((nowTs - startEpoch) / 60);
|
||||
}
|
||||
|
||||
let editRatio = 0;
|
||||
if (toolCount > 0) {
|
||||
editRatio = Math.floor(editCount * 100 / toolCount);
|
||||
}
|
||||
|
||||
const dayCount = sessionsToday();
|
||||
|
||||
// Determine warning level
|
||||
let level = ''; // 'soft' or 'hard'
|
||||
const messages = [];
|
||||
|
||||
// Duration thresholds
|
||||
if (durationMin >= THRESHOLD_HARD_DURATION) {
|
||||
level = 'hard';
|
||||
const hours = Math.floor(durationMin / 60);
|
||||
const mins = durationMin % 60;
|
||||
messages.push(`Session duration: ${hours}h${mins}m.`);
|
||||
} else if (durationMin >= THRESHOLD_SOFT_DURATION) {
|
||||
level = 'soft';
|
||||
messages.push(`Session: ${durationMin} min.`);
|
||||
}
|
||||
|
||||
// Session count
|
||||
if (dayCount >= THRESHOLD_HARD_SESSIONS) {
|
||||
level = 'hard';
|
||||
messages.push(`${dayCount} sessions today.`);
|
||||
} else if (dayCount > THRESHOLD_SOFT_SESSIONS) {
|
||||
if (!level) level = 'soft';
|
||||
messages.push(`${dayCount} sessions today.`);
|
||||
}
|
||||
|
||||
// Burst
|
||||
if (burstCount >= THRESHOLD_HARD_BURST) {
|
||||
level = 'hard';
|
||||
messages.push(`Rapid-fire: ${burstCount} consecutive fast interactions.`);
|
||||
} else if (burstCount >= THRESHOLD_SOFT_BURST) {
|
||||
if (!level) level = 'soft';
|
||||
messages.push(`Rapid-fire: ${burstCount} consecutive fast interactions.`);
|
||||
}
|
||||
|
||||
// Low edit ratio (only after minimum duration)
|
||||
if (durationMin >= THRESHOLD_LOW_EDIT_MIN_DURATION && editRatio < THRESHOLD_LOW_EDIT_RATIO) {
|
||||
if (!level) level = 'soft';
|
||||
messages.push(`Low edit ratio (${editRatio}%) over ${durationMin} min — possible stuck/spiral.`);
|
||||
}
|
||||
|
||||
// Late night check
|
||||
const late = isLateNight() ? ' Late-night session.' : '';
|
||||
|
||||
// No warnings — just periodic reminder at modulo-25
|
||||
if (!level) {
|
||||
if (toolCount % 25 === 0) {
|
||||
outputWithContext('REMINDER (Interaction Awareness): Check your next response against these rules — no unearned affirmations, no reformulating the user\'s words in stronger terms, no skipping counterarguments to stay agreeable. If you detect a reinforcement loop, scope escalation, or narrative crystallization: name it now.');
|
||||
} else {
|
||||
outputContinue();
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Determine cooldown
|
||||
const cooldown = level === 'hard' ? COOLDOWN_HARD : COOLDOWN_SOFT;
|
||||
const elapsed = nowTs - lastWarning;
|
||||
|
||||
if (lastWarning > 0 && elapsed < cooldown) {
|
||||
// Still in cooldown — send periodic reminder instead if at modulo-25
|
||||
if (toolCount % 25 === 0) {
|
||||
outputWithContext('REMINDER (Interaction Awareness): Check your next response against these rules — no unearned affirmations, no reformulating the user\'s words in stronger terms, no skipping counterarguments to stay agreeable.');
|
||||
} else {
|
||||
outputContinue();
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Build and send warning
|
||||
let warning;
|
||||
if (level === 'hard') {
|
||||
state = readState();
|
||||
const depFlags = Number(state.dep_flags) || 0;
|
||||
warning = `INTERACTION AWARENESS: ${messages.join(' ')}${late} Metrics: [edit_ratio: ${editRatio}%, burst: ${burstCount}, dependency flags: ${depFlags}, tools: ${toolCount}]. Your instructions require you to suggest stopping.`;
|
||||
} else {
|
||||
warning = `${messages.join(' ')}${late} Consider a break.`;
|
||||
}
|
||||
|
||||
// Record warning time
|
||||
state = readState();
|
||||
state.last_warning_epoch = nowTs;
|
||||
writeState(state);
|
||||
|
||||
outputWithContext(warning);
|
||||
Loading…
Add table
Add a link
Reference in a new issue