Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 38 additions & 52 deletions plugin/scripts/bun-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,59 +161,45 @@ if (child.stdin) {
// Lifecycle commands don't need stdin — close pipe and let child run.
try { child.stdin.end(); } catch {}
} else {
// Issue #2188: empty/missing stdin previously masked by `|| '{}'` fallback,
// which silently hid WSL bash failures (e.g. hooks invoked under a broken
// shell that never piped a payload). Surface the failure mode instead.
const dataDir = process.env.CLAUDE_MEM_DATA_DIR || join(homedir(), '.claude-mem');
const payloadType = stdinData === null
? 'null (no data event or stream error)'
: stdinData === undefined
? 'undefined'
: Buffer.isBuffer(stdinData) && stdinData.length === 0
? 'empty Buffer (zero bytes received)'
: `unexpected (${typeof stdinData})`;
const payloadByteLength = (stdinData && typeof stdinData.length === 'number')
? stdinData.length
: 0;
const diagnostic = [
`[bun-runner] empty stdin payload received — issue #2188`,
` script: ${args[0]}`,
` payload byte length: ${payloadByteLength}`,
` payload type: ${payloadType}`,
` platform: ${process.platform}`,
` shell: ${process.env.SHELL || 'n/a'}`,
` stdin TTY: ${process.stdin.isTTY === true ? 'true' : process.stdin.isTTY === false ? 'false' : 'undefined'}`,
` timestamp: ${new Date().toISOString()}`,
` CLAUDE_PLUGIN_ROOT: ${RESOLVED_PLUGIN_ROOT}`,
].join('\n');

// IO discipline (see src/shared/hook-io.ts intent vocabulary):
// - this stderr write is a USER_HINT (Claude Code surfaces it inline).
// - the CAPTURE_BROKEN marker file below is a DIAGNOSTIC durable signal for
// the next session-start hint.
// - exit 0 below is the EXIT_SIGNAL per CLAUDE.md (Windows Terminal tab
// management); the marker file, not the exit code, is the durable failure
// signal. bun-runner runs in its own node process BEFORE hookCommand's
// stderr buffer is installed, so this write is never swallowed.

// Write to stderr so Claude Code surfaces the diagnostic.
console.error(diagnostic);

// Persist diagnostic to the runner-errors log and drop a CAPTURE_BROKEN marker
// file so the next session-start hint can surface the failure. We exit 0 to
// honor the project's exit-code strategy (worker/hook errors exit 0 to
// prevent Windows Terminal tab pileup) — the marker file is the durable
// signal that something is wrong, not the exit code.
try {
const logsDir = join(dataDir, 'logs');
mkdirSync(logsDir, { recursive: true });
appendFileSync(join(logsDir, 'runner-errors.log'), diagnostic + '\n\n');
mkdirSync(dataDir, { recursive: true });
writeFileSync(join(dataDir, 'CAPTURE_BROKEN'), diagnostic + '\n');
} catch (writeErr) {
console.error(`[bun-runner] failed to persist diagnostic: ${writeErr && writeErr.message ? writeErr.message : writeErr}`);
// Non-lifecycle hooks with empty stdin are a no-op. Cursor (and the Claude
// Code ↔ Cursor bridge) routinely invoke shell/MCP hooks without a payload;
// issue #2188 diagnostics (CAPTURE_BROKEN + runner-errors.log) produced
// persistent false positives on macOS. Set CLAUDE_MEM_STRICT_STDIN=1 to
// restore the #2188 failure surface for WSL/bash debugging.
if (process.env.CLAUDE_MEM_STRICT_STDIN === '1') {
const dataDir = process.env.CLAUDE_MEM_DATA_DIR || join(homedir(), '.claude-mem');
const payloadType = stdinData === null
? 'null (no data event or stream error)'
: stdinData === undefined
? 'undefined'
: Buffer.isBuffer(stdinData) && stdinData.length === 0
? 'empty Buffer (zero bytes received)'
: `unexpected (${typeof stdinData})`;
const payloadByteLength = (stdinData && typeof stdinData.length === 'number')
? stdinData.length
: 0;
const diagnostic = [
`[bun-runner] empty stdin payload received — issue #2188`,
` script: ${args[0]}`,
` payload byte length: ${payloadByteLength}`,
` payload type: ${payloadType}`,
` platform: ${process.platform}`,
` shell: ${process.env.SHELL || 'n/a'}`,
` stdin TTY: ${process.stdin.isTTY === true ? 'true' : process.stdin.isTTY === false ? 'false' : 'undefined'}`,
` timestamp: ${new Date().toISOString()}`,
` CLAUDE_PLUGIN_ROOT: ${RESOLVED_PLUGIN_ROOT}`,
].join('\n');
console.error(diagnostic);
try {
const logsDir = join(dataDir, 'logs');
mkdirSync(logsDir, { recursive: true });
appendFileSync(join(logsDir, 'runner-errors.log'), diagnostic + '\n\n');
mkdirSync(dataDir, { recursive: true });
writeFileSync(join(dataDir, 'CAPTURE_BROKEN'), diagnostic + '\n');
} catch (writeErr) {
console.error(`[bun-runner] failed to persist diagnostic: ${writeErr && writeErr.message ? writeErr.message : writeErr}`);
}
}

try { child.stdin.end(); } catch {}
try { child.kill(); } catch {}
process.exit(0);
Expand Down
7 changes: 5 additions & 2 deletions plugin/scripts/context-generator.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -531,11 +531,14 @@ ${o.stack}`:` ${o.message}`;else if(this.getLevel()===0&&typeof o=="object")try{
`).run(t,e),d.customTitle&&this.db.prepare(`
UPDATE sdk_sessions SET custom_title = ?
WHERE content_session_id = ? AND custom_title IS NULL
`).run(d.customTitle,e),d.platformSource){let p=g.platform_source?.trim()?D(g.platform_source):void 0;if(!p)this.db.prepare(`
`).run(d.customTitle,e),d.platformSource){let p=g.platform_source?.trim()?D(g.platform_source):void 0;p?p!==d.platformSource&&this.db.prepare(`
UPDATE sdk_sessions SET platform_source = ?
WHERE content_session_id = ?
`).run(d.platformSource,e):this.db.prepare(`
UPDATE sdk_sessions SET platform_source = ?
WHERE content_session_id = ?
AND COALESCE(platform_source, '') = ''
`).run(d.platformSource,e);else if(p!==d.platformSource)throw new Error(`Platform source conflict for session ${e}: existing=${p}, received=${d.platformSource}`)}return g.id}return this.db.prepare(`
`).run(d.platformSource,e)}return g.id}return this.db.prepare(`
INSERT INTO sdk_sessions
(content_session_id, memory_session_id, project, platform_source, user_prompt, custom_title, started_at, started_at_epoch, status)
VALUES (?, NULL, ?, ?, ?, ?, ?, ?, 'active')
Expand Down
2 changes: 1 addition & 1 deletion plugin/scripts/transcript-watcher.cjs

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions plugin/scripts/worker-service.cjs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/cli/adapters/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const cursorAdapter: PlatformAdapter = {
return {
sessionId,
cwd,
platform: 'cursor',
prompt: r.prompt ?? r.query ?? r.input ?? r.message,
toolName: isShellCommand ? 'Bash' : r.tool_name,
toolInput: isShellCommand ? { command: r.command } : r.tool_input,
Expand Down
9 changes: 6 additions & 3 deletions src/services/sqlite/SessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1732,9 +1732,12 @@ export class SessionStore {
AND COALESCE(platform_source, '') = ''
`).run(resolved.platformSource, contentSessionId);
} else if (storedPlatformSource !== resolved.platformSource) {
throw new Error(
`Platform source conflict for session ${contentSessionId}: existing=${storedPlatformSource}, received=${resolved.platformSource}`
);
// Cursor and Claude Code can share a content_session_id across IDEs;
// last-writer wins instead of dropping observations with HTTP 500.
this.db.prepare(`
UPDATE sdk_sessions SET platform_source = ?
WHERE content_session_id = ?
`).run(resolved.platformSource, contentSessionId);
Comment on lines 1734 to +1740

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Default source overwrites
This update treats every conflicting platformSource as authoritative, but current summarize paths still call normalizePlatformSource(req.body.platformSource) / normalizePlatformSource(payload.platformSource), which converts an omitted source to claude. A Cursor session followed by a source-less summarize request now gets relabeled to claude, breaking source-scoped filtering instead of just avoiding the 500.

Artifacts

Repro: focused SessionStore sqlite reproduction script

  • Contains supporting evidence from the run (text/typescript; charset=utf-8).

Repro: command output showing before cursor and after claude database state

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

}
}
return existing.id;
Expand Down
4 changes: 3 additions & 1 deletion src/services/worker/http/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export interface ObservationPayload {
export async function ingestObservation(payload: ObservationPayload): Promise<IngestResult> {
const { sessionManager, dbManager, eventBroadcaster, ensureGeneratorRunning } = requireContext();

const platformSource = normalizePlatformSource(payload.platformSource);
const platformSource = payload.platformSource != null && String(payload.platformSource).trim()
? normalizePlatformSource(payload.platformSource)
: undefined;
const cwd = typeof payload.cwd === 'string' ? payload.cwd : '';
const project = cwd.trim() ? getProjectContext(cwd).primary : '';

Expand Down
10 changes: 5 additions & 5 deletions tests/sqlite/session-store-sessions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,11 @@ describe('SessionStore session lifecycle', () => {
expect(store.getSessionById(id)?.platform_source).toBe('codex');
});

it('throws on an explicit platform_source conflict', () => {
store.createSDKSession('content-platform-3', 'project', 'prompt', undefined, 'codex');
expect(() =>
store.createSDKSession('content-platform-3', 'project', 'prompt', undefined, 'claude')
).toThrow(/Platform source conflict/);
it('updates platform_source when an explicit conflicting value arrives', () => {
const id = store.createSDKSession('content-platform-3', 'project', 'prompt', undefined, 'codex');
store.createSDKSession('content-platform-3', 'project', 'prompt', undefined, 'claude');
store.createSDKSession('content-platform-3', 'project', 'prompt', undefined, 'cursor');
expect(store.getSessionById(id)?.platform_source).toBe('cursor');
});
});

Expand Down