Skip to content
Merged
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
16 changes: 14 additions & 2 deletions assistant/src/daemon/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,12 @@ export class DaemonServer {
rebindClient ? sendToClient : () => {},
workingDir,
);
// When created without a socket (HTTP path), mark the session
// so interactive prompts (e.g. host attachment reads) can fail
// fast instead of waiting for a timeout with no client to respond.
if (!socket) {
newSession.updateClient(sendToClient, true);
}
await newSession.loadFromDb();
if (rebindClient && socket) {
newSession.setSandboxOverride(this.socketSandboxOverride.get(socket));
Expand Down Expand Up @@ -591,14 +597,17 @@ export class DaemonServer {
attachmentIds?: string[],
): Promise<{ messageId: string }> {
const session = await this.getOrCreateSession(conversationId);
session.setAssistantId(assistantId);

// Reject concurrent requests upfront. The HTTP path should never use
// the message queue — it returns 409 to the caller instead.
if (session.isProcessing()) {
throw new Error('Session is already processing a message');
}

// Set assistantId AFTER the isProcessing check so a rejected request
// doesn't mutate the session state visible to an in-flight request.
session.setAssistantId(assistantId);

// Resolve attachment IDs to full attachment data for the session
const attachments = attachmentIds
? attachmentsStore.getAttachmentsByIds(assistantId, attachmentIds).map((a) => ({
Expand Down Expand Up @@ -634,12 +643,15 @@ export class DaemonServer {
attachmentIds?: string[],
): Promise<{ messageId: string }> {
const session = await this.getOrCreateSession(conversationId);
session.setAssistantId(assistantId);

if (session.isProcessing()) {
throw new Error('Session is already processing a message');
}

// Set assistantId AFTER the isProcessing check so a rejected request
// doesn't mutate the session state visible to an in-flight request.
session.setAssistantId(assistantId);

// Resolve attachment IDs to full attachment data for the session
const attachments = attachmentIds
? attachmentsStore.getAttachmentsByIds(assistantId, attachmentIds).map((a) => ({
Expand Down
12 changes: 11 additions & 1 deletion assistant/src/daemon/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export class Session {
private contextCompactedMessageCount = 0;
private currentRequestId?: string;
private assistantId: string | null = null;
private hasNoClient = false;
private messageQueue: QueuedMessage[] = [];
private pendingSurfaceActions = new Map<string, {
surfaceType: SurfaceType;
Expand Down Expand Up @@ -293,8 +294,9 @@ export class Session {
log.info({ conversationId: this.conversationId, count: this.messages.length }, 'Loaded messages from DB');
}

updateClient(sendToClient: (msg: ServerMessage) => void): void {
updateClient(sendToClient: (msg: ServerMessage) => void, hasNoClient = false): void {
this.sendToClient = sendToClient;
this.hasNoClient = hasNoClient;
this.prompter.updateSender(sendToClient);
this.secretPrompter.updateSender(sendToClient);
this.traceEmitter.updateSender(sendToClient);
Expand Down Expand Up @@ -435,6 +437,14 @@ export class Session {
return false;
}

// HTTP-created sessions use a no-op sendToClient — prompting would
// block for the full permission timeout before auto-denying. Fail
// fast instead.
if (this.hasNoClient) {
log.info({ filePath }, 'Denying host attachment read: no interactive client connected');
return false;
}

const response = await this.prompter.prompt(
toolName,
input,
Expand Down
9 changes: 7 additions & 2 deletions assistant/src/runtime/run-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,14 @@ export class RunOrchestrator {
const messageId = session.persistUserMessage(content, attachments, requestId);
const run = runsStore.createRun(assistantId, conversationId, messageId);

// Set the assistant ID so attachments are scoped correctly.
session.setAssistantId(assistantId);

// Hook into session to intercept confirmation_request events.
// When the prompter sends a confirmation_request, we record it in the
// run store so the web UI can poll and submit a decision.
// Pass hasNoClient=true so interactive-only prompts (e.g. host
// attachment reads) fail fast instead of waiting for a timeout.
let lastError: string | null = null;
session.updateClient((msg: ServerMessage) => {
if (msg.type === 'confirmation_request') {
Expand All @@ -101,14 +106,14 @@ export class RunOrchestrator {
session,
});
}
});
}, true);
Comment thread
siddseethepalli marked this conversation as resolved.

// Fire-and-forget the agent loop
const cleanup = () => {
this.pending.delete(run.id);
// Reset the session's client callback to a no-op so the stale
// closure doesn't intercept events from future runs on the same session.
session.updateClient(() => {});
session.updateClient(() => {}, true);
};

session.runAgentLoop(content, messageId, (msg: ServerMessage) => {
Expand Down
Loading