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
24 changes: 2 additions & 22 deletions cli/src/claude/claudeRemoteLauncher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,16 @@ import { SDKAssistantMessage, SDKMessage, SDKUserMessage } from "./sdk";
import { formatClaudeMessageForInk } from "@/ui/messageFormatterInk";
import { logger } from "@/ui/logger";
import { SDKToLogConverter } from "./utils/sdkToLogConverter";
import { buildClaudeToolResultPermissions } from "./utils/toolResultPermissions";
import { PLAN_FAKE_REJECT } from "./sdk/prompts";
import { EnhancedMode } from "./loop";
import { OutgoingMessageQueue } from "./utils/OutgoingMessageQueue";
import type { ClaudePermissionMode } from "@hapi/protocol/types";
import {
RemoteLauncherBase,
type RemoteLauncherDisplayContext,
type RemoteLauncherExitReason
} from "@/modules/common/remote/RemoteLauncherBase";

interface PermissionsField {
date: number;
result: 'approved' | 'denied';
mode?: ClaudePermissionMode;
allowedTools?: string[];
}

class ClaudeRemoteLauncher extends RemoteLauncherBase {
private readonly session: Session;
private abortController: AbortController | null = null;
Expand Down Expand Up @@ -201,22 +194,9 @@ class ClaudeRemoteLauncher extends RemoteLauncherBase {
const response = responses.get(c.tool_use_id);

if (response) {
const permissions: PermissionsField = {
date: response.receivedAt || Date.now(),
result: response.approved ? 'approved' : 'denied'
};

if (response.mode) {
permissions.mode = response.mode;
}

if (response.allowTools && response.allowTools.length > 0) {
permissions.allowedTools = response.allowTools;
}

content[i] = {
...c,
permissions
permissions: buildClaudeToolResultPermissions(response)
};
}
}
Expand Down
3 changes: 3 additions & 0 deletions cli/src/claude/runClaude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
allowedTools: messageAllowedTools,
disallowedTools: messageDisallowedTools
};
currentSessionRef.current?.setModeSnapshot(enhancedMode);
// Use raw text only, ignore attachments for special commands
const commandText = specialCommand.originalMessage || message.content.text;
messageQueue.pushIsolateAndClear(commandText, enhancedMode);
Expand All @@ -275,6 +276,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
allowedTools: messageAllowedTools,
disallowedTools: messageDisallowedTools
};
currentSessionRef.current?.setModeSnapshot(enhancedMode);
// Use raw text only, ignore attachments for special commands
const commandText = specialCommand.originalMessage || message.content.text;
messageQueue.pushIsolateAndClear(commandText, enhancedMode);
Expand All @@ -293,6 +295,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
allowedTools: messageAllowedTools,
disallowedTools: messageDisallowedTools
};
currentSessionRef.current?.setModeSnapshot(enhancedMode);
messageQueue.push(formattedText, enhancedMode);
logger.debugLargeJson('User message pushed to queue:', message)
});
Expand Down
90 changes: 90 additions & 0 deletions cli/src/claude/session.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, expect, it, vi } from 'vitest';
import { MessageQueue2 } from '@/utils/MessageQueue2';
import { Session } from './session';
import type { EnhancedMode } from './loop';

describe('Claude Session.clearSessionId', () => {
it('removes the persisted Claude session token from metadata', () => {
let clearedMetadata: Record<string, unknown> | null = null;

const client = {
keepAlive: vi.fn(),
updateMetadata: vi.fn((handler: (metadata: Record<string, unknown>) => Record<string, unknown>) => {
clearedMetadata = handler({
path: '/tmp/project',
claudeSessionId: 'claude-session-123'
});
}),
rpcHandlerManager: {}
};

const session = new Session({
api: {} as never,
client: client as never,
path: '/tmp/project',
logPath: '/tmp/project/hapi.log',
sessionId: 'claude-session-123',
mcpServers: {},
messageQueue: new MessageQueue2<EnhancedMode>((mode) => JSON.stringify(mode)),
onModeChange: () => {},
startedBy: 'terminal',
startingMode: 'remote',
hookSettingsPath: '/tmp/hooks.json',
permissionMode: 'default'
});

try {
session.clearSessionId();

expect(session.sessionId).toBeNull();
expect(client.updateMetadata).toHaveBeenCalledTimes(1);
expect(clearedMetadata).toEqual({
path: '/tmp/project'
});
} finally {
session.stopKeepAlive();
}
});

it('removes stale persisted Claude session metadata even if the in-memory session id is already null', () => {
let clearedMetadata: Record<string, unknown> | null = null;

const client = {
keepAlive: vi.fn(),
updateMetadata: vi.fn((handler: (metadata: Record<string, unknown>) => Record<string, unknown>) => {
clearedMetadata = handler({
path: '/tmp/project',
claudeSessionId: 'stale-claude-session'
});
}),
rpcHandlerManager: {}
};

const session = new Session({
api: {} as never,
client: client as never,
path: '/tmp/project',
logPath: '/tmp/project/hapi.log',
sessionId: null,
mcpServers: {},
messageQueue: new MessageQueue2<EnhancedMode>((mode) => JSON.stringify(mode)),
onModeChange: () => {},
startedBy: 'terminal',
startingMode: 'remote',
hookSettingsPath: '/tmp/hooks.json',
permissionMode: 'default'
});

try {
session.clearSessionId();

expect(session.sessionId).toBeNull();
expect(client.updateMetadata).toHaveBeenCalledTimes(1);
expect(clearedMetadata).toEqual({
path: '/tmp/project'
});
} finally {
session.stopKeepAlive();
}
});
});
43 changes: 43 additions & 0 deletions cli/src/claude/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class Session extends AgentSessionBase<EnhancedMode> {
readonly startedBy: 'runner' | 'terminal';
readonly startingMode: 'local' | 'remote';
localLaunchFailure: LocalLaunchFailure | null = null;
private currentModeSnapshot: EnhancedMode;

constructor(opts: {
api: ApiClient;
Expand Down Expand Up @@ -72,20 +73,54 @@ export class Session extends AgentSessionBase<EnhancedMode> {
this.permissionMode = opts.permissionMode;
this.model = opts.model;
this.effort = opts.effort;
this.currentModeSnapshot = {
permissionMode: opts.permissionMode ?? 'default',
model: opts.model ?? undefined,
effort: opts.effort ?? undefined
};
}

setPermissionMode = (mode: PermissionMode): void => {
this.permissionMode = mode;
this.currentModeSnapshot = {
...this.currentModeSnapshot,
permissionMode: mode
};
};

setModel = (model: SessionModel): void => {
this.model = model;
this.currentModeSnapshot = {
...this.currentModeSnapshot,
model: model ?? undefined
};
};

setEffort = (effort: SessionEffort): void => {
this.effort = effort;
this.currentModeSnapshot = {
...this.currentModeSnapshot,
effort: effort ?? undefined
};
};

setModeSnapshot = (mode: EnhancedMode): void => {
this.currentModeSnapshot = {
...mode,
allowedTools: mode.allowedTools ? [...mode.allowedTools] : undefined,
disallowedTools: mode.disallowedTools ? [...mode.disallowedTools] : undefined
};
this.permissionMode = mode.permissionMode;
this.model = mode.model ?? null;
this.effort = mode.effort ?? null;
};

getModeSnapshot = (): EnhancedMode => ({
...this.currentModeSnapshot,
allowedTools: this.currentModeSnapshot.allowedTools ? [...this.currentModeSnapshot.allowedTools] : undefined,
disallowedTools: this.currentModeSnapshot.disallowedTools ? [...this.currentModeSnapshot.disallowedTools] : undefined
});

recordLocalLaunchFailure = (message: string, exitReason: LocalLaunchExitReason): void => {
this.localLaunchFailure = { message, exitReason };
};
Expand All @@ -95,6 +130,14 @@ export class Session extends AgentSessionBase<EnhancedMode> {
*/
clearSessionId = (): void => {
this.sessionId = null;
this.client.updateMetadata((metadata) => {
if (metadata.claudeSessionId === undefined) {
return metadata;
}

const { claudeSessionId: _removed, ...rest } = metadata;
return rest;
});
logger.debug('[Session] Session ID cleared');
};

Expand Down
Loading
Loading