Skip to content

Commit 21d34d7

Browse files
engineerclaude
andcommitted
fix: session-scoped reflection toggle — no cross-instance contamination
Previous .reflection/disabled was a global file shared across all instances in the same directory, so disabling one session silently disabled all of them. New approach: .reflection/disabled is a newline list of session IDs. Each instance only skips its own session ID — two CC/OC processes in the same dir are unaffected by each other's toggle. OpenCode: - isSessionDisabled(sessionId) / setSessionDisabled(sessionId, bool) read+write the session-ID list - reflection tool now uses currentSessionId (updated on each session.idle) so it always targets the active session Claude Code: - reflect.mjs writes .reflection/current_session on every fire so agents can reference their own session ID without knowing it - disabled check reads the list and matches session_id from payload - To disable: echo "SESSION_ID" >> .reflection/disabled (or: echo $(cat .reflection/current_session) >> .reflection/disabled) - To enable: grep -v SESSION_ID .reflection/disabled > .tmp && mv .tmp .reflection/disabled Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
1 parent 1882ca2 commit 21d34d7

2 files changed

Lines changed: 58 additions & 22 deletions

File tree

claude/bin/reflect.mjs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -447,12 +447,26 @@ async function main() {
447447
// uncaughtException handler exits 0 (fail-safe: no inject, no fs ops).
448448
const cwd = sanitizeCwd(payload?.cwd ?? process.cwd());
449449

450-
// ── 1.5. DISABLED FLAG ───────────────────────────────────────────────────
451-
const disabledFlag = path.join(cwd, '.reflection', 'disabled');
452-
if (fs.existsSync(disabledFlag)) {
453-
debug({ msg: 'disabled_by_flag' }, cwd);
454-
process.exit(0);
455-
}
450+
// ── 1.5. SESSION-SCOPED DISABLED CHECK ──────────────────────────────────
451+
// Write current session ID so agents can reference it without knowing it upfront:
452+
// cat .reflection/current_session
453+
// Disable this session:
454+
// echo "SESSION_ID" >> .reflection/disabled
455+
// Enable:
456+
// grep -v "SESSION_ID" .reflection/disabled > .reflection/disabled.tmp && mv .reflection/disabled.tmp .reflection/disabled
457+
const reflDir = path.join(cwd, '.reflection');
458+
fs.mkdirSync(reflDir, { recursive: true });
459+
fs.writeFileSync(path.join(reflDir, 'current_session'), session_id, 'utf8');
460+
461+
const disabledFlag = path.join(reflDir, 'disabled');
462+
try {
463+
const disabledIds = fs.readFileSync(disabledFlag, 'utf8')
464+
.split('\n').map(l => l.trim()).filter(Boolean);
465+
if (disabledIds.includes(session_id)) {
466+
debug({ msg: 'disabled_for_session', session_id }, cwd);
467+
process.exit(0);
468+
}
469+
} catch { /* file absent = not disabled */ }
456470

457471
// ── 2. ATTEMPT CAP ────────────────────────────────────────────────────────
458472
const attempts = readAttempts(session_id, cwd);

reflection-3.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,6 +1565,31 @@ export const Reflection3Plugin: Plugin = async ({ client, directory }) => {
15651565
const recentlyAbortedSessions = new Map<string, number>()
15661566
const attempts = new Map<string, number>()
15671567
let toolReflectionPrompt: string | null = null
1568+
let currentSessionId: string | null = null // tracks most recent active session for the toggle tool
1569+
1570+
const disabledFlagPath = join(directory, '.reflection', 'disabled')
1571+
1572+
async function isSessionDisabled(sessionId: string): Promise<boolean> {
1573+
try {
1574+
const content = await readFile(disabledFlagPath, 'utf8')
1575+
return content.split('\n').map(l => l.trim()).filter(Boolean).includes(sessionId)
1576+
} catch { return false }
1577+
}
1578+
1579+
async function setSessionDisabled(sessionId: string, disabled: boolean): Promise<void> {
1580+
let lines: string[] = []
1581+
try {
1582+
const content = await readFile(disabledFlagPath, 'utf8')
1583+
lines = content.split('\n').map(l => l.trim()).filter(Boolean)
1584+
} catch {}
1585+
if (disabled) {
1586+
if (!lines.includes(sessionId)) lines.push(sessionId)
1587+
} else {
1588+
lines = lines.filter(l => l !== sessionId)
1589+
}
1590+
await mkdir(join(directory, '.reflection'), { recursive: true })
1591+
await writeFile(disabledFlagPath, lines.join('\n') + (lines.length ? '\n' : ''))
1592+
}
15681593

15691594
const setReflectionDescription = "Use this for difficult or complex tasks to set reflection guidance with a plan/checklist. Provide concrete steps and verification checks so reflection can validate completion quality and catch missed work."
15701595
const executeSetReflection = async (args: { guidance?: string; clear?: boolean }) => {
@@ -1585,21 +1610,15 @@ export const Reflection3Plugin: Plugin = async ({ client, directory }) => {
15851610
return msg
15861611
}
15871612

1588-
// reflection on/off toggle tool
1589-
const reflectionToggleDescription = "Enable or disable the reflection judge for this project. Use 'off' to stop reflection from firing on session.idle; 'on' to re-enable it. State persists via .reflection/disabled flag file."
1613+
// reflection on/off toggle tool (session-scoped: only affects the current session's ID)
1614+
const reflectionToggleDescription = "Enable or disable the reflection judge for the current session. 'off' adds this session's ID to .reflection/disabled so the judge skips it; 'on' removes it. Other sessions and other running instances are unaffected."
15901615
const executeReflectionToggle = async (args: { action: "on" | "off" }) => {
1591-
const flagPath = join(directory, '.reflection', 'disabled')
1592-
if (args.action === "off") {
1593-
await mkdir(join(directory, '.reflection'), { recursive: true })
1594-
await writeFile(flagPath, '')
1595-
return "Reflection disabled for this project. Touch .reflection/disabled to re-disable, or call this tool with action=on."
1596-
} else {
1597-
try {
1598-
const { unlink } = await import("fs/promises")
1599-
await unlink(flagPath)
1600-
} catch {}
1601-
return "Reflection enabled. The judge will fire on the next session.idle."
1602-
}
1616+
const sid = currentSessionId
1617+
if (!sid) return "No active session tracked yet — wait for the first session.idle event, then try again."
1618+
await setSessionDisabled(sid, args.action === "off")
1619+
return args.action === "off"
1620+
? `Reflection disabled for session ${sid.slice(0, 8)}… (added to .reflection/disabled).`
1621+
: `Reflection enabled for session ${sid.slice(0, 8)}… (removed from .reflection/disabled).`
16031622
}
16041623

16051624
let setReflectionTool: any = null
@@ -1649,8 +1668,11 @@ export const Reflection3Plugin: Plugin = async ({ client, directory }) => {
16491668
debug("runReflection called for session:", sessionId.slice(0, 8))
16501669
if (activeReflections.has(sessionId)) return
16511670

1652-
// Disabled flag: `touch .reflection/disabled` (project) disables reflection
1653-
try { await stat(join(directory, '.reflection', 'disabled')); return } catch {}
1671+
currentSessionId = sessionId
1672+
if (await isSessionDisabled(sessionId)) {
1673+
debug("Reflection disabled for session:", sessionId.slice(0, 8))
1674+
return
1675+
}
16541676

16551677
activeReflections.add(sessionId)
16561678

0 commit comments

Comments
 (0)