feat(ai-psychosis): tier-2 user-info isolation alert (cross-session)
This commit is contained in:
parent
4fd5e7b24a
commit
61584f42d6
3 changed files with 105 additions and 0 deletions
|
|
@ -52,6 +52,11 @@ if (startEpoch > 0) {
|
||||||
durationMin = Math.floor((nowTs - startEpoch) / 60);
|
durationMin = Math.floor((nowTs - startEpoch) / 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v1.2: also persist user_info_class (read-only — set during prompt-analyzer).
|
||||||
|
const userInfoClass = state.user_info_class || null;
|
||||||
|
const valseekCount = Number(state.valseek_count) || 0;
|
||||||
|
const turnCount = Number(state.turn_count) || 0;
|
||||||
|
|
||||||
// Append finalized session record
|
// Append finalized session record
|
||||||
appendJsonl(SESSIONS_LOG, {
|
appendJsonl(SESSIONS_LOG, {
|
||||||
session_id: sid,
|
session_id: sid,
|
||||||
|
|
@ -61,6 +66,9 @@ appendJsonl(SESSIONS_LOG, {
|
||||||
tool_count: toolCount,
|
tool_count: toolCount,
|
||||||
edit_count: editCount,
|
edit_count: editCount,
|
||||||
domain_context: domainContextArray,
|
domain_context: domainContextArray,
|
||||||
|
user_info_class: userInfoClass,
|
||||||
|
valseek_count: valseekCount,
|
||||||
|
turn_count: turnCount,
|
||||||
flags: {
|
flags: {
|
||||||
dependency: depFlags,
|
dependency: depFlags,
|
||||||
escalation: escFlags,
|
escalation: escFlags,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ import {
|
||||||
readStdin, initConfig, requireLayer, getSessionId,
|
readStdin, initConfig, requireLayer, getSessionId,
|
||||||
nowEpoch, nowIso, currentHour, isLateNight,
|
nowEpoch, nowIso, currentHour, isLateNight,
|
||||||
STATE_DIR, SESSIONS_LOG, THRESHOLD_SOFT_SESSIONS,
|
STATE_DIR, SESSIONS_LOG, THRESHOLD_SOFT_SESSIONS,
|
||||||
|
TIER2_SESSION_THRESHOLD, HIGH_STAKES_DOMAINS,
|
||||||
ensureDir, appendJsonl, writeState, sessionsToday,
|
ensureDir, appendJsonl, writeState, sessionsToday,
|
||||||
|
readRecentEndRecords, checkCooldown,
|
||||||
outputWithContext
|
outputWithContext
|
||||||
} from './lib.mjs';
|
} from './lib.mjs';
|
||||||
|
|
||||||
|
|
@ -75,4 +77,20 @@ if (dayCount > THRESHOLD_SOFT_SESSIONS) {
|
||||||
msg += ` This is your ${dayCount}th session today. Consider whether you need a longer break.`;
|
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);
|
outputWithContext(msg);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { describe, it, afterEach } from 'node:test';
|
import { describe, it, afterEach } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { writeFileSync } from 'fs';
|
||||||
import { runHook, setupTestDir, cleanupTestDir, readState, readJsonl } from './test-helper.mjs';
|
import { runHook, setupTestDir, cleanupTestDir, readState, readJsonl } from './test-helper.mjs';
|
||||||
|
|
||||||
let dir;
|
let dir;
|
||||||
|
|
@ -55,4 +56,82 @@ describe('session-start', () => {
|
||||||
assert.equal(state.pushback_count, 0);
|
assert.equal(state.pushback_count, 0);
|
||||||
assert.equal(state.domain_context, null);
|
assert.equal(state.domain_context, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('initializes v1.2 user-info, valseek, turn_count fields', () => {
|
||||||
|
dir = setupTestDir();
|
||||||
|
runHook('session-start.mjs', { session_id: 's4b', cwd: '/tmp' }, dir);
|
||||||
|
const state = readState(dir, 's4b');
|
||||||
|
assert.equal(state.user_info_class, null);
|
||||||
|
assert.deepEqual(state.user_info_flags, { yes_people: 0, yes_digital: 0, no: 0 });
|
||||||
|
assert.equal(state.turn_count, 0);
|
||||||
|
assert.equal(state.valseek_count, 0);
|
||||||
|
assert.equal(state.valseek_flag, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Tier-2 cross-session alert ---
|
||||||
|
//
|
||||||
|
// Fires at SessionStart when last 3 end records all have user_info_class='no'
|
||||||
|
// AND each session had at least one HIGH_STAKES_DOMAINS hit.
|
||||||
|
|
||||||
|
function writeFixture(dir, records) {
|
||||||
|
const lines = records.map(r => JSON.stringify(r)).join('\n') + '\n';
|
||||||
|
writeFileSync(join(dir, 'sessions.jsonl'), lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('tier-2 cross-session isolation alert', () => {
|
||||||
|
it('fires when 3 prior end records all show no + high-stakes', () => {
|
||||||
|
dir = setupTestDir();
|
||||||
|
writeFixture(dir, [
|
||||||
|
{ session_id: 'p1', duration_min: 30, user_info_class: 'no', domain_context: ['legal'] },
|
||||||
|
{ session_id: 'p2', duration_min: 25, user_info_class: 'no', domain_context: ['health'] },
|
||||||
|
{ session_id: 'p3', duration_min: 40, user_info_class: 'no', domain_context: ['parenting', 'financial'] },
|
||||||
|
]);
|
||||||
|
const out = runHook('session-start.mjs', { session_id: 'snew', cwd: '/tmp' }, dir);
|
||||||
|
assert.match(out.hookSpecificOutput.additionalContext, /tier-2/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does NOT fire when only 2 prior "no" records exist', () => {
|
||||||
|
dir = setupTestDir();
|
||||||
|
writeFixture(dir, [
|
||||||
|
{ session_id: 'p1', duration_min: 30, user_info_class: 'no', domain_context: ['legal'] },
|
||||||
|
{ session_id: 'p2', duration_min: 30, user_info_class: 'no', domain_context: ['health'] },
|
||||||
|
]);
|
||||||
|
const out = runHook('session-start.mjs', { session_id: 'snew2', cwd: '/tmp' }, dir);
|
||||||
|
const text = out.hookSpecificOutput.additionalContext;
|
||||||
|
assert.ok(!/tier-2/.test(text), 'tier-2 must require N consecutive sessions');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does NOT fire when one record has yes_people class', () => {
|
||||||
|
dir = setupTestDir();
|
||||||
|
writeFixture(dir, [
|
||||||
|
{ session_id: 'p1', duration_min: 30, user_info_class: 'no', domain_context: ['legal'] },
|
||||||
|
{ session_id: 'p2', duration_min: 30, user_info_class: 'yes_people', domain_context: ['health'] },
|
||||||
|
{ session_id: 'p3', duration_min: 30, user_info_class: 'no', domain_context: ['financial'] },
|
||||||
|
]);
|
||||||
|
const out = runHook('session-start.mjs', { session_id: 'snew3', cwd: '/tmp' }, dir);
|
||||||
|
assert.ok(!/tier-2/.test(out.hookSpecificOutput.additionalContext));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does NOT fire when any session is in low-stakes domain', () => {
|
||||||
|
dir = setupTestDir();
|
||||||
|
writeFixture(dir, [
|
||||||
|
{ session_id: 'p1', duration_min: 30, user_info_class: 'no', domain_context: ['legal'] },
|
||||||
|
{ session_id: 'p2', duration_min: 30, user_info_class: 'no', domain_context: ['consumer'] },
|
||||||
|
{ session_id: 'p3', duration_min: 30, user_info_class: 'no', domain_context: ['health'] },
|
||||||
|
]);
|
||||||
|
const out = runHook('session-start.mjs', { session_id: 'snew4', cwd: '/tmp' }, dir);
|
||||||
|
assert.ok(!/tier-2/.test(out.hookSpecificOutput.additionalContext));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles v1.1.0 records with string domain_context (backward compat)', () => {
|
||||||
|
dir = setupTestDir();
|
||||||
|
writeFixture(dir, [
|
||||||
|
{ session_id: 'p1', duration_min: 30, user_info_class: 'no', domain_context: 'health' }, // string shape
|
||||||
|
{ session_id: 'p2', duration_min: 30, user_info_class: 'no', domain_context: ['legal'] },
|
||||||
|
{ session_id: 'p3', duration_min: 30, user_info_class: 'no', domain_context: ['parenting'] },
|
||||||
|
]);
|
||||||
|
const out = runHook('session-start.mjs', { session_id: 'snew5', cwd: '/tmp' }, dir);
|
||||||
|
assert.match(out.hookSpecificOutput.additionalContext, /tier-2/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue