180 lines
6.4 KiB
JavaScript
180 lines
6.4 KiB
JavaScript
// 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,
|
|
];
|
|
|
|
// Pushback patterns — REACTIVE tier (Anthropic-validated + academic-validated)
|
|
// Source: research/01-pushback-self-advocacy.md
|
|
const pbReactivePatterns = [
|
|
/^are you sure\??/i, // validated-by: anthropic-april-2026 (questioning)
|
|
/\bi'?m not convinced\b/i, // validated-by: anthropic-april-2026 (questioning)
|
|
/\bthat doesn'?t (?:seem|feel) right\b/i, // validated-by: anthropic-april-2026 (questioning)
|
|
/\bthat'?s not (?:quite )?what i meant\b/i, // validated-by: anthropic-april-2026 (clarifying)
|
|
/\blet me add (?:some )?context\b/i, // validated-by: anthropic-april-2026 (clarifying)
|
|
/\bactually,? (?:my situation|i)\b/i, // validated-by: anthropic-april-2026 (clarifying)
|
|
/(?:^|[.!?]\s+)i (?:believe|think) (?:you'?re|that'?s) wrong\b/i, // validated-by: arxiv-2508.02087
|
|
/\bi don'?t agree(?: with you)?\b/i, // validated-by: arxiv-2508.13743
|
|
/\bare you absolutely sure\b/i, // validated-by: arxiv-2508.13743
|
|
];
|
|
// Pushback patterns — PREEMPTIVE tier (community-derived)
|
|
const pbPreemptivePatterns = [
|
|
/\bsteelman\b/i, // validated-by: community-multi-source-2025
|
|
/\bplay (?:the )?devil'?s advocate\b/i, // validated-by: community-multi-source-2025
|
|
/\bargue against (?:this|my)\b/i, // validated-by: community-multi-source-2025
|
|
];
|
|
// Domain-context: relationship — uses (?:my|our) prefix to avoid false positives
|
|
// on technical "function relationship", "database relationship" etc.
|
|
const domainRelationshipPatterns = [
|
|
/\b(?:my|our) (?:partner|spouse|wife|husband|girlfriend|boyfriend)\b/i,
|
|
/\bin our relationship\b/i,
|
|
/\b(?:dating|breakup|divorce)\b/i,
|
|
/\bromantic(?:ally)? (?:involved|interested)\b/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; } }
|
|
let pbReactiveHit = 0; for (const p of pbReactivePatterns) { if (p.test(prompt)) { pbReactiveHit = 1; break; } }
|
|
let pbPreemptiveHit = 0; for (const p of pbPreemptivePatterns) { if (p.test(prompt)) { pbPreemptiveHit = 1; break; } }
|
|
let domainHit = 0; for (const p of domainRelationshipPatterns) { if (p.test(prompt)) { domainHit = 1; break; } }
|
|
|
|
// Clear prompt from memory
|
|
prompt = '';
|
|
|
|
// Same-invocation valence guard (research/01 frustration-spiral finding):
|
|
// pushback in fat/esc context is NOT protective — suppress in same prompt.
|
|
if (fatHit === 1 || escHit === 1) {
|
|
pbReactiveHit = 0;
|
|
pbPreemptiveHit = 0;
|
|
}
|
|
|
|
// 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;
|
|
state.pushback_count = (Number(state.pushback_count) || 0) + pbReactiveHit + pbPreemptiveHit;
|
|
if (domainHit === 1 && !state.domain_context) state.domain_context = 'relationship';
|
|
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();
|
|
}
|