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
3 changes: 2 additions & 1 deletion cloud-agent-next/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ RUN npm install -g pnpm @kilocode/cli@${KILOCODE_CLI_VERSION}
COPY wrapper /tmp/wrapper-build/wrapper
COPY src/shared /tmp/wrapper-build/src/shared

# Build the wrapper bundle and restore-session script
# Install wrapper dependencies (pnpm symlinks don't survive COPY) and build
RUN cd /tmp/wrapper-build/wrapper && \
bun install --production && \
bun build src/main.ts --outfile=/usr/local/bin/kilocode-wrapper.js --target=bun --minify && \
bun build src/restore-session.ts --outfile=/usr/local/bin/kilo-restore-session.js --target=bun --minify && \
rm -rf /tmp/wrapper-build
Expand Down
2 changes: 2 additions & 0 deletions cloud-agent-next/Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ RUN npm install -g pnpm @kilocode/cli@${KILOCODE_CLI_VERSION}
COPY wrapper /tmp/wrapper-build/wrapper
COPY src/shared /tmp/wrapper-build/src/shared

# Install wrapper dependencies (pnpm symlinks don't survive COPY) and build
RUN cd /tmp/wrapper-build/wrapper && \
bun install --production && \
bun build src/main.ts --outfile=/usr/local/bin/kilocode-wrapper.js --target=bun --minify && \
bun build src/restore-session.ts --outfile=/usr/local/bin/kilo-restore-session.js --target=bun --minify && \
rm -rf /tmp/wrapper-build
Expand Down
85 changes: 25 additions & 60 deletions cloud-agent-next/src/execution/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { ExecutionError } from './errors.js';
import { SessionService, type PreparedSession } from '../session-service.js';
import { logger } from '../logger.js';
import { updateGitRemoteToken } from '../workspace.js';
import { ensureKiloServer } from '../kilo/server-manager.js';
import { WrapperClient } from '../kilo/wrapper-client.js';
import { withDORetry } from '../utils/do-retry.js';
import { normalizeAgentMode } from '../schema.js';
Expand Down Expand Up @@ -102,23 +101,26 @@ export class ExecutionOrchestrator {
}
}

// 4. Ensure kilo server is running (may throw KILO_SERVER_FAILED)
let kiloServerPort: number;
// 4. Ensure wrapper is running (starts kilo server in-process)
let wrapperClient: WrapperClient;
let kiloSessionId: string;
try {
kiloServerPort = await ensureKiloServer(
sandbox,
prepared.session,
sessionId,
prepared.context.workspacePath
);
const result = await WrapperClient.ensureWrapper(sandbox, prepared.session, {
agentSessionId: sessionId,
userId,
workspacePath: prepared.context.workspacePath,
sessionId: wrapper.kiloSessionId,
});
wrapperClient = result.client;
kiloSessionId = result.sessionId;
} catch (error) {
throw ExecutionError.kiloServerFailed(
`Failed to start kilo server: ${error instanceof Error ? error.message : String(error)}`,
throw ExecutionError.wrapperStartFailed(
`Failed to start wrapper: ${error instanceof Error ? error.message : String(error)}`,
error
);
}

// Record kilo server activity for idle timeout tracking
// 5. Record activity for idle timeout tracking
try {
await withDORetry(
() => this.deps.getSessionStub(userId, sessionId),
Expand All @@ -130,59 +132,19 @@ export class ExecutionOrchestrator {
logger.warn('Failed to record kilo server activity');
}

// 5. Ensure wrapper is running (may throw WRAPPER_START_FAILED)
let wrapperClient: WrapperClient;
try {
wrapperClient = await WrapperClient.ensureWrapper(
sandbox,
prepared.session,
sessionId,
kiloServerPort,
prepared.context.workspacePath,
{
autoCommit: wrapper.autoCommit,
condenseOnComplete: wrapper.condenseOnComplete,
upstreamBranch: prepared.context.upstreamBranch,
model: wrapper.model?.modelID,
}
);
} catch (error) {
throw ExecutionError.wrapperStartFailed(
`Failed to start wrapper: ${error instanceof Error ? error.message : String(error)}`,
error
);
}

// 6. Start job (create/resume kilo session)
// 6. Send prompt with execution binding (async - returns messageId immediately)
const ingestUrl = this.deps.getIngestUrl(sessionId, userId);
// Ingest token must match executionId for /ingest auth validation
const ingestToken = executionId;

// Get kilocode token from plan
const kilocodeToken = this.getKilocodeToken(plan);

let kiloSessionId: string;
try {
const result = await wrapperClient.startJob({
executionId,
ingestUrl,
ingestToken,
sessionId,
userId,
kilocodeToken,
kiloSessionId: wrapper.kiloSessionId,
kiloSessionTitle: wrapper.kiloSessionTitle,
});
kiloSessionId = result.kiloSessionId;
logger.withFields({ kiloSessionId }).info('Wrapper job started');
} catch (error) {
throw ExecutionError.wrapperStartFailed(
`Failed to start wrapper job: ${error instanceof Error ? error.message : String(error)}`,
error
);
}
const execution = {
executionId,
ingestUrl,
ingestToken,
workerAuthToken: kilocodeToken,
upstreamBranch: prepared.context.upstreamBranch,
};

// 7. Send prompt (async - returns messageId immediately)
// Normalize mode to internal mode (e.g., 'architect' -> 'plan', 'orchestrator' -> 'code')
const normalizedMode = normalizeAgentMode(mode);
try {
Expand All @@ -191,6 +153,9 @@ export class ExecutionOrchestrator {
model: wrapper.model,
variant: wrapper.variant,
agent: normalizedMode,
autoCommit: wrapper.autoCommit,
condenseOnComplete: wrapper.condenseOnComplete,
execution,
});
logger.withFields({ inflightId: result.messageId }).info('Prompt sent to wrapper');
} catch (error) {
Expand Down
4 changes: 2 additions & 2 deletions cloud-agent-next/src/kilo/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
KiloTimeoutError,
} from './errors.js';
import type { ExecutionSession } from '../types.js';
import type { Session, SessionCommandResponse } from './types.js';
import type { Session } from './types.js';

type KiloClientPrivates = {
parseResponse: (stdout: string) => { responseBody: string; httpStatus: number };
Expand Down Expand Up @@ -219,7 +219,7 @@ describe('KiloClient', () => {
const callSpy = vi
.spyOn(client, 'call')
.mockResolvedValueOnce(buildSession('ses_1'))
.mockResolvedValueOnce({} as unknown as SessionCommandResponse);
.mockResolvedValueOnce({} as unknown);

await client.resumeSession('ses_1');
await client.command('help', '', { model: 'anthropic/claude-sonnet-4-20250514' });
Expand Down
1 change: 0 additions & 1 deletion cloud-agent-next/src/kilo/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './server-manager.js';
export * from './client.js';
export * from './errors.js';
export * from './types.js';
Expand Down
37 changes: 37 additions & 0 deletions cloud-agent-next/src/kilo/ports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest';
import { PORT_RANGE_MAX, PORT_RANGE_MIN, randomPort } from './ports.js';

describe('port constants', () => {
it('PORT_RANGE_MIN is 10000', () => {
expect(PORT_RANGE_MIN).toBe(10000);
});

it('PORT_RANGE_MAX is 60000', () => {
expect(PORT_RANGE_MAX).toBe(60000);
});
});

describe('randomPort', () => {
it('returns a number within [PORT_RANGE_MIN, PORT_RANGE_MAX)', () => {
for (let i = 0; i < 100; i++) {
const port = randomPort();
expect(port).toBeGreaterThanOrEqual(PORT_RANGE_MIN);
expect(port).toBeLessThan(PORT_RANGE_MAX);
}
});

it('returns integers', () => {
for (let i = 0; i < 100; i++) {
const port = randomPort();
expect(Number.isInteger(port)).toBe(true);
}
});

it('produces varying results across multiple calls', () => {
const ports = new Set<number>();
for (let i = 0; i < 100; i++) {
ports.add(randomPort());
}
expect(ports.size).toBeGreaterThanOrEqual(2);
});
});
8 changes: 8 additions & 0 deletions cloud-agent-next/src/kilo/ports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Range partially overlaps the Linux ephemeral range (32768–60999), but in an
// isolated container a 50k-port range makes collisions statistically negligible.
export const PORT_RANGE_MIN = 10000;
export const PORT_RANGE_MAX = 60000;

export function randomPort(): number {
return PORT_RANGE_MIN + Math.floor(Math.random() * (PORT_RANGE_MAX - PORT_RANGE_MIN));
}
Loading
Loading