@companyhelm/cli 0.0.2 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -62
- package/RUNTIME_IMAGE_VERSION +1 -1
- package/dist/cli.js +29 -1
- package/dist/commands/register-commands.js +2 -0
- package/dist/commands/root.js +280 -20
- package/dist/commands/startup.js +138 -55
- package/dist/commands/status.js +32 -0
- package/dist/service/app_server.js +23 -9
- package/dist/service/docker/app_server_container.js +3 -1
- package/dist/service/thread_lifecycle.js +4 -1
- package/dist/state/daemon_state.js +83 -0
- package/dist/state/schema.js +9 -1
- package/dist/templates/app_server_bootstrap.sh.j2 +46 -0
- package/dist/templates/runtime_agents.md.j2 +50 -0
- package/dist/templates/runtime_bashrc.j2 +19 -0
- package/dist/utils/daemon.js +15 -0
- package/dist/utils/process.js +22 -0
- package/drizzle/0011_actual_lucky.sql +7 -0
- package/drizzle/meta/_journal.json +8 -1
- package/package.json +6 -3
- package/dist/commands/agent/index.js +0 -10
- package/dist/commands/agent/list.js +0 -31
- package/dist/commands/agent/register-agent-commands.js +0 -10
- package/dist/commands/index.js +0 -15
- package/dist/commands/sdk/index.js +0 -12
- package/dist/commands/thread/index.js +0 -12
- package/dist/config/local.js +0 -1
- package/dist/config/schema.js +0 -7
- package/dist/model.js +0 -22
- package/dist/schema.js +0 -47
- package/dist/service/docker/docker_provider.js +0 -1
- package/dist/service/docker/runtime_container.js +0 -1
- package/dist/service/docker/runtime_image.js +0 -40
- package/dist/startup.js +0 -166
- package/dist/state/service/app_server.js +0 -392
- package/dist/state/service/buffered_client_message_sender.js +0 -73
- package/dist/state/service/companyhelm_api_client.js +0 -316
- package/dist/state/service/docker/app_server_container.js +0 -165
- package/dist/state/service/docker/dind.js +0 -114
- package/dist/state/service/docker/runtime_app_server_exec.js +0 -95
- package/dist/state/service/host.js +0 -15
- package/dist/state/service/runtime_shell.js +0 -23
- package/dist/state/service/sdk/refresh_models.js +0 -83
- package/dist/state/service/thread_lifecycle.js +0 -327
- package/dist/state/service/thread_runtime.js +0 -11
- package/dist/state/service/thread_turn_state.js +0 -45
- package/dist/state/service/workspace_agents.js +0 -115
package/README.md
CHANGED
|
@@ -1,95 +1,57 @@
|
|
|
1
|
-
# CompanyHelm
|
|
1
|
+
# CompanyHelm CLI
|
|
2
2
|
|
|
3
|
-
Run coding agents in
|
|
4
|
-
|
|
5
|
-
Features:
|
|
6
|
-
|
|
7
|
-
- Secure agent containers: no risk of agents going rogue on your main file system
|
|
8
|
-
- Yolo mode: no more permission prompts
|
|
9
|
-
- DinD (Docker-in-Docker): allows agents to spin up your services (backend, frontend, etc.) and test end-to-end
|
|
10
|
-
- Multi-agent support: each agent gets its own environment and can operate autonomously
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## Why CompanyHelm Runner?
|
|
15
|
-
|
|
16
|
-
Modern coding agents are powerful, but they often run directly on your machine.
|
|
17
|
-
|
|
18
|
-
CompanyHelm Runner adds:
|
|
19
|
-
|
|
20
|
-
- Isolation
|
|
21
|
-
- Docker-in-Docker (DIND)
|
|
22
|
-
- Clean workspace lifecycle
|
|
23
|
-
|
|
24
|
-
Think:
|
|
25
|
-
|
|
26
|
-
Codex or Claude Code, but inside a sandbox you control.
|
|
27
|
-
|
|
28
|
-
---
|
|
3
|
+
Run coding agents in isolated Docker sandboxes on your machine.
|
|
29
4
|
|
|
30
5
|
## Install
|
|
31
6
|
|
|
32
7
|
```bash
|
|
33
|
-
npm install -g companyhelm
|
|
8
|
+
npm install -g @companyhelm/cli
|
|
34
9
|
```
|
|
35
10
|
|
|
36
|
-
Or run
|
|
11
|
+
Or run it without installing globally:
|
|
37
12
|
|
|
38
13
|
```bash
|
|
39
|
-
npx companyhelm
|
|
14
|
+
npx @companyhelm/cli --help
|
|
40
15
|
```
|
|
41
16
|
|
|
42
|
-
|
|
17
|
+
Package: [@companyhelm/cli](https://www.npmjs.com/package/@companyhelm/cli)
|
|
43
18
|
|
|
44
|
-
##
|
|
19
|
+
## Basic Usage
|
|
45
20
|
|
|
46
|
-
|
|
21
|
+
Start the CLI in the foreground:
|
|
47
22
|
|
|
48
23
|
```bash
|
|
49
|
-
companyhelm
|
|
24
|
+
companyhelm-runner
|
|
50
25
|
```
|
|
51
26
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Generate SQL migrations from the schema:
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
npm run db:generate
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Apply migrations directly with Drizzle Kit (defaults to `~/.local/share/companyhelm/state.db`):
|
|
27
|
+
Start it as a daemon:
|
|
61
28
|
|
|
62
29
|
```bash
|
|
63
|
-
|
|
30
|
+
companyhelm-runner --daemon
|
|
64
31
|
```
|
|
65
32
|
|
|
66
|
-
|
|
33
|
+
Check whether the daemon is running:
|
|
67
34
|
|
|
68
35
|
```bash
|
|
69
|
-
|
|
36
|
+
companyhelm-runner status
|
|
70
37
|
```
|
|
71
38
|
|
|
72
|
-
|
|
39
|
+
The `status` command prints:
|
|
73
40
|
|
|
74
|
-
|
|
41
|
+
- whether the daemon is running
|
|
42
|
+
- the recorded daemon PID
|
|
43
|
+
- the daemon log directory and log file path
|
|
75
44
|
|
|
76
|
-
|
|
77
|
-
- Context7 stdio MCP (`resolve-library-id`, `query-docs`)
|
|
45
|
+
## Why Use It
|
|
78
46
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
Run:
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
scripts/runtime/e2e-thread-mcp --company-id <company-id>
|
|
88
|
-
```
|
|
47
|
+
- Runs agents in isolated containers instead of directly on your machine
|
|
48
|
+
- Supports Docker-in-Docker for end-to-end workflows
|
|
49
|
+
- Keeps runner state in a local SQLite database
|
|
50
|
+
- Supports long-running daemon mode for background operation
|
|
89
51
|
|
|
90
|
-
|
|
52
|
+
## For Developers
|
|
91
53
|
|
|
92
|
-
|
|
54
|
+
Development and maintenance notes live in [DEVELOPING.md](./DEVELOPING.md).
|
|
93
55
|
|
|
94
56
|
## License
|
|
95
57
|
|
package/RUNTIME_IMAGE_VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
1
|
+
0.0.8
|
package/dist/cli.js
CHANGED
|
@@ -21,4 +21,32 @@ program
|
|
|
21
21
|
.description("Run coding agents in fully isolated Docker sandboxes, locally.")
|
|
22
22
|
.version(getVersion());
|
|
23
23
|
(0, register_commands_js_1.registerCommands)(program);
|
|
24
|
-
|
|
24
|
+
function formatCliError(error) {
|
|
25
|
+
if (error instanceof commander_1.CommanderError) {
|
|
26
|
+
return {
|
|
27
|
+
message: error.message,
|
|
28
|
+
exitCode: error.exitCode,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (error instanceof Error) {
|
|
32
|
+
return {
|
|
33
|
+
message: error.message,
|
|
34
|
+
exitCode: 1,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
message: String(error),
|
|
39
|
+
exitCode: 1,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async function main() {
|
|
43
|
+
try {
|
|
44
|
+
await program.parseAsync(process.argv);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const { message, exitCode } = formatCliError(error);
|
|
48
|
+
process.stderr.write(`${message}\n`);
|
|
49
|
+
process.exitCode = exitCode;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
void main();
|
|
@@ -4,9 +4,11 @@ exports.registerCommands = registerCommands;
|
|
|
4
4
|
const root_js_1 = require("./root.js");
|
|
5
5
|
const shell_js_1 = require("./shell.js");
|
|
6
6
|
const register_sdk_commands_js_1 = require("./sdk/register-sdk-commands.js");
|
|
7
|
+
const status_js_1 = require("./status.js");
|
|
7
8
|
const register_thread_commands_js_1 = require("./thread/register-thread-commands.js");
|
|
8
9
|
function registerCommands(program) {
|
|
9
10
|
(0, root_js_1.registerRootCommand)(program);
|
|
11
|
+
(0, status_js_1.registerStatusCommand)(program);
|
|
10
12
|
(0, register_thread_commands_js_1.registerThreadCommands)(program);
|
|
11
13
|
(0, shell_js_1.registerShellCommand)(program);
|
|
12
14
|
(0, register_sdk_commands_js_1.registerSdkCommands)(program);
|
package/dist/commands/root.js
CHANGED
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.isRetryableApiConnectionError = isRetryableApiConnectionError;
|
|
36
37
|
exports.shouldUseTurnSteer = shouldUseTurnSteer;
|
|
37
38
|
exports.isNoActiveTurnSteerError = isNoActiveTurnSteerError;
|
|
38
39
|
exports.isNoRunningTurnInterruptError = isNoRunningTurnInterruptError;
|
|
@@ -40,11 +41,13 @@ exports.normalizeThreadAgentApiUrlForRuntime = normalizeThreadAgentApiUrlForRunt
|
|
|
40
41
|
exports.extractThreadNameUpdateFromNotification = extractThreadNameUpdateFromNotification;
|
|
41
42
|
exports.extractServerMessageRequestId = extractServerMessageRequestId;
|
|
42
43
|
exports.runRootCommand = runRootCommand;
|
|
44
|
+
exports.buildRootConfig = buildRootConfig;
|
|
43
45
|
exports.registerRootCommand = registerRootCommand;
|
|
44
46
|
const protobuf_1 = require("@bufbuild/protobuf");
|
|
45
47
|
const protos_1 = require("@companyhelm/protos");
|
|
46
48
|
const drizzle_orm_1 = require("drizzle-orm");
|
|
47
49
|
const grpc = __importStar(require("@grpc/grpc-js"));
|
|
50
|
+
const node_child_process_1 = require("node:child_process");
|
|
48
51
|
const node_crypto_1 = require("node:crypto");
|
|
49
52
|
const node_fs_1 = require("node:fs");
|
|
50
53
|
const node_path_1 = require("node:path");
|
|
@@ -60,8 +63,10 @@ const thread_runtime_js_1 = require("../service/thread_runtime.js");
|
|
|
60
63
|
const thread_turn_state_js_1 = require("../service/thread_turn_state.js");
|
|
61
64
|
const thread_user_message_request_store_js_1 = require("../service/thread_user_message_request_store.js");
|
|
62
65
|
const thread_lifecycle_js_1 = require("../service/thread_lifecycle.js");
|
|
66
|
+
const daemon_state_js_1 = require("../state/daemon_state.js");
|
|
63
67
|
const db_js_1 = require("../state/db.js");
|
|
64
68
|
const schema_js_1 = require("../state/schema.js");
|
|
69
|
+
const daemon_js_1 = require("../utils/daemon.js");
|
|
65
70
|
const logger_js_1 = require("../utils/logger.js");
|
|
66
71
|
const workspace_agents_js_1 = require("../service/workspace_agents.js");
|
|
67
72
|
const COMMAND_CHANNEL_CONNECT_RETRY_DELAY_MS = 1000;
|
|
@@ -83,6 +88,7 @@ const YOLO_SANDBOX_MODE = "danger-full-access";
|
|
|
83
88
|
const YOLO_SANDBOX_POLICY = { type: "dangerFullAccess" };
|
|
84
89
|
const DOCKER_INTERNAL_HOSTNAME = "host.docker.internal";
|
|
85
90
|
const LOCALHOST_HOSTNAMES = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]);
|
|
91
|
+
const DAEMON_STARTUP_TIMEOUT_MS = 15000;
|
|
86
92
|
const threadAppServerSessions = new Map();
|
|
87
93
|
const threadRolloutPaths = new Map();
|
|
88
94
|
function rememberThreadRolloutPath(threadId, rolloutPath) {
|
|
@@ -193,6 +199,26 @@ function normalizeReasoningLevels(value) {
|
|
|
193
199
|
function toErrorMessage(error) {
|
|
194
200
|
return error instanceof Error ? error.message : String(error);
|
|
195
201
|
}
|
|
202
|
+
function getGrpcStatusCode(error) {
|
|
203
|
+
if (!error || typeof error !== "object" || !("code" in error)) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
const { code } = error;
|
|
207
|
+
return typeof code === "number" ? code : undefined;
|
|
208
|
+
}
|
|
209
|
+
function isRetryableApiConnectionError(error) {
|
|
210
|
+
return getGrpcStatusCode(error) !== grpc.status.UNAUTHENTICATED;
|
|
211
|
+
}
|
|
212
|
+
function toApiConnectionFailureMessage(error, secret) {
|
|
213
|
+
const message = toErrorMessage(error);
|
|
214
|
+
if (getGrpcStatusCode(error) !== grpc.status.UNAUTHENTICATED) {
|
|
215
|
+
return message;
|
|
216
|
+
}
|
|
217
|
+
if (!secret || secret.trim().length === 0) {
|
|
218
|
+
return `${message} Provide --secret <secret> to authenticate.`;
|
|
219
|
+
}
|
|
220
|
+
return message;
|
|
221
|
+
}
|
|
196
222
|
function shouldUseTurnSteer(allowSteer, startedFromIdle) {
|
|
197
223
|
return allowSteer && !startedFromIdle;
|
|
198
224
|
}
|
|
@@ -202,6 +228,9 @@ function isNoActiveTurnSteerError(error) {
|
|
|
202
228
|
function isNoRunningTurnInterruptError(error) {
|
|
203
229
|
return /no running turn to interrupt/i.test(toErrorMessage(error));
|
|
204
230
|
}
|
|
231
|
+
function isTurnCompletionTimeoutError(error) {
|
|
232
|
+
return /timed out waiting for completion of turn/i.test(toErrorMessage(error));
|
|
233
|
+
}
|
|
205
234
|
function isRecord(value) {
|
|
206
235
|
return typeof value === "object" && value !== null;
|
|
207
236
|
}
|
|
@@ -1280,8 +1309,9 @@ async function sendRequestError(commandChannel, errorMessage, requestId) {
|
|
|
1280
1309
|
});
|
|
1281
1310
|
await commandChannel.send(message);
|
|
1282
1311
|
}
|
|
1283
|
-
async function sendThreadUpdate(commandChannel, threadId, status) {
|
|
1312
|
+
async function sendThreadUpdate(commandChannel, threadId, status, requestId) {
|
|
1284
1313
|
const message = (0, protobuf_1.create)(protos_1.ClientMessageSchema, {
|
|
1314
|
+
requestId,
|
|
1285
1315
|
payload: {
|
|
1286
1316
|
case: "threadUpdate",
|
|
1287
1317
|
value: {
|
|
@@ -1416,11 +1446,18 @@ async function resolveThreadAuthMode(cfg) {
|
|
|
1416
1446
|
}
|
|
1417
1447
|
async function handleCreateThreadRequest(cfg, commandChannel, request, requestId, apiClient, apiCallOptions, logger) {
|
|
1418
1448
|
const threadId = (request.threadId ?? "").trim();
|
|
1449
|
+
const modelName = (request.model ?? "").trim();
|
|
1450
|
+
const requestedReasoningLevel = (request.reasoningLevel ?? "").trim();
|
|
1419
1451
|
if (!threadId) {
|
|
1420
1452
|
logger.warn("Rejecting createThreadRequest: threadId is required.");
|
|
1421
1453
|
await sendRequestError(commandChannel, "Thread id is required.", requestId);
|
|
1422
1454
|
return;
|
|
1423
1455
|
}
|
|
1456
|
+
if (!modelName) {
|
|
1457
|
+
logger.warn("Rejecting createThreadRequest: model is required.");
|
|
1458
|
+
await sendRequestError(commandChannel, "Model is required.", requestId);
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1424
1461
|
const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
|
|
1425
1462
|
const threadDirectory = (0, thread_lifecycle_js_1.resolveThreadDirectory)(cfg.config_directory, cfg.workspaces_directory, threadId);
|
|
1426
1463
|
const containerNames = (0, thread_lifecycle_js_1.buildThreadContainerNames)(threadId);
|
|
@@ -1429,16 +1466,40 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
|
|
|
1429
1466
|
const threadGitSkillPackages = normalizeThreadGitSkillPackagesForThreadConfig(request.gitSkillPackages, logger);
|
|
1430
1467
|
const threadMcpServers = normalizeThreadMcpServersForThreadConfig(request.mcpServers, logger);
|
|
1431
1468
|
const cliSecret = String(request.cliSecret ?? "").trim();
|
|
1432
|
-
logger.debug(`Received createThreadRequest for thread '${threadId}' (model '${
|
|
1469
|
+
logger.debug(`Received createThreadRequest for thread '${threadId}' (model '${modelName}', reasoning '${requestedReasoningLevel}', additional instructions length '${normalizedAdditionalModelInstructions?.length ?? 0}', git skill packages '${threadGitSkillPackages.length}', MCP servers '${threadMcpServers.length}').`);
|
|
1433
1470
|
let authMode;
|
|
1434
1471
|
try {
|
|
1435
1472
|
authMode = await resolveThreadAuthMode(cfg);
|
|
1473
|
+
const modelConfig = await db
|
|
1474
|
+
.select({
|
|
1475
|
+
name: schema_js_1.llmModels.name,
|
|
1476
|
+
reasoningLevels: schema_js_1.llmModels.reasoningLevels,
|
|
1477
|
+
})
|
|
1478
|
+
.from(schema_js_1.llmModels)
|
|
1479
|
+
.where((0, drizzle_orm_1.eq)(schema_js_1.llmModels.name, modelName))
|
|
1480
|
+
.get();
|
|
1481
|
+
const configuredModelSample = await db
|
|
1482
|
+
.select({ name: schema_js_1.llmModels.name })
|
|
1483
|
+
.from(schema_js_1.llmModels)
|
|
1484
|
+
.limit(1)
|
|
1485
|
+
.all();
|
|
1486
|
+
if (configuredModelSample.length > 0) {
|
|
1487
|
+
if (!modelConfig) {
|
|
1488
|
+
throw new Error(`Model '${modelName}' is not configured.`);
|
|
1489
|
+
}
|
|
1490
|
+
if (requestedReasoningLevel.length > 0) {
|
|
1491
|
+
const supportedReasoningLevels = normalizeReasoningLevels(modelConfig.reasoningLevels);
|
|
1492
|
+
if (supportedReasoningLevels.length > 0 && !supportedReasoningLevels.includes(requestedReasoningLevel)) {
|
|
1493
|
+
throw new Error(`Reasoning level '${requestedReasoningLevel}' is not configured for model '${modelName}'.`);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1436
1497
|
await db.insert(schema_js_1.threads).values({
|
|
1437
1498
|
id: threadId,
|
|
1438
1499
|
sdkThreadId: null,
|
|
1439
1500
|
cliSecret: cliSecret.length > 0 ? cliSecret : null,
|
|
1440
|
-
model:
|
|
1441
|
-
reasoningLevel:
|
|
1501
|
+
model: modelName,
|
|
1502
|
+
reasoningLevel: requestedReasoningLevel,
|
|
1442
1503
|
additionalModelInstructions: normalizedAdditionalModelInstructions,
|
|
1443
1504
|
status: "pending",
|
|
1444
1505
|
currentSdkTurnId: null,
|
|
@@ -1508,9 +1569,73 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
|
|
|
1508
1569
|
await sendRequestError(commandChannel, `Failed to create containers for thread '${threadId}': ${toErrorMessage(error)}`, requestId);
|
|
1509
1570
|
return;
|
|
1510
1571
|
}
|
|
1572
|
+
let readyRequestId;
|
|
1511
1573
|
const { db: updateDb, client: updateClient } = await (0, db_js_1.initDb)(cfg.state_db_path);
|
|
1512
1574
|
try {
|
|
1513
|
-
|
|
1575
|
+
const threadState = await (0, thread_turn_state_js_1.loadThreadMessageExecutionState)(cfg.state_db_path, threadId);
|
|
1576
|
+
if (!threadState) {
|
|
1577
|
+
throw new Error(`Thread '${threadId}' disappeared before SDK bootstrap.`);
|
|
1578
|
+
}
|
|
1579
|
+
const threadMcpSetup = buildThreadCodexMcpSetup(readWorkspaceThreadMcpConfig(threadState.workspace, logger));
|
|
1580
|
+
const threadAgentCliConfig = readWorkspaceThreadAgentCliConfig(threadState.workspace, logger);
|
|
1581
|
+
const appServerSession = await getOrCreateThreadAppServerSession(threadId, threadState.runtimeContainer, threadMcpSetup.appServerEnv, cfg.codex.app_server_client_name, logger);
|
|
1582
|
+
const runtimeUser = {
|
|
1583
|
+
uid: threadState.uid,
|
|
1584
|
+
gid: threadState.gid,
|
|
1585
|
+
agentUser: cfg.agent_user,
|
|
1586
|
+
agentHomeDirectory: threadState.homeDirectory,
|
|
1587
|
+
};
|
|
1588
|
+
await (0, thread_runtime_js_1.ensureThreadRuntimeReady)({
|
|
1589
|
+
dindContainer: threadState.dindContainer,
|
|
1590
|
+
runtimeContainer: threadState.runtimeContainer,
|
|
1591
|
+
containerService,
|
|
1592
|
+
gitUserName: cfg.git_user_name,
|
|
1593
|
+
gitUserEmail: cfg.git_user_email,
|
|
1594
|
+
user: runtimeUser,
|
|
1595
|
+
});
|
|
1596
|
+
await ensureThreadGitSkillsInRuntime(cfg, threadState, containerService, logger);
|
|
1597
|
+
if (threadAgentCliConfig) {
|
|
1598
|
+
await containerService.ensureRuntimeContainerAgentCliConfig(threadState.runtimeContainer, runtimeUser, threadAgentCliConfig);
|
|
1599
|
+
}
|
|
1600
|
+
if (!appServerSession.started) {
|
|
1601
|
+
await containerService.ensureRuntimeContainerCodexConfig(threadState.runtimeContainer, runtimeUser, threadMcpSetup.configToml);
|
|
1602
|
+
}
|
|
1603
|
+
await ensureThreadAppServerSessionStarted(appServerSession);
|
|
1604
|
+
const developerInstructions = buildThreadDeveloperInstructions(threadState.additionalModelInstructions);
|
|
1605
|
+
logger.debug(`Starting app-server thread '${threadId}' with developer instructions: ${JSON.stringify(developerInstructions)}.`);
|
|
1606
|
+
const threadStartResponse = await appServerSession.appServer.startThreadWithResponse({
|
|
1607
|
+
model: threadState.model,
|
|
1608
|
+
modelProvider: null,
|
|
1609
|
+
cwd: "/workspace",
|
|
1610
|
+
approvalPolicy: YOLO_APPROVAL_POLICY,
|
|
1611
|
+
sandbox: YOLO_SANDBOX_MODE,
|
|
1612
|
+
config: null,
|
|
1613
|
+
baseInstructions: null,
|
|
1614
|
+
developerInstructions,
|
|
1615
|
+
personality: null,
|
|
1616
|
+
ephemeral: null,
|
|
1617
|
+
experimentalRawEvents: false,
|
|
1618
|
+
persistExtendedHistory: true,
|
|
1619
|
+
}, requestId);
|
|
1620
|
+
if (requestId && threadStartResponse.id !== requestId) {
|
|
1621
|
+
throw new Error(`App-server thread/start response id '${String(threadStartResponse.id)}' did not match runner request id '${requestId}'.`);
|
|
1622
|
+
}
|
|
1623
|
+
if (!threadStartResponse.result.thread?.id) {
|
|
1624
|
+
throw new Error(`App-server thread/start did not return an SDK thread id for thread '${threadId}'.`);
|
|
1625
|
+
}
|
|
1626
|
+
readyRequestId = typeof threadStartResponse.id === "string" && threadStartResponse.id.length > 0
|
|
1627
|
+
? threadStartResponse.id
|
|
1628
|
+
: undefined;
|
|
1629
|
+
appServerSession.sdkThreadId = threadStartResponse.result.thread.id;
|
|
1630
|
+
appServerSession.rolloutPath = threadStartResponse.result.thread.path;
|
|
1631
|
+
rememberThreadRolloutPath(threadId, threadStartResponse.result.thread.path);
|
|
1632
|
+
await updateDb
|
|
1633
|
+
.update(schema_js_1.threads)
|
|
1634
|
+
.set({
|
|
1635
|
+
status: "ready",
|
|
1636
|
+
sdkThreadId: threadStartResponse.result.thread.id,
|
|
1637
|
+
})
|
|
1638
|
+
.where((0, drizzle_orm_1.eq)(schema_js_1.threads.id, threadId));
|
|
1514
1639
|
}
|
|
1515
1640
|
catch (error) {
|
|
1516
1641
|
logger.warn(`Failed to mark thread '${threadId}' as ready: ${toErrorMessage(error)}`);
|
|
@@ -1527,7 +1652,7 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
|
|
|
1527
1652
|
updateClient.close();
|
|
1528
1653
|
}
|
|
1529
1654
|
logger.info(`Thread '${threadId}' created and ready.`);
|
|
1530
|
-
await sendThreadUpdate(commandChannel, threadId, protos_1.ThreadStatus.READY);
|
|
1655
|
+
await sendThreadUpdate(commandChannel, threadId, protos_1.ThreadStatus.READY, readyRequestId);
|
|
1531
1656
|
}
|
|
1532
1657
|
async function deleteThreadWithCleanup(cfg, request) {
|
|
1533
1658
|
const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
|
|
@@ -1742,6 +1867,7 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
|
|
|
1742
1867
|
let keepRuntimeWarm = false;
|
|
1743
1868
|
let shouldTrackTurnCompletion = trackTurnCompletion;
|
|
1744
1869
|
let enqueuedRequestTurnId = null;
|
|
1870
|
+
let turnCompletionWaitStarted = false;
|
|
1745
1871
|
try {
|
|
1746
1872
|
await (0, thread_runtime_js_1.ensureThreadRuntimeReady)({
|
|
1747
1873
|
dindContainer: threadState.dindContainer,
|
|
@@ -1827,14 +1953,18 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
|
|
|
1827
1953
|
if (!threadState.currentSdkTurnId) {
|
|
1828
1954
|
throw new Error(`Thread '${request.threadId}' is marked running but has no current SDK turn id.`);
|
|
1829
1955
|
}
|
|
1956
|
+
const activeSdkTurnId = threadState.currentSdkTurnId;
|
|
1830
1957
|
const steerParams = {
|
|
1831
1958
|
threadId: resolvedSdkThreadId,
|
|
1832
1959
|
input,
|
|
1833
|
-
expectedTurnId:
|
|
1960
|
+
expectedTurnId: activeSdkTurnId,
|
|
1834
1961
|
};
|
|
1835
1962
|
try {
|
|
1836
1963
|
const turnSteerResult = await appServer.steerTurn(steerParams);
|
|
1837
|
-
|
|
1964
|
+
if (turnSteerResult.turnId && turnSteerResult.turnId !== activeSdkTurnId) {
|
|
1965
|
+
logger.debug(`turn/steer returned turn '${turnSteerResult.turnId}' for thread '${request.threadId}', preserving active turn '${activeSdkTurnId}' as the canonical turn id.`);
|
|
1966
|
+
}
|
|
1967
|
+
sdkTurnId = activeSdkTurnId;
|
|
1838
1968
|
}
|
|
1839
1969
|
catch (error) {
|
|
1840
1970
|
if (!isNoActiveTurnSteerError(error)) {
|
|
@@ -1864,6 +1994,7 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
|
|
|
1864
1994
|
keepRuntimeWarm = true;
|
|
1865
1995
|
return;
|
|
1866
1996
|
}
|
|
1997
|
+
turnCompletionWaitStarted = true;
|
|
1867
1998
|
const terminalStatus = await waitForThreadTurnCompletion(cfg.state_db_path, appServer, commandChannel, request.threadId, sdkThreadId, sdkTurnId, logger, requestId);
|
|
1868
1999
|
await updateThreadTurnState(cfg, request.threadId, {
|
|
1869
2000
|
currentSdkTurnId: sdkTurnId,
|
|
@@ -1886,7 +2017,12 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
|
|
|
1886
2017
|
if (enqueuedRequestTurnId && requestId) {
|
|
1887
2018
|
await (0, thread_user_message_request_store_js_1.removePendingUserMessageRequestIdForTurn)(cfg.state_db_path, request.threadId, enqueuedRequestTurnId, requestId);
|
|
1888
2019
|
}
|
|
1889
|
-
if (
|
|
2020
|
+
if (turnCompletionWaitStarted && !isTurnCompletionTimeoutError(error)) {
|
|
2021
|
+
await updateThreadTurnState(cfg, request.threadId, {
|
|
2022
|
+
isCurrentTurnRunning: false,
|
|
2023
|
+
}).catch(() => undefined);
|
|
2024
|
+
}
|
|
2025
|
+
else if (startedFromIdle && !turnAccepted) {
|
|
1890
2026
|
await updateThreadTurnState(cfg, request.threadId, {
|
|
1891
2027
|
isCurrentTurnRunning: false,
|
|
1892
2028
|
}).catch(() => undefined);
|
|
@@ -1979,25 +2115,109 @@ function buildGrpcAuthCallOptions(secret) {
|
|
|
1979
2115
|
metadata.set("authorization", `Bearer ${secret}`);
|
|
1980
2116
|
return { metadata };
|
|
1981
2117
|
}
|
|
1982
|
-
|
|
2118
|
+
function isInternalDaemonChildProcess() {
|
|
2119
|
+
return process.env[daemon_js_1.DAEMON_CHILD_ENV] === "1";
|
|
2120
|
+
}
|
|
2121
|
+
function resolveEffectiveDaemonLogPath(cfg) {
|
|
2122
|
+
const envPath = process.env[daemon_js_1.DAEMON_LOG_PATH_ENV];
|
|
2123
|
+
if (envPath && envPath.trim().length > 0) {
|
|
2124
|
+
return envPath;
|
|
2125
|
+
}
|
|
2126
|
+
return (0, daemon_js_1.resolveDaemonLogPath)(cfg.state_db_path);
|
|
2127
|
+
}
|
|
2128
|
+
async function runDetachedDaemonProcess(options) {
|
|
2129
|
+
const cfg = buildRootConfig(options);
|
|
2130
|
+
const logPath = (0, daemon_js_1.resolveDaemonLogPath)(cfg.state_db_path);
|
|
2131
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(logPath), { recursive: true });
|
|
2132
|
+
const logFd = (0, node_fs_1.openSync)(logPath, "a");
|
|
2133
|
+
try {
|
|
2134
|
+
await new Promise((resolve, reject) => {
|
|
2135
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, process.argv.slice(1), {
|
|
2136
|
+
cwd: process.cwd(),
|
|
2137
|
+
detached: true,
|
|
2138
|
+
env: {
|
|
2139
|
+
...process.env,
|
|
2140
|
+
[daemon_js_1.DAEMON_CHILD_ENV]: "1",
|
|
2141
|
+
[daemon_js_1.DAEMON_LOG_PATH_ENV]: logPath,
|
|
2142
|
+
},
|
|
2143
|
+
stdio: ["ignore", logFd, logFd, "ipc"],
|
|
2144
|
+
windowsHide: true,
|
|
2145
|
+
});
|
|
2146
|
+
let settled = false;
|
|
2147
|
+
const timeout = setTimeout(() => {
|
|
2148
|
+
if (settled) {
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
settled = true;
|
|
2152
|
+
child.kill();
|
|
2153
|
+
reject(new Error(`Timed out waiting for daemon startup confirmation. See ${logPath}.`));
|
|
2154
|
+
}, DAEMON_STARTUP_TIMEOUT_MS);
|
|
2155
|
+
const finish = (callback) => {
|
|
2156
|
+
if (settled) {
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
settled = true;
|
|
2160
|
+
clearTimeout(timeout);
|
|
2161
|
+
callback();
|
|
2162
|
+
};
|
|
2163
|
+
child.once("error", (error) => {
|
|
2164
|
+
finish(() => reject(error));
|
|
2165
|
+
});
|
|
2166
|
+
child.once("exit", (code, signal) => {
|
|
2167
|
+
finish(() => {
|
|
2168
|
+
reject(new Error(`Daemon exited before startup completed (code=${code ?? "null"}, signal=${signal ?? "null"}). See ${logPath}.`));
|
|
2169
|
+
});
|
|
2170
|
+
});
|
|
2171
|
+
child.on("message", (message) => {
|
|
2172
|
+
if (!message || typeof message !== "object" || !("type" in message)) {
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
const type = message.type;
|
|
2176
|
+
if (type === "daemon-ready") {
|
|
2177
|
+
finish(() => {
|
|
2178
|
+
if (child.connected) {
|
|
2179
|
+
child.disconnect();
|
|
2180
|
+
}
|
|
2181
|
+
child.unref();
|
|
2182
|
+
console.log(`CompanyHelm daemon started (pid ${child.pid}). Logs: ${logPath}`);
|
|
2183
|
+
resolve();
|
|
2184
|
+
});
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
if (type === "daemon-error") {
|
|
2188
|
+
const messageValue = message.message;
|
|
2189
|
+
const daemonErrorMessage = typeof messageValue === "string" ? messageValue : `Daemon startup failed. See ${logPath}.`;
|
|
2190
|
+
finish(() => reject(new Error(daemonErrorMessage)));
|
|
2191
|
+
}
|
|
2192
|
+
});
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
finally {
|
|
2196
|
+
(0, node_fs_1.closeSync)(logFd);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
function sendDaemonParentMessage(message) {
|
|
2200
|
+
if (typeof process.send === "function") {
|
|
2201
|
+
process.send(message);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
async function runRootCommand(options, runtimeOptions) {
|
|
1983
2205
|
const logger = (0, logger_js_1.createLogger)(options.logLevel ?? "INFO", { daemonMode: options.daemon ?? false });
|
|
1984
|
-
const cfg =
|
|
1985
|
-
companyhelm_api_url: options.serverUrl,
|
|
1986
|
-
agent_api_url: options.agentApiUrl,
|
|
1987
|
-
use_host_docker_runtime: options.useHostDockerRuntime,
|
|
1988
|
-
host_docker_path: options.hostDockerPath,
|
|
1989
|
-
thread_git_skills_directory: options.threadGitSkillsDirectory,
|
|
1990
|
-
});
|
|
2206
|
+
const cfg = buildRootConfig(options);
|
|
1991
2207
|
const configuredSdks = await hasConfiguredSdks(cfg);
|
|
1992
2208
|
if (!configuredSdks && options.daemon) {
|
|
1993
2209
|
throw new Error("No SDKs configured. Daemon mode requires at least one configured SDK.");
|
|
1994
2210
|
}
|
|
1995
2211
|
if (!configuredSdks) {
|
|
1996
|
-
await (0, startup_js_1.startup)();
|
|
2212
|
+
await (0, startup_js_1.startup)(cfg);
|
|
1997
2213
|
}
|
|
1998
2214
|
await refreshCodexModelsForRegistration(cfg, logger);
|
|
1999
2215
|
const registerRequest = await buildRegisterRunnerRequest(cfg);
|
|
2000
2216
|
const apiCallOptions = buildGrpcAuthCallOptions(options.secret);
|
|
2217
|
+
if (options.daemon) {
|
|
2218
|
+
await (0, daemon_state_js_1.claimCurrentDaemonState)(cfg.state_db_path, process.pid, resolveEffectiveDaemonLogPath(cfg));
|
|
2219
|
+
runtimeOptions?.onDaemonReady?.();
|
|
2220
|
+
}
|
|
2001
2221
|
const commandMessageSink = new buffered_client_message_sender_js_1.BufferedClientMessageSender({
|
|
2002
2222
|
maxBufferedEvents: cfg.client_message_buffer_limit,
|
|
2003
2223
|
logger,
|
|
@@ -2032,7 +2252,10 @@ async function runRootCommand(options) {
|
|
|
2032
2252
|
logger.warn("CompanyHelm API command channel closed. Reconnecting...");
|
|
2033
2253
|
}
|
|
2034
2254
|
catch (error) {
|
|
2035
|
-
const failureMessage =
|
|
2255
|
+
const failureMessage = toApiConnectionFailureMessage(error, options.secret);
|
|
2256
|
+
if (!isRetryableApiConnectionError(error)) {
|
|
2257
|
+
throw new Error(failureMessage);
|
|
2258
|
+
}
|
|
2036
2259
|
logger.warn(`CompanyHelm API connection attempt ${reconnectAttempt} failed: ${failureMessage}. ` +
|
|
2037
2260
|
"Retrying...");
|
|
2038
2261
|
}
|
|
@@ -2057,21 +2280,58 @@ async function runRootCommand(options) {
|
|
|
2057
2280
|
if (droppedMessages > 0) {
|
|
2058
2281
|
logger.warn(`Dropped ${droppedMessages} outbound client message(s) while command channel was disconnected.`);
|
|
2059
2282
|
}
|
|
2283
|
+
if (options.daemon) {
|
|
2284
|
+
await (0, daemon_state_js_1.clearCurrentDaemonState)(cfg.state_db_path, process.pid).catch((error) => {
|
|
2285
|
+
logger.warn(`Failed to clear daemon state: ${toErrorMessage(error)}`);
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2060
2288
|
await stopAllThreadAppServerSessions();
|
|
2061
2289
|
await stopAllThreadContainers(cfg, logger);
|
|
2062
2290
|
}
|
|
2063
2291
|
}
|
|
2292
|
+
function buildRootConfig(options) {
|
|
2293
|
+
return config_js_1.config.parse({
|
|
2294
|
+
config_directory: options.configPath,
|
|
2295
|
+
state_db_path: options.stateDbPath,
|
|
2296
|
+
companyhelm_api_url: options.serverUrl,
|
|
2297
|
+
agent_api_url: options.agentApiUrl,
|
|
2298
|
+
use_host_docker_runtime: options.useHostDockerRuntime,
|
|
2299
|
+
host_docker_path: options.hostDockerPath,
|
|
2300
|
+
thread_git_skills_directory: options.threadGitSkillsDirectory,
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2064
2303
|
function registerRootCommand(program) {
|
|
2065
2304
|
program
|
|
2305
|
+
.option("--config-path <path>", "Config directory override (defaults to ~/.config/companyhelm).")
|
|
2066
2306
|
.option("--server-url <url>", "CompanyHelm gRPC API URL override.")
|
|
2067
2307
|
.option("--agent-api-url <url>", "Agent gRPC API URL for companyhelm-agent in runtime containers (localhost is rewritten to http://host.docker.internal).")
|
|
2068
2308
|
.option("--secret <secret>", "Bearer secret used as gRPC Authorization header.")
|
|
2309
|
+
.option("--state-db-path <path>", "State database path override (defaults to ~/.local/share/companyhelm/state.db).")
|
|
2069
2310
|
.option("--use-host-docker-runtime", "Mount host Docker socket into runtime containers instead of creating DinD sidecars.")
|
|
2070
2311
|
.option("--host-docker-path <path>", "Host Docker endpoint when --use-host-docker-runtime is enabled (unix:///<socket-path> or tcp://localhost:<port>).")
|
|
2071
2312
|
.option("--thread-git-skills-directory <path>", "Container path where thread git skill repositories are cloned before linking into ~/.codex/skills.")
|
|
2072
2313
|
.option("-d, --daemon", "Run in daemon mode and fail fast when no SDK is configured.")
|
|
2073
2314
|
.option("--log-level <level>", "Log level (DEBUG, INFO, WARN, ERROR).", "INFO")
|
|
2074
2315
|
.action(async () => {
|
|
2075
|
-
|
|
2316
|
+
const options = program.opts();
|
|
2317
|
+
if (options.daemon && !isInternalDaemonChildProcess()) {
|
|
2318
|
+
await runDetachedDaemonProcess(options);
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
try {
|
|
2322
|
+
await runRootCommand(options, isInternalDaemonChildProcess()
|
|
2323
|
+
? {
|
|
2324
|
+
onDaemonReady: () => {
|
|
2325
|
+
sendDaemonParentMessage({ type: "daemon-ready" });
|
|
2326
|
+
},
|
|
2327
|
+
}
|
|
2328
|
+
: undefined);
|
|
2329
|
+
}
|
|
2330
|
+
catch (error) {
|
|
2331
|
+
if (isInternalDaemonChildProcess()) {
|
|
2332
|
+
sendDaemonParentMessage({ type: "daemon-error", message: toErrorMessage(error) });
|
|
2333
|
+
}
|
|
2334
|
+
throw error;
|
|
2335
|
+
}
|
|
2076
2336
|
});
|
|
2077
2337
|
}
|