ai-psychosis/hooks/scripts/session-start.mjs
Kjell Tore Guttormsen 7d53a7325e fix(hooks): include required hookEventName in hookSpecificOutput JSON
outputWithContext() emitted a hookSpecificOutput object without the
required hookEventName field, so Claude Code rejected the hook output
("hookSpecificOutput is missing required field hookEventName") and
dropped the context the SessionStart, UserPromptSubmit, and PostToolUse
hooks tried to inject. Thread the event name through the helper; each
caller now passes its own (SessionStart / UserPromptSubmit / PostToolUse).
Existing emission tests now assert hookEventName per event.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
2026-06-24 06:46:11 +02:00

96 lines
3.2 KiB
JavaScript

// 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,
TIER2_SESSION_THRESHOLD, HIGH_STAKES_DOMAINS,
ensureDir, appendJsonl, writeState, sessionsToday,
readRecentEndRecords, checkCooldown,
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,
pushback_count: 0,
domain_context: null,
// v1.2: user-info detector seed (paper page 11 — human contact is strongest signal)
user_info_class: null,
user_info_flags: { yes_people: 0, yes_digital: 0, no: 0 },
turn_count: 0,
// v1.2: validation-seeking detector seed
valseek_count: 0,
valseek_flag: 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.`;
}
// v1.2: Tier-2 cross-session isolation alert.
// Fires when the last N completed sessions all classify user as 'no' (no human
// contact) AND each one had at least one HIGH_STAKES_DOMAINS hit. This signals
// a sustained pattern across sessions, not just one-off context.
const recent = readRecentEndRecords(TIER2_SESSION_THRESHOLD);
if (recent.length >= TIER2_SESSION_THRESHOLD) {
const allNo = recent.every(r => r.user_info_class === 'no');
const allHighStakes = recent.every(r => {
const ds = Array.isArray(r.domain_context) ? r.domain_context : (r.domain_context ? [r.domain_context] : []);
return ds.some(d => HIGH_STAKES_DOMAINS.includes(d));
});
if (allNo && allHighStakes) {
msg += ` INTERACTION AWARENESS (tier-2 cross-session isolation): ${recent.length} consecutive sessions show no human contact in high-stakes domains. This is a sustained pattern. Recommend a human check-in (trusted person, professional, or domain specialist) before proceeding here.`;
}
}
outputWithContext(msg, 'SessionStart');