Merge branch 'pr2942' into mainline

# Conflicts:
#	src/services/worker/ClaudeProvider.ts
#	src/supervisor/process-registry.ts
This commit is contained in:
Alex Newman
2026-07-23 00:10:16 -07:00
5 changed files with 85 additions and 17 deletions
+4
View File
@@ -32,6 +32,10 @@ npm run build-and-sync # Build, sync to marketplace, restart worker
No need to edit the changelog ever, it's generated automatically.
## Local Status Notes
- 2026-06-15: Issue #2909 is intentionally split. PR #2919 covers session-isolation/read-path behavior, while target 29 covers the observer `.jsonl` accumulation half by disabling session persistence for observer tool-use SDK queries.
## Daily Maintenance
Run a daily version check across all package manifests and upgrade every dependency to its latest version — including major version bumps. Staying on the latest is the goal; do not skip majors.
+12 -3
View File
@@ -174,6 +174,7 @@ export class ClaudeProvider {
async startSession(session: ActiveSession, worker?: WorkerRef): Promise<void> {
const cwdTracker = { lastCwd: undefined as string | undefined };
const observerExtraArgs = ['--no-session-persistence'];
// Find and validate Claude executable (shared utility, closes #2222)
let claudePath: string;
@@ -199,8 +200,16 @@ export class ClaudeProvider {
const activeResponseContext = { current: snapshotResponseContext(session) };
const messageGenerator = this.createMessageGenerator(session, cwdTracker, activeResponseContext);
const hasRealMemorySessionId = !!session.memorySessionId;
const shouldResume = hasRealMemorySessionId && session.lastPromptNumber > 1 && !session.forceInit;
if (session.memorySessionId) {
// Observer spawns intentionally opt out of Claude transcript persistence.
// A carried session_id from an earlier no-persist spawn is therefore not
// safe to feed back into `resume` on a later fresh process.
this.dbManager.getSessionStore().updateMemorySessionId(session.sessionDbId, null);
session.memorySessionId = null;
}
const hasRealMemorySessionId = false;
const shouldResume = false;
if (session.forceInit) {
logger.info('SDK', 'forceInit flag set, starting fresh SDK session', {
@@ -256,7 +265,7 @@ export class ClaudeProvider {
pathToClaudeCodeExecutable: claudePath,
abortController: session.abortController,
...(shouldResume && session.memorySessionId ? { resume: session.memorySessionId } : {}),
spawnClaudeCodeProcess: createSdkSpawnFactory(session.sessionDbId, slotReservation),
spawnClaudeCodeProcess: createSdkSpawnFactory(session.sessionDbId, slotReservation, observerExtraArgs),
}),
});
+31 -13
View File
@@ -618,11 +618,36 @@ export interface SpawnedSdkProcess {
export interface SpawnSdkOptions {
command: string;
args: string[];
extraArgs?: string[];
cwd?: string;
env?: NodeJS.ProcessEnv;
signal?: AbortSignal;
}
export function normalizeSpawnSdkArgs(args: string[], extraArgs: string[] = []): string[] {
const filteredArgs: string[] = [];
for (const arg of args) {
if (arg === '') {
// The SDK encodes optional flag/value pairs as `--flag ''` when the
// value is absent. Strip the whole pair, but only when the preceding
// token is a long option so positional args are left untouched.
if (filteredArgs.length > 0 && filteredArgs[filteredArgs.length - 1].startsWith('--')) {
filteredArgs.pop();
}
continue;
}
filteredArgs.push(arg);
}
for (const extraArg of extraArgs) {
if (extraArg !== '') {
filteredArgs.push(extraArg);
}
}
return filteredArgs;
}
export function spawnSdkProcess(
sessionDbId: number,
options: SpawnSdkOptions
@@ -631,17 +656,7 @@ export function spawnSdkProcess(
const useCmdWrapper = process.platform === 'win32' && options.command.endsWith('.cmd');
const env = sanitizeEnv(options.env ?? process.env);
const filteredArgs: string[] = [];
for (const arg of options.args) {
if (arg === '') {
if (filteredArgs.length > 0 && filteredArgs[filteredArgs.length - 1].startsWith('--')) {
filteredArgs.pop();
}
continue;
}
filteredArgs.push(arg);
}
const filteredArgs = normalizeSpawnSdkArgs(options.args, options.extraArgs);
const isWin = process.platform === 'win32';
const child = useCmdWrapper
@@ -763,7 +778,7 @@ function sigtermDuplicateSdkProcess(record: ManagedProcessRecord, sessionDbId: n
});
}
export function createSdkSpawnFactory(sessionDbId: number, slotReservation?: SlotReservation) {
export function createSdkSpawnFactory(sessionDbId: number, slotReservation?: SlotReservation, extraArgs: string[] = []) {
return (spawnOptions: SpawnSdkOptions): SpawnedSdkProcess => {
const registry = getProcessRegistry();
@@ -788,7 +803,10 @@ export function createSdkSpawnFactory(sessionDbId: number, slotReservation?: Slo
let result: ReturnType<typeof spawnSdkProcess>;
try {
result = spawnSdkProcess(sessionDbId, spawnOptions);
result = spawnSdkProcess(sessionDbId, {
...spawnOptions,
extraArgs: [...(spawnOptions.extraArgs ?? []), ...extraArgs],
});
} finally {
// The waitForSlot() reservation is consumed here: on success the
// process is now a registry record (registered inside spawnSdkProcess)
+14
View File
@@ -4,7 +4,9 @@ describe('ClaudeProvider Resume Parameter Logic', () => {
function shouldPassResumeParameter(session: {
memorySessionId: string | null;
lastPromptNumber: number;
disableSessionPersistence?: boolean;
}): boolean {
if (session.disableSessionPersistence) return false;
const hasRealMemorySessionId = !!session.memorySessionId;
return hasRealMemorySessionId && session.lastPromptNumber > 1;
}
@@ -126,5 +128,17 @@ describe('ClaudeProvider Resume Parameter Logic', () => {
expect(shouldResume).toBe(true);
});
it('should NOT resume when session persistence is disabled for observer spawns', () => {
const session = {
memorySessionId: '5439891b-7d4b-4ee3-8662-c000f66bc199',
lastPromptNumber: 2,
disableSessionPersistence: true,
};
const shouldResume = shouldPassResumeParameter(session);
expect(shouldResume).toBe(false);
});
});
});
+24 -1
View File
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it } from 'bun:test';
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
import { tmpdir } from 'os';
import path from 'path';
import { createProcessRegistry, isPidAlive } from '../../src/supervisor/process-registry.js';
import { createProcessRegistry, isPidAlive, normalizeSpawnSdkArgs } from '../../src/supervisor/process-registry.js';
function makeTempDir(): string {
return path.join(tmpdir(), `claude-mem-supervisor-${Date.now()}-${Math.random().toString(36).slice(2)}`);
@@ -361,6 +361,29 @@ describe('supervisor ProcessRegistry', () => {
});
});
describe('normalizeSpawnSdkArgs', () => {
it('appends explicit extra args after SDK args', () => {
expect(normalizeSpawnSdkArgs(['--print', 'json'], ['--no-session-persistence'])).toEqual([
'--print',
'json',
'--no-session-persistence',
]);
});
it('strips empty placeholder flags before appending extra args', () => {
expect(normalizeSpawnSdkArgs([
'--append-system-prompt',
'',
'--resume',
'session-123',
], ['--no-session-persistence'])).toEqual([
'--resume',
'session-123',
'--no-session-persistence',
]);
});
});
describe('reapSession', () => {
it('unregisters dead processes for the given session', async () => {
const tempDir = makeTempDir();