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
1 change: 1 addition & 0 deletions .eslintcache

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions cloud-agent-next/.eslintcache

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions cloud-agent-next/drizzle/0001_add_entity_id.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE `events` ADD COLUMN `entity_id` text;
--> statement-breakpoint
CREATE UNIQUE INDEX `events_entity_id_unique` ON `events` (`entity_id`);
7 changes: 7 additions & 0 deletions cloud-agent-next/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
"when": 1772242306388,
"tag": "0000_high_mimic",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1772242306389,
"tag": "0001_add_entity_id",
"breakpoints": true
}
]
}
2 changes: 2 additions & 0 deletions cloud-agent-next/drizzle/migrations.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import journal from './meta/_journal.json';
import m0000 from './0000_high_mimic.sql';
import m0001 from './0001_add_entity_id.sql';

export default {
journal,
migrations: {
m0000,
m0001,
},
};
1 change: 1 addition & 0 deletions cloud-agent-next/src/db/sqlite-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const events = sqliteTable(
stream_event_type: text('stream_event_type').notNull(),
payload: text('payload').notNull(),
timestamp: integer('timestamp').notNull(),
entity_id: text('entity_id').unique(),
},
table => [
index('idx_events_execution').on(table.execution_id),
Expand Down
1 change: 1 addition & 0 deletions cloud-agent-next/src/kilo/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export class KiloClient {
modelID: options.model.modelID,
}
: undefined,
variant: options?.variant,
agent: options?.agent,
noReply: options?.noReply,
system: options?.system,
Expand Down
2 changes: 2 additions & 0 deletions cloud-agent-next/src/kilo/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export interface CreateSessionOptions {
export interface PromptOptions {
messageId?: string;
model?: { providerID?: string; modelID: string };
/** Thinking/reasoning effort variant (e.g., "high", "max", "low") */
variant?: string;
agent?: string;
noReply?: boolean;
/** Custom system prompt override */
Expand Down
115 changes: 104 additions & 11 deletions cloud-agent-next/src/persistence/CloudAgentSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import {
type IngestDOContext,
} from '../websocket/ingest.js';
import type { StoredEvent } from '../websocket/types.js';
import type { WrapperCommand, PreparingStep } from '../shared/protocol.js';
import type { WrapperCommand, PreparingStep, CloudStatusData } from '../shared/protocol.js';
import { STALE_THRESHOLD_MS, SANDBOX_SLEEP_AFTER_SECONDS } from '../core/lease.js';
import { ExecutionOrchestrator, type OrchestratorDeps } from '../execution/orchestrator.js';
import type {
Expand Down Expand Up @@ -126,7 +126,6 @@ export class CloudAgentSession extends DurableObject {
private ingestHandlerSessionId?: SessionId;
private sessionId?: SessionId;
private orchestrator?: ExecutionOrchestrator;

private isTerminalStatus(
status: ExecutionStatus
): status is 'completed' | 'failed' | 'interrupted' {
Expand Down Expand Up @@ -251,7 +250,9 @@ export class CloudAgentSession extends DurableObject {
private async getStreamHandler(expected?: SessionId): Promise<StreamHandler> {
const sessionId = await this.requireSessionId(expected);
if (!this.streamHandler || this.streamHandlerSessionId !== sessionId) {
this.streamHandler = createStreamHandler(this.ctx, this.eventQueries, sessionId);
this.streamHandler = createStreamHandler(this.ctx, this.eventQueries, sessionId, {
deriveCloudStatus: () => this.deriveCloudStatus(),
});
this.streamHandlerSessionId = sessionId;
}
return this.streamHandler;
Expand Down Expand Up @@ -366,7 +367,14 @@ export class CloudAgentSession extends DurableObject {
}

const streamHandler = await this.getStreamHandler(sessionIdParam ?? undefined);
return streamHandler.handleStreamRequest(request);
const response = await streamHandler.handleStreamRequest(request);

// Request fresh kilo state from wrapper if connected.
// The wrapper will respond with regular kilocode events (session.status,
// question.asked, permission.asked) that are broadcast via the normal pipeline.
this.requestKiloSnapshot();

return response;
}

// Route ingest WebSocket (internal only - from queue consumer)
Expand Down Expand Up @@ -523,6 +531,28 @@ export class CloudAgentSession extends DurableObject {
});
}

/**
* Derive current cloud infrastructure status from execution state.
* Used to populate the `connected` event on WebSocket upgrade.
*/
private async deriveCloudStatus(): Promise<CloudStatusData['cloudStatus'] | null> {
const activeExecId = await this.executionQueries.getActiveExecutionId();
if (!activeExecId) {
const metadata = await this.ctx.storage.get<CloudAgentSessionState>('metadata');
return metadata?.preparedAt ? { type: 'ready' } : null;
}

const exec = await this.executionQueries.get(activeExecId);
if (!exec) return null;

if (exec.status === 'pending') {
return { type: 'preparing' };
}

// Running executions mean the agent has control — infrastructure is ready
return { type: 'ready' };
}

/**
* Get count of connected stream clients.
*
Expand Down Expand Up @@ -718,6 +748,19 @@ export class CloudAgentSession extends DurableObject {
}
}

/**
* Request fresh kilo state from the wrapper.
* The wrapper will respond with regular kilocode events (session.status,
* question.asked, permission.asked) that flow through the normal ingest pipeline.
* Best-effort: silently does nothing if no wrapper is connected.
*/
private requestKiloSnapshot(): void {
void this.executionQueries.getActiveExecutionId().then(activeExecId => {
if (!activeExecId) return;
this.sendToWrapper(activeExecId, { type: 'request_snapshot' });
});
}

/**
* Interrupt the currently active execution by sending a kill command to the wrapper.
* Returns success/failure status.
Expand Down Expand Up @@ -903,12 +946,28 @@ export class CloudAgentSession extends DurableObject {
const env = this.env as unknown as WorkerEnv;

const emitProgress = (step: PreparingStep, message: string) => {
const now = Date.now();
// Backward-compatible preparing event
this.broadcastVolatileEvent({
executionId: prepExecutionId,
sessionId: input.sessionId,
streamEventType: 'preparing',
payload: JSON.stringify({ step, message }),
timestamp: Date.now(),
timestamp: now,
});
// cloud.status event derived from preparation step
const cloudStatus =
step === 'ready'
? { type: 'ready' as const }
: step === 'failed'
? { type: 'error' as const, message }
: { type: 'preparing' as const, step, message };
this.broadcastVolatileEvent({
executionId: prepExecutionId,
sessionId: input.sessionId,
streamEventType: 'cloud.status',
payload: JSON.stringify({ cloudStatus }),
timestamp: now,
});
};

Expand Down Expand Up @@ -1193,15 +1252,21 @@ export class CloudAgentSession extends DurableObject {

if (this.sessionId) {
const prepId: EventSourceId = `prep_${this.sessionId}`;
const failNow = Date.now();
const failMessage = 'Internal error: invalid preparation data';
this.broadcastVolatileEvent({
executionId: prepId,
sessionId: this.sessionId,
streamEventType: 'preparing',
payload: JSON.stringify({
step: 'failed',
message: 'Internal error: invalid preparation data',
}),
timestamp: Date.now(),
payload: JSON.stringify({ step: 'failed', message: failMessage }),
timestamp: failNow,
});
this.broadcastVolatileEvent({
executionId: prepId,
sessionId: this.sessionId,
streamEventType: 'cloud.status',
payload: JSON.stringify({ cloudStatus: { type: 'error', message: failMessage } }),
timestamp: failNow,
});
}
}
Expand Down Expand Up @@ -2528,17 +2593,37 @@ export class CloudAgentSession extends DurableObject {
const orchestrator = this.getOrCreateOrchestrator();

const emitProgress = (step: string, message: string) => {
const now = Date.now();
this.broadcastVolatileEvent({
executionId,
sessionId,
streamEventType: 'preparing',
payload: JSON.stringify({ step, message }),
timestamp: Date.now(),
timestamp: now,
});
// cloud.status mirrors the preparation step
this.broadcastVolatileEvent({
executionId,
sessionId,
streamEventType: 'cloud.status',
payload: JSON.stringify({
cloudStatus: { type: 'preparing' as const, step, message },
}),
timestamp: now,
});
};

const result = await orchestrator.execute(plan, { onProgress: emitProgress });

// Emit cloud.status = ready after successful execution start
this.broadcastVolatileEvent({
executionId,
sessionId,
streamEventType: 'cloud.status',
payload: JSON.stringify({ cloudStatus: { type: 'ready' } }),
timestamp: Date.now(),
});

logger
.withFields({ sessionId, executionId, kiloSessionId: result.kiloSessionId })
.info('Execution started successfully');
Expand All @@ -2547,6 +2632,14 @@ export class CloudAgentSession extends DurableObject {
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);

this.broadcastVolatileEvent({
executionId,
sessionId,
streamEventType: 'cloud.status',
payload: JSON.stringify({ cloudStatus: { type: 'error', message: errorMessage } }),
timestamp: Date.now(),
});

await this.failExecution({
executionId,
status: 'failed',
Expand Down
1 change: 1 addition & 0 deletions cloud-agent-next/src/router/handlers/session-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ export function createSessionManagementHandlers() {
// mode is validated by zod (AgentModeSchema) at storage time
mode: metadata.mode as AgentMode | undefined,
model: metadata.model,
variant: metadata.variant,
autoCommit: metadata.autoCommit,
upstreamBranch: metadata.upstreamBranch,

Expand Down
38 changes: 38 additions & 0 deletions cloud-agent-next/src/router/handlers/session-questions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,43 @@ export function createSessionQuestionHandlers() {
}
});
}),

answerPermission: protectedProcedure
.input(
z.object({
sessionId: sessionIdSchema,
permissionId: z.string().min(1),
response: z.enum(['once', 'always', 'reject']),
})
)
.output(z.object({ success: z.boolean() }))
.mutation(async ({ input, ctx }) => {
return withLogTags({ source: 'answerPermission' }, async () => {
const sessionId = input.sessionId as SessionId;
const { userId, env } = ctx;

logger.setTags({ userId, sessionId });
logger.info('Answering permission', { permissionId: input.permissionId });

try {
const wrapperClient = await resolveWrapperClient({
sessionId,
userId,
env,
authToken: ctx.authToken,
});
const result = await wrapperClient.answerPermission(input.permissionId, input.response);
return { success: result.success };
} catch (error) {
if (error instanceof TRPCError) throw error;
const errorMsg = error instanceof Error ? error.message : String(error);
logger.withFields({ error: errorMsg }).error('Failed to answer permission');
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: `Failed to answer permission: ${errorMsg}`,
});
}
});
}),
};
}
1 change: 1 addition & 0 deletions cloud-agent-next/src/router/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ export const GetSessionOutput = z.object({
prompt: z.string().optional().describe('Task prompt'),
mode: AgentModeSchema.optional().describe('Execution mode'),
model: z.string().optional().describe('AI model'),
variant: z.string().optional().describe('Thinking effort variant'),
autoCommit: z.boolean().optional().describe('Auto-commit setting'),
upstreamBranch: z.string().optional().describe('Upstream branch name'),

Expand Down
18 changes: 9 additions & 9 deletions cloud-agent-next/src/session-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ describe('SessionService', () => {
KILOCODE_ORGANIZATION_ID: 'org',
KILO_PLATFORM: 'cloud-agent',
KILOCODE_FEATURE: 'cloud-agent',
OPENCODE_CONFIG_CONTENT: `{"permission":{"external_directory":{"*":"deny","/tmp/${sessionId}/**":"allow","/workspace/org/user/sessions/${sessionId}/**":"allow"}},"provider":{"kilo":{"options":{"apiKey":"token","kilocodeToken":"token","kilocodeOrganizationId":"org"}}},"model":"kilo/test-model"}`,
KILO_CONFIG_CONTENT: `{"permission":{"external_directory":{"*":"deny","/tmp/${sessionId}/**":"allow","/workspace/org/user/sessions/${sessionId}/**":"allow"}},"provider":{"kilo":{"options":{"apiKey":"token","kilocodeToken":"token","kilocodeOrganizationId":"org"}}},"model":"kilo/test-model"}`,
OPENCODE_CONFIG_CONTENT: `{"permission":{"external_directory":{"*":"deny","/tmp/${sessionId}/**":"allow","/workspace/org/user/sessions/${sessionId}/**":"allow"},"question":"deny"},"provider":{"kilo":{"options":{"apiKey":"token","kilocodeToken":"token","kilocodeOrganizationId":"org"}}},"model":"kilo/test-model"}`,
KILO_CONFIG_CONTENT: `{"permission":{"external_directory":{"*":"deny","/tmp/${sessionId}/**":"allow","/workspace/org/user/sessions/${sessionId}/**":"allow"},"question":"deny"},"provider":{"kilo":{"options":{"apiKey":"token","kilocodeToken":"token","kilocodeOrganizationId":"org"}}},"model":"kilo/test-model"}`,
},
cwd: `/workspace/org/user/sessions/${sessionId}`,
});
Expand Down Expand Up @@ -307,8 +307,8 @@ describe('SessionService', () => {
KILOCODE_ORGANIZATION_ID: 'org',
KILO_PLATFORM: 'cloud-agent',
KILOCODE_FEATURE: 'cloud-agent',
OPENCODE_CONFIG_CONTENT: `{"permission":{"external_directory":{"*":"deny","/tmp/${sessionId}/**":"allow","/workspace/org/user/sessions/${sessionId}/**":"allow"}},"provider":{"kilo":{"options":{"apiKey":"token","kilocodeToken":"token","kilocodeOrganizationId":"org"}}},"model":"kilo/test-model"}`,
KILO_CONFIG_CONTENT: `{"permission":{"external_directory":{"*":"deny","/tmp/${sessionId}/**":"allow","/workspace/org/user/sessions/${sessionId}/**":"allow"}},"provider":{"kilo":{"options":{"apiKey":"token","kilocodeToken":"token","kilocodeOrganizationId":"org"}}},"model":"kilo/test-model"}`,
OPENCODE_CONFIG_CONTENT: `{"permission":{"external_directory":{"*":"deny","/tmp/${sessionId}/**":"allow","/workspace/org/user/sessions/${sessionId}/**":"allow"},"question":"deny"},"provider":{"kilo":{"options":{"apiKey":"token","kilocodeToken":"token","kilocodeOrganizationId":"org"}}},"model":"kilo/test-model"}`,
KILO_CONFIG_CONTENT: `{"permission":{"external_directory":{"*":"deny","/tmp/${sessionId}/**":"allow","/workspace/org/user/sessions/${sessionId}/**":"allow"},"question":"deny"},"provider":{"kilo":{"options":{"apiKey":"token","kilocodeToken":"token","kilocodeOrganizationId":"org"}}},"model":"kilo/test-model"}`,
},
cwd: `/workspace/org/user/sessions/${sessionId}`,
});
Expand Down Expand Up @@ -1165,8 +1165,8 @@ describe('SessionService', () => {
KILOCODE_ORGANIZATION_ID: 'org',
KILO_PLATFORM: 'cloud-agent',
KILOCODE_FEATURE: 'cloud-agent',
OPENCODE_CONFIG_CONTENT: `{"permission":{"external_directory":{"*":"deny","/tmp/${sessionId}/**":"allow","/workspace/org/user/sessions/${sessionId}/**":"allow"}},"provider":{"kilo":{"options":{"apiKey":"token","kilocodeToken":"token","kilocodeOrganizationId":"org"}}},"model":"kilo/test-model"}`,
KILO_CONFIG_CONTENT: `{"permission":{"external_directory":{"*":"deny","/tmp/${sessionId}/**":"allow","/workspace/org/user/sessions/${sessionId}/**":"allow"}},"provider":{"kilo":{"options":{"apiKey":"token","kilocodeToken":"token","kilocodeOrganizationId":"org"}}},"model":"kilo/test-model"}`,
OPENCODE_CONFIG_CONTENT: `{"permission":{"external_directory":{"*":"deny","/tmp/${sessionId}/**":"allow","/workspace/org/user/sessions/${sessionId}/**":"allow"},"question":"deny"},"provider":{"kilo":{"options":{"apiKey":"token","kilocodeToken":"token","kilocodeOrganizationId":"org"}}},"model":"kilo/test-model"}`,
KILO_CONFIG_CONTENT: `{"permission":{"external_directory":{"*":"deny","/tmp/${sessionId}/**":"allow","/workspace/org/user/sessions/${sessionId}/**":"allow"},"question":"deny"},"provider":{"kilo":{"options":{"apiKey":"token","kilocodeToken":"token","kilocodeOrganizationId":"org"}}},"model":"kilo/test-model"}`,
API_KEY: 'test-key-123',
DATABASE_URL: 'postgres://localhost:5432/test',
NODE_ENV: 'development',
Expand Down Expand Up @@ -1270,8 +1270,8 @@ describe('SessionService', () => {
KILOCODE_ORGANIZATION_ID: 'org',
KILO_PLATFORM: 'cloud-agent',
KILOCODE_FEATURE: 'cloud-agent',
OPENCODE_CONFIG_CONTENT: `{"permission":{"external_directory":{"*":"deny","/tmp/${sessionId}/**":"allow","/workspace/org/user/sessions/${sessionId}/**":"allow"}},"provider":{"kilo":{"options":{"apiKey":"token","kilocodeToken":"token","kilocodeOrganizationId":"org"}}},"model":"kilo/test-model"}`,
KILO_CONFIG_CONTENT: `{"permission":{"external_directory":{"*":"deny","/tmp/${sessionId}/**":"allow","/workspace/org/user/sessions/${sessionId}/**":"allow"}},"provider":{"kilo":{"options":{"apiKey":"token","kilocodeToken":"token","kilocodeOrganizationId":"org"}}},"model":"kilo/test-model"}`,
OPENCODE_CONFIG_CONTENT: `{"permission":{"external_directory":{"*":"deny","/tmp/${sessionId}/**":"allow","/workspace/org/user/sessions/${sessionId}/**":"allow"},"question":"deny"},"provider":{"kilo":{"options":{"apiKey":"token","kilocodeToken":"token","kilocodeOrganizationId":"org"}}},"model":"kilo/test-model"}`,
KILO_CONFIG_CONTENT: `{"permission":{"external_directory":{"*":"deny","/tmp/${sessionId}/**":"allow","/workspace/org/user/sessions/${sessionId}/**":"allow"},"question":"deny"},"provider":{"kilo":{"options":{"apiKey":"token","kilocodeToken":"token","kilocodeOrganizationId":"org"}}},"model":"kilo/test-model"}`,
},
cwd: `/workspace/org/user/sessions/${sessionId}`,
});
Expand Down Expand Up @@ -1303,7 +1303,7 @@ describe('SessionService', () => {
};
};

it.each([undefined])(
it.each(['cloud-agent-web'])(
'should NOT include question:deny for interactive platform %s',
async createdOnPlatform => {
const { sandbox, sandboxCreateSession } = setupForPlatformTest();
Expand Down
2 changes: 1 addition & 1 deletion cloud-agent-next/src/session-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ export class SessionService {
if (env.KILO_OPENROUTER_BASE) {
providerOptions.baseURL = env.KILO_OPENROUTER_BASE;
}
const isInteractive = !createdOnPlatform;
const isInteractive = createdOnPlatform == 'cloud-agent-web';
const commandGuardPolicy = getCommandGuardPolicy(createdOnPlatform);

const permission: Record<string, unknown> = {
Expand Down
Loading
Loading