@companyhelm/cli 0.0.2 → 0.0.6
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 +341 -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 +7 -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/dist/commands/root.js
CHANGED
|
@@ -33,6 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.isRetryableApiConnectionError = isRetryableApiConnectionError;
|
|
37
|
+
exports.formatApiConnectionFailureMessage = formatApiConnectionFailureMessage;
|
|
38
|
+
exports.formatApiConnectionFailureDiagnostics = formatApiConnectionFailureDiagnostics;
|
|
36
39
|
exports.shouldUseTurnSteer = shouldUseTurnSteer;
|
|
37
40
|
exports.isNoActiveTurnSteerError = isNoActiveTurnSteerError;
|
|
38
41
|
exports.isNoRunningTurnInterruptError = isNoRunningTurnInterruptError;
|
|
@@ -40,11 +43,13 @@ exports.normalizeThreadAgentApiUrlForRuntime = normalizeThreadAgentApiUrlForRunt
|
|
|
40
43
|
exports.extractThreadNameUpdateFromNotification = extractThreadNameUpdateFromNotification;
|
|
41
44
|
exports.extractServerMessageRequestId = extractServerMessageRequestId;
|
|
42
45
|
exports.runRootCommand = runRootCommand;
|
|
46
|
+
exports.buildRootConfig = buildRootConfig;
|
|
43
47
|
exports.registerRootCommand = registerRootCommand;
|
|
44
48
|
const protobuf_1 = require("@bufbuild/protobuf");
|
|
45
49
|
const protos_1 = require("@companyhelm/protos");
|
|
46
50
|
const drizzle_orm_1 = require("drizzle-orm");
|
|
47
51
|
const grpc = __importStar(require("@grpc/grpc-js"));
|
|
52
|
+
const node_child_process_1 = require("node:child_process");
|
|
48
53
|
const node_crypto_1 = require("node:crypto");
|
|
49
54
|
const node_fs_1 = require("node:fs");
|
|
50
55
|
const node_path_1 = require("node:path");
|
|
@@ -60,8 +65,10 @@ const thread_runtime_js_1 = require("../service/thread_runtime.js");
|
|
|
60
65
|
const thread_turn_state_js_1 = require("../service/thread_turn_state.js");
|
|
61
66
|
const thread_user_message_request_store_js_1 = require("../service/thread_user_message_request_store.js");
|
|
62
67
|
const thread_lifecycle_js_1 = require("../service/thread_lifecycle.js");
|
|
68
|
+
const daemon_state_js_1 = require("../state/daemon_state.js");
|
|
63
69
|
const db_js_1 = require("../state/db.js");
|
|
64
70
|
const schema_js_1 = require("../state/schema.js");
|
|
71
|
+
const daemon_js_1 = require("../utils/daemon.js");
|
|
65
72
|
const logger_js_1 = require("../utils/logger.js");
|
|
66
73
|
const workspace_agents_js_1 = require("../service/workspace_agents.js");
|
|
67
74
|
const COMMAND_CHANNEL_CONNECT_RETRY_DELAY_MS = 1000;
|
|
@@ -83,6 +90,7 @@ const YOLO_SANDBOX_MODE = "danger-full-access";
|
|
|
83
90
|
const YOLO_SANDBOX_POLICY = { type: "dangerFullAccess" };
|
|
84
91
|
const DOCKER_INTERNAL_HOSTNAME = "host.docker.internal";
|
|
85
92
|
const LOCALHOST_HOSTNAMES = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]);
|
|
93
|
+
const DAEMON_STARTUP_TIMEOUT_MS = 15000;
|
|
86
94
|
const threadAppServerSessions = new Map();
|
|
87
95
|
const threadRolloutPaths = new Map();
|
|
88
96
|
function rememberThreadRolloutPath(threadId, rolloutPath) {
|
|
@@ -193,6 +201,81 @@ function normalizeReasoningLevels(value) {
|
|
|
193
201
|
function toErrorMessage(error) {
|
|
194
202
|
return error instanceof Error ? error.message : String(error);
|
|
195
203
|
}
|
|
204
|
+
function getGrpcStatusCode(error) {
|
|
205
|
+
if (!error || typeof error !== "object" || !("code" in error)) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
const { code } = error;
|
|
209
|
+
return typeof code === "number" ? code : undefined;
|
|
210
|
+
}
|
|
211
|
+
function getGrpcStatusName(error) {
|
|
212
|
+
const code = getGrpcStatusCode(error);
|
|
213
|
+
if (code === undefined) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
const statusName = grpc.status[code];
|
|
217
|
+
return typeof statusName === "string" ? statusName : undefined;
|
|
218
|
+
}
|
|
219
|
+
function isRetryableApiConnectionError(error) {
|
|
220
|
+
return getGrpcStatusCode(error) !== grpc.status.UNAUTHENTICATED;
|
|
221
|
+
}
|
|
222
|
+
function formatGrpcMetadataForLog(metadata) {
|
|
223
|
+
if (!metadata) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
const rawEntries = metadata.getMap();
|
|
227
|
+
const entries = Object.entries(rawEntries);
|
|
228
|
+
if (entries.length === 0) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
const normalizedEntries = Object.fromEntries(entries.map(([key, value]) => [
|
|
232
|
+
key,
|
|
233
|
+
Buffer.isBuffer(value) ? value.toString("base64") : String(value),
|
|
234
|
+
]));
|
|
235
|
+
return JSON.stringify(normalizedEntries);
|
|
236
|
+
}
|
|
237
|
+
function formatApiConnectionFailureMessage(error, apiUrl, secret) {
|
|
238
|
+
const statusCode = getGrpcStatusCode(error);
|
|
239
|
+
const statusName = getGrpcStatusName(error);
|
|
240
|
+
const serviceError = isGrpcServiceError(error) ? error : undefined;
|
|
241
|
+
const baseMessage = serviceError && typeof serviceError.details === "string" && serviceError.details.trim().length > 0
|
|
242
|
+
? serviceError.details.trim()
|
|
243
|
+
: toErrorMessage(error);
|
|
244
|
+
let message = baseMessage;
|
|
245
|
+
if (statusCode !== undefined) {
|
|
246
|
+
message = `gRPC ${statusName ?? "UNKNOWN"} (${statusCode}): ${baseMessage}`;
|
|
247
|
+
}
|
|
248
|
+
message += ` [endpoint=${apiUrl}]`;
|
|
249
|
+
if (statusCode === grpc.status.UNAUTHENTICATED && (!secret || secret.trim().length === 0)) {
|
|
250
|
+
message += " Provide --secret <secret> to authenticate.";
|
|
251
|
+
}
|
|
252
|
+
return message;
|
|
253
|
+
}
|
|
254
|
+
function formatApiConnectionFailureDiagnostics(error) {
|
|
255
|
+
if (!isGrpcServiceError(error)) {
|
|
256
|
+
return error instanceof Error && typeof error.stack === "string" ? error.stack : undefined;
|
|
257
|
+
}
|
|
258
|
+
const diagnostics = [];
|
|
259
|
+
const statusCode = getGrpcStatusCode(error);
|
|
260
|
+
const statusName = getGrpcStatusName(error);
|
|
261
|
+
if (statusCode !== undefined) {
|
|
262
|
+
diagnostics.push(`code=${statusCode}`);
|
|
263
|
+
}
|
|
264
|
+
if (statusName) {
|
|
265
|
+
diagnostics.push(`status=${statusName}`);
|
|
266
|
+
}
|
|
267
|
+
if (typeof error.details === "string" && error.details.trim().length > 0) {
|
|
268
|
+
diagnostics.push(`details=${JSON.stringify(error.details.trim())}`);
|
|
269
|
+
}
|
|
270
|
+
const metadata = formatGrpcMetadataForLog(error.metadata);
|
|
271
|
+
if (metadata) {
|
|
272
|
+
diagnostics.push(`metadata=${metadata}`);
|
|
273
|
+
}
|
|
274
|
+
if (typeof error.stack === "string" && error.stack.trim().length > 0) {
|
|
275
|
+
diagnostics.push(`stack=${JSON.stringify(error.stack)}`);
|
|
276
|
+
}
|
|
277
|
+
return diagnostics.length > 0 ? diagnostics.join(" ") : undefined;
|
|
278
|
+
}
|
|
196
279
|
function shouldUseTurnSteer(allowSteer, startedFromIdle) {
|
|
197
280
|
return allowSteer && !startedFromIdle;
|
|
198
281
|
}
|
|
@@ -202,6 +285,9 @@ function isNoActiveTurnSteerError(error) {
|
|
|
202
285
|
function isNoRunningTurnInterruptError(error) {
|
|
203
286
|
return /no running turn to interrupt/i.test(toErrorMessage(error));
|
|
204
287
|
}
|
|
288
|
+
function isTurnCompletionTimeoutError(error) {
|
|
289
|
+
return /timed out waiting for completion of turn/i.test(toErrorMessage(error));
|
|
290
|
+
}
|
|
205
291
|
function isRecord(value) {
|
|
206
292
|
return typeof value === "object" && value !== null;
|
|
207
293
|
}
|
|
@@ -1280,8 +1366,9 @@ async function sendRequestError(commandChannel, errorMessage, requestId) {
|
|
|
1280
1366
|
});
|
|
1281
1367
|
await commandChannel.send(message);
|
|
1282
1368
|
}
|
|
1283
|
-
async function sendThreadUpdate(commandChannel, threadId, status) {
|
|
1369
|
+
async function sendThreadUpdate(commandChannel, threadId, status, requestId) {
|
|
1284
1370
|
const message = (0, protobuf_1.create)(protos_1.ClientMessageSchema, {
|
|
1371
|
+
requestId,
|
|
1285
1372
|
payload: {
|
|
1286
1373
|
case: "threadUpdate",
|
|
1287
1374
|
value: {
|
|
@@ -1416,11 +1503,18 @@ async function resolveThreadAuthMode(cfg) {
|
|
|
1416
1503
|
}
|
|
1417
1504
|
async function handleCreateThreadRequest(cfg, commandChannel, request, requestId, apiClient, apiCallOptions, logger) {
|
|
1418
1505
|
const threadId = (request.threadId ?? "").trim();
|
|
1506
|
+
const modelName = (request.model ?? "").trim();
|
|
1507
|
+
const requestedReasoningLevel = (request.reasoningLevel ?? "").trim();
|
|
1419
1508
|
if (!threadId) {
|
|
1420
1509
|
logger.warn("Rejecting createThreadRequest: threadId is required.");
|
|
1421
1510
|
await sendRequestError(commandChannel, "Thread id is required.", requestId);
|
|
1422
1511
|
return;
|
|
1423
1512
|
}
|
|
1513
|
+
if (!modelName) {
|
|
1514
|
+
logger.warn("Rejecting createThreadRequest: model is required.");
|
|
1515
|
+
await sendRequestError(commandChannel, "Model is required.", requestId);
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1424
1518
|
const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
|
|
1425
1519
|
const threadDirectory = (0, thread_lifecycle_js_1.resolveThreadDirectory)(cfg.config_directory, cfg.workspaces_directory, threadId);
|
|
1426
1520
|
const containerNames = (0, thread_lifecycle_js_1.buildThreadContainerNames)(threadId);
|
|
@@ -1429,16 +1523,40 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
|
|
|
1429
1523
|
const threadGitSkillPackages = normalizeThreadGitSkillPackagesForThreadConfig(request.gitSkillPackages, logger);
|
|
1430
1524
|
const threadMcpServers = normalizeThreadMcpServersForThreadConfig(request.mcpServers, logger);
|
|
1431
1525
|
const cliSecret = String(request.cliSecret ?? "").trim();
|
|
1432
|
-
logger.debug(`Received createThreadRequest for thread '${threadId}' (model '${
|
|
1526
|
+
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
1527
|
let authMode;
|
|
1434
1528
|
try {
|
|
1435
1529
|
authMode = await resolveThreadAuthMode(cfg);
|
|
1530
|
+
const modelConfig = await db
|
|
1531
|
+
.select({
|
|
1532
|
+
name: schema_js_1.llmModels.name,
|
|
1533
|
+
reasoningLevels: schema_js_1.llmModels.reasoningLevels,
|
|
1534
|
+
})
|
|
1535
|
+
.from(schema_js_1.llmModels)
|
|
1536
|
+
.where((0, drizzle_orm_1.eq)(schema_js_1.llmModels.name, modelName))
|
|
1537
|
+
.get();
|
|
1538
|
+
const configuredModelSample = await db
|
|
1539
|
+
.select({ name: schema_js_1.llmModels.name })
|
|
1540
|
+
.from(schema_js_1.llmModels)
|
|
1541
|
+
.limit(1)
|
|
1542
|
+
.all();
|
|
1543
|
+
if (configuredModelSample.length > 0) {
|
|
1544
|
+
if (!modelConfig) {
|
|
1545
|
+
throw new Error(`Model '${modelName}' is not configured.`);
|
|
1546
|
+
}
|
|
1547
|
+
if (requestedReasoningLevel.length > 0) {
|
|
1548
|
+
const supportedReasoningLevels = normalizeReasoningLevels(modelConfig.reasoningLevels);
|
|
1549
|
+
if (supportedReasoningLevels.length > 0 && !supportedReasoningLevels.includes(requestedReasoningLevel)) {
|
|
1550
|
+
throw new Error(`Reasoning level '${requestedReasoningLevel}' is not configured for model '${modelName}'.`);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1436
1554
|
await db.insert(schema_js_1.threads).values({
|
|
1437
1555
|
id: threadId,
|
|
1438
1556
|
sdkThreadId: null,
|
|
1439
1557
|
cliSecret: cliSecret.length > 0 ? cliSecret : null,
|
|
1440
|
-
model:
|
|
1441
|
-
reasoningLevel:
|
|
1558
|
+
model: modelName,
|
|
1559
|
+
reasoningLevel: requestedReasoningLevel,
|
|
1442
1560
|
additionalModelInstructions: normalizedAdditionalModelInstructions,
|
|
1443
1561
|
status: "pending",
|
|
1444
1562
|
currentSdkTurnId: null,
|
|
@@ -1508,9 +1626,73 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
|
|
|
1508
1626
|
await sendRequestError(commandChannel, `Failed to create containers for thread '${threadId}': ${toErrorMessage(error)}`, requestId);
|
|
1509
1627
|
return;
|
|
1510
1628
|
}
|
|
1629
|
+
let readyRequestId;
|
|
1511
1630
|
const { db: updateDb, client: updateClient } = await (0, db_js_1.initDb)(cfg.state_db_path);
|
|
1512
1631
|
try {
|
|
1513
|
-
|
|
1632
|
+
const threadState = await (0, thread_turn_state_js_1.loadThreadMessageExecutionState)(cfg.state_db_path, threadId);
|
|
1633
|
+
if (!threadState) {
|
|
1634
|
+
throw new Error(`Thread '${threadId}' disappeared before SDK bootstrap.`);
|
|
1635
|
+
}
|
|
1636
|
+
const threadMcpSetup = buildThreadCodexMcpSetup(readWorkspaceThreadMcpConfig(threadState.workspace, logger));
|
|
1637
|
+
const threadAgentCliConfig = readWorkspaceThreadAgentCliConfig(threadState.workspace, logger);
|
|
1638
|
+
const appServerSession = await getOrCreateThreadAppServerSession(threadId, threadState.runtimeContainer, threadMcpSetup.appServerEnv, cfg.codex.app_server_client_name, logger);
|
|
1639
|
+
const runtimeUser = {
|
|
1640
|
+
uid: threadState.uid,
|
|
1641
|
+
gid: threadState.gid,
|
|
1642
|
+
agentUser: cfg.agent_user,
|
|
1643
|
+
agentHomeDirectory: threadState.homeDirectory,
|
|
1644
|
+
};
|
|
1645
|
+
await (0, thread_runtime_js_1.ensureThreadRuntimeReady)({
|
|
1646
|
+
dindContainer: threadState.dindContainer,
|
|
1647
|
+
runtimeContainer: threadState.runtimeContainer,
|
|
1648
|
+
containerService,
|
|
1649
|
+
gitUserName: cfg.git_user_name,
|
|
1650
|
+
gitUserEmail: cfg.git_user_email,
|
|
1651
|
+
user: runtimeUser,
|
|
1652
|
+
});
|
|
1653
|
+
await ensureThreadGitSkillsInRuntime(cfg, threadState, containerService, logger);
|
|
1654
|
+
if (threadAgentCliConfig) {
|
|
1655
|
+
await containerService.ensureRuntimeContainerAgentCliConfig(threadState.runtimeContainer, runtimeUser, threadAgentCliConfig);
|
|
1656
|
+
}
|
|
1657
|
+
if (!appServerSession.started) {
|
|
1658
|
+
await containerService.ensureRuntimeContainerCodexConfig(threadState.runtimeContainer, runtimeUser, threadMcpSetup.configToml);
|
|
1659
|
+
}
|
|
1660
|
+
await ensureThreadAppServerSessionStarted(appServerSession);
|
|
1661
|
+
const developerInstructions = buildThreadDeveloperInstructions(threadState.additionalModelInstructions);
|
|
1662
|
+
logger.debug(`Starting app-server thread '${threadId}' with developer instructions: ${JSON.stringify(developerInstructions)}.`);
|
|
1663
|
+
const threadStartResponse = await appServerSession.appServer.startThreadWithResponse({
|
|
1664
|
+
model: threadState.model,
|
|
1665
|
+
modelProvider: null,
|
|
1666
|
+
cwd: "/workspace",
|
|
1667
|
+
approvalPolicy: YOLO_APPROVAL_POLICY,
|
|
1668
|
+
sandbox: YOLO_SANDBOX_MODE,
|
|
1669
|
+
config: null,
|
|
1670
|
+
baseInstructions: null,
|
|
1671
|
+
developerInstructions,
|
|
1672
|
+
personality: null,
|
|
1673
|
+
ephemeral: null,
|
|
1674
|
+
experimentalRawEvents: false,
|
|
1675
|
+
persistExtendedHistory: true,
|
|
1676
|
+
}, requestId);
|
|
1677
|
+
if (requestId && threadStartResponse.id !== requestId) {
|
|
1678
|
+
throw new Error(`App-server thread/start response id '${String(threadStartResponse.id)}' did not match runner request id '${requestId}'.`);
|
|
1679
|
+
}
|
|
1680
|
+
if (!threadStartResponse.result.thread?.id) {
|
|
1681
|
+
throw new Error(`App-server thread/start did not return an SDK thread id for thread '${threadId}'.`);
|
|
1682
|
+
}
|
|
1683
|
+
readyRequestId = typeof threadStartResponse.id === "string" && threadStartResponse.id.length > 0
|
|
1684
|
+
? threadStartResponse.id
|
|
1685
|
+
: undefined;
|
|
1686
|
+
appServerSession.sdkThreadId = threadStartResponse.result.thread.id;
|
|
1687
|
+
appServerSession.rolloutPath = threadStartResponse.result.thread.path;
|
|
1688
|
+
rememberThreadRolloutPath(threadId, threadStartResponse.result.thread.path);
|
|
1689
|
+
await updateDb
|
|
1690
|
+
.update(schema_js_1.threads)
|
|
1691
|
+
.set({
|
|
1692
|
+
status: "ready",
|
|
1693
|
+
sdkThreadId: threadStartResponse.result.thread.id,
|
|
1694
|
+
})
|
|
1695
|
+
.where((0, drizzle_orm_1.eq)(schema_js_1.threads.id, threadId));
|
|
1514
1696
|
}
|
|
1515
1697
|
catch (error) {
|
|
1516
1698
|
logger.warn(`Failed to mark thread '${threadId}' as ready: ${toErrorMessage(error)}`);
|
|
@@ -1527,7 +1709,7 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
|
|
|
1527
1709
|
updateClient.close();
|
|
1528
1710
|
}
|
|
1529
1711
|
logger.info(`Thread '${threadId}' created and ready.`);
|
|
1530
|
-
await sendThreadUpdate(commandChannel, threadId, protos_1.ThreadStatus.READY);
|
|
1712
|
+
await sendThreadUpdate(commandChannel, threadId, protos_1.ThreadStatus.READY, readyRequestId);
|
|
1531
1713
|
}
|
|
1532
1714
|
async function deleteThreadWithCleanup(cfg, request) {
|
|
1533
1715
|
const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
|
|
@@ -1742,6 +1924,7 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
|
|
|
1742
1924
|
let keepRuntimeWarm = false;
|
|
1743
1925
|
let shouldTrackTurnCompletion = trackTurnCompletion;
|
|
1744
1926
|
let enqueuedRequestTurnId = null;
|
|
1927
|
+
let turnCompletionWaitStarted = false;
|
|
1745
1928
|
try {
|
|
1746
1929
|
await (0, thread_runtime_js_1.ensureThreadRuntimeReady)({
|
|
1747
1930
|
dindContainer: threadState.dindContainer,
|
|
@@ -1827,14 +2010,18 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
|
|
|
1827
2010
|
if (!threadState.currentSdkTurnId) {
|
|
1828
2011
|
throw new Error(`Thread '${request.threadId}' is marked running but has no current SDK turn id.`);
|
|
1829
2012
|
}
|
|
2013
|
+
const activeSdkTurnId = threadState.currentSdkTurnId;
|
|
1830
2014
|
const steerParams = {
|
|
1831
2015
|
threadId: resolvedSdkThreadId,
|
|
1832
2016
|
input,
|
|
1833
|
-
expectedTurnId:
|
|
2017
|
+
expectedTurnId: activeSdkTurnId,
|
|
1834
2018
|
};
|
|
1835
2019
|
try {
|
|
1836
2020
|
const turnSteerResult = await appServer.steerTurn(steerParams);
|
|
1837
|
-
|
|
2021
|
+
if (turnSteerResult.turnId && turnSteerResult.turnId !== activeSdkTurnId) {
|
|
2022
|
+
logger.debug(`turn/steer returned turn '${turnSteerResult.turnId}' for thread '${request.threadId}', preserving active turn '${activeSdkTurnId}' as the canonical turn id.`);
|
|
2023
|
+
}
|
|
2024
|
+
sdkTurnId = activeSdkTurnId;
|
|
1838
2025
|
}
|
|
1839
2026
|
catch (error) {
|
|
1840
2027
|
if (!isNoActiveTurnSteerError(error)) {
|
|
@@ -1864,6 +2051,7 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
|
|
|
1864
2051
|
keepRuntimeWarm = true;
|
|
1865
2052
|
return;
|
|
1866
2053
|
}
|
|
2054
|
+
turnCompletionWaitStarted = true;
|
|
1867
2055
|
const terminalStatus = await waitForThreadTurnCompletion(cfg.state_db_path, appServer, commandChannel, request.threadId, sdkThreadId, sdkTurnId, logger, requestId);
|
|
1868
2056
|
await updateThreadTurnState(cfg, request.threadId, {
|
|
1869
2057
|
currentSdkTurnId: sdkTurnId,
|
|
@@ -1886,7 +2074,12 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
|
|
|
1886
2074
|
if (enqueuedRequestTurnId && requestId) {
|
|
1887
2075
|
await (0, thread_user_message_request_store_js_1.removePendingUserMessageRequestIdForTurn)(cfg.state_db_path, request.threadId, enqueuedRequestTurnId, requestId);
|
|
1888
2076
|
}
|
|
1889
|
-
if (
|
|
2077
|
+
if (turnCompletionWaitStarted && !isTurnCompletionTimeoutError(error)) {
|
|
2078
|
+
await updateThreadTurnState(cfg, request.threadId, {
|
|
2079
|
+
isCurrentTurnRunning: false,
|
|
2080
|
+
}).catch(() => undefined);
|
|
2081
|
+
}
|
|
2082
|
+
else if (startedFromIdle && !turnAccepted) {
|
|
1890
2083
|
await updateThreadTurnState(cfg, request.threadId, {
|
|
1891
2084
|
isCurrentTurnRunning: false,
|
|
1892
2085
|
}).catch(() => undefined);
|
|
@@ -1979,25 +2172,109 @@ function buildGrpcAuthCallOptions(secret) {
|
|
|
1979
2172
|
metadata.set("authorization", `Bearer ${secret}`);
|
|
1980
2173
|
return { metadata };
|
|
1981
2174
|
}
|
|
1982
|
-
|
|
2175
|
+
function isInternalDaemonChildProcess() {
|
|
2176
|
+
return process.env[daemon_js_1.DAEMON_CHILD_ENV] === "1";
|
|
2177
|
+
}
|
|
2178
|
+
function resolveEffectiveDaemonLogPath(cfg) {
|
|
2179
|
+
const envPath = process.env[daemon_js_1.DAEMON_LOG_PATH_ENV];
|
|
2180
|
+
if (envPath && envPath.trim().length > 0) {
|
|
2181
|
+
return envPath;
|
|
2182
|
+
}
|
|
2183
|
+
return (0, daemon_js_1.resolveDaemonLogPath)(cfg.state_db_path);
|
|
2184
|
+
}
|
|
2185
|
+
async function runDetachedDaemonProcess(options) {
|
|
2186
|
+
const cfg = buildRootConfig(options);
|
|
2187
|
+
const logPath = (0, daemon_js_1.resolveDaemonLogPath)(cfg.state_db_path);
|
|
2188
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(logPath), { recursive: true });
|
|
2189
|
+
const logFd = (0, node_fs_1.openSync)(logPath, "a");
|
|
2190
|
+
try {
|
|
2191
|
+
await new Promise((resolve, reject) => {
|
|
2192
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, process.argv.slice(1), {
|
|
2193
|
+
cwd: process.cwd(),
|
|
2194
|
+
detached: true,
|
|
2195
|
+
env: {
|
|
2196
|
+
...process.env,
|
|
2197
|
+
[daemon_js_1.DAEMON_CHILD_ENV]: "1",
|
|
2198
|
+
[daemon_js_1.DAEMON_LOG_PATH_ENV]: logPath,
|
|
2199
|
+
},
|
|
2200
|
+
stdio: ["ignore", logFd, logFd, "ipc"],
|
|
2201
|
+
windowsHide: true,
|
|
2202
|
+
});
|
|
2203
|
+
let settled = false;
|
|
2204
|
+
const timeout = setTimeout(() => {
|
|
2205
|
+
if (settled) {
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
settled = true;
|
|
2209
|
+
child.kill();
|
|
2210
|
+
reject(new Error(`Timed out waiting for daemon startup confirmation. See ${logPath}.`));
|
|
2211
|
+
}, DAEMON_STARTUP_TIMEOUT_MS);
|
|
2212
|
+
const finish = (callback) => {
|
|
2213
|
+
if (settled) {
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
settled = true;
|
|
2217
|
+
clearTimeout(timeout);
|
|
2218
|
+
callback();
|
|
2219
|
+
};
|
|
2220
|
+
child.once("error", (error) => {
|
|
2221
|
+
finish(() => reject(error));
|
|
2222
|
+
});
|
|
2223
|
+
child.once("exit", (code, signal) => {
|
|
2224
|
+
finish(() => {
|
|
2225
|
+
reject(new Error(`Daemon exited before startup completed (code=${code ?? "null"}, signal=${signal ?? "null"}). See ${logPath}.`));
|
|
2226
|
+
});
|
|
2227
|
+
});
|
|
2228
|
+
child.on("message", (message) => {
|
|
2229
|
+
if (!message || typeof message !== "object" || !("type" in message)) {
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
const type = message.type;
|
|
2233
|
+
if (type === "daemon-ready") {
|
|
2234
|
+
finish(() => {
|
|
2235
|
+
if (child.connected) {
|
|
2236
|
+
child.disconnect();
|
|
2237
|
+
}
|
|
2238
|
+
child.unref();
|
|
2239
|
+
console.log(`CompanyHelm daemon started (pid ${child.pid}). Logs: ${logPath}`);
|
|
2240
|
+
resolve();
|
|
2241
|
+
});
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
if (type === "daemon-error") {
|
|
2245
|
+
const messageValue = message.message;
|
|
2246
|
+
const daemonErrorMessage = typeof messageValue === "string" ? messageValue : `Daemon startup failed. See ${logPath}.`;
|
|
2247
|
+
finish(() => reject(new Error(daemonErrorMessage)));
|
|
2248
|
+
}
|
|
2249
|
+
});
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
finally {
|
|
2253
|
+
(0, node_fs_1.closeSync)(logFd);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
function sendDaemonParentMessage(message) {
|
|
2257
|
+
if (typeof process.send === "function") {
|
|
2258
|
+
process.send(message);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
async function runRootCommand(options, runtimeOptions) {
|
|
1983
2262
|
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
|
-
});
|
|
2263
|
+
const cfg = buildRootConfig(options);
|
|
1991
2264
|
const configuredSdks = await hasConfiguredSdks(cfg);
|
|
1992
2265
|
if (!configuredSdks && options.daemon) {
|
|
1993
2266
|
throw new Error("No SDKs configured. Daemon mode requires at least one configured SDK.");
|
|
1994
2267
|
}
|
|
1995
2268
|
if (!configuredSdks) {
|
|
1996
|
-
await (0, startup_js_1.startup)();
|
|
2269
|
+
await (0, startup_js_1.startup)(cfg);
|
|
1997
2270
|
}
|
|
1998
2271
|
await refreshCodexModelsForRegistration(cfg, logger);
|
|
1999
2272
|
const registerRequest = await buildRegisterRunnerRequest(cfg);
|
|
2000
2273
|
const apiCallOptions = buildGrpcAuthCallOptions(options.secret);
|
|
2274
|
+
if (options.daemon) {
|
|
2275
|
+
await (0, daemon_state_js_1.claimCurrentDaemonState)(cfg.state_db_path, process.pid, resolveEffectiveDaemonLogPath(cfg));
|
|
2276
|
+
runtimeOptions?.onDaemonReady?.();
|
|
2277
|
+
}
|
|
2001
2278
|
const commandMessageSink = new buffered_client_message_sender_js_1.BufferedClientMessageSender({
|
|
2002
2279
|
maxBufferedEvents: cfg.client_message_buffer_limit,
|
|
2003
2280
|
logger,
|
|
@@ -2032,7 +2309,14 @@ async function runRootCommand(options) {
|
|
|
2032
2309
|
logger.warn("CompanyHelm API command channel closed. Reconnecting...");
|
|
2033
2310
|
}
|
|
2034
2311
|
catch (error) {
|
|
2035
|
-
const failureMessage =
|
|
2312
|
+
const failureMessage = formatApiConnectionFailureMessage(error, cfg.companyhelm_api_url, options.secret);
|
|
2313
|
+
const diagnostics = formatApiConnectionFailureDiagnostics(error);
|
|
2314
|
+
if (diagnostics) {
|
|
2315
|
+
logger.debug(`CompanyHelm API failure diagnostics: ${diagnostics}`);
|
|
2316
|
+
}
|
|
2317
|
+
if (!isRetryableApiConnectionError(error)) {
|
|
2318
|
+
throw new Error(failureMessage);
|
|
2319
|
+
}
|
|
2036
2320
|
logger.warn(`CompanyHelm API connection attempt ${reconnectAttempt} failed: ${failureMessage}. ` +
|
|
2037
2321
|
"Retrying...");
|
|
2038
2322
|
}
|
|
@@ -2057,21 +2341,58 @@ async function runRootCommand(options) {
|
|
|
2057
2341
|
if (droppedMessages > 0) {
|
|
2058
2342
|
logger.warn(`Dropped ${droppedMessages} outbound client message(s) while command channel was disconnected.`);
|
|
2059
2343
|
}
|
|
2344
|
+
if (options.daemon) {
|
|
2345
|
+
await (0, daemon_state_js_1.clearCurrentDaemonState)(cfg.state_db_path, process.pid).catch((error) => {
|
|
2346
|
+
logger.warn(`Failed to clear daemon state: ${toErrorMessage(error)}`);
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2060
2349
|
await stopAllThreadAppServerSessions();
|
|
2061
2350
|
await stopAllThreadContainers(cfg, logger);
|
|
2062
2351
|
}
|
|
2063
2352
|
}
|
|
2353
|
+
function buildRootConfig(options) {
|
|
2354
|
+
return config_js_1.config.parse({
|
|
2355
|
+
config_directory: options.configPath,
|
|
2356
|
+
state_db_path: options.stateDbPath,
|
|
2357
|
+
companyhelm_api_url: options.serverUrl,
|
|
2358
|
+
agent_api_url: options.agentApiUrl,
|
|
2359
|
+
use_host_docker_runtime: options.useHostDockerRuntime,
|
|
2360
|
+
host_docker_path: options.hostDockerPath,
|
|
2361
|
+
thread_git_skills_directory: options.threadGitSkillsDirectory,
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2064
2364
|
function registerRootCommand(program) {
|
|
2065
2365
|
program
|
|
2366
|
+
.option("--config-path <path>", "Config directory override (defaults to ~/.config/companyhelm).")
|
|
2066
2367
|
.option("--server-url <url>", "CompanyHelm gRPC API URL override.")
|
|
2067
2368
|
.option("--agent-api-url <url>", "Agent gRPC API URL for companyhelm-agent in runtime containers (localhost is rewritten to http://host.docker.internal).")
|
|
2068
2369
|
.option("--secret <secret>", "Bearer secret used as gRPC Authorization header.")
|
|
2370
|
+
.option("--state-db-path <path>", "State database path override (defaults to ~/.local/share/companyhelm/state.db).")
|
|
2069
2371
|
.option("--use-host-docker-runtime", "Mount host Docker socket into runtime containers instead of creating DinD sidecars.")
|
|
2070
2372
|
.option("--host-docker-path <path>", "Host Docker endpoint when --use-host-docker-runtime is enabled (unix:///<socket-path> or tcp://localhost:<port>).")
|
|
2071
2373
|
.option("--thread-git-skills-directory <path>", "Container path where thread git skill repositories are cloned before linking into ~/.codex/skills.")
|
|
2072
2374
|
.option("-d, --daemon", "Run in daemon mode and fail fast when no SDK is configured.")
|
|
2073
2375
|
.option("--log-level <level>", "Log level (DEBUG, INFO, WARN, ERROR).", "INFO")
|
|
2074
2376
|
.action(async () => {
|
|
2075
|
-
|
|
2377
|
+
const options = program.opts();
|
|
2378
|
+
if (options.daemon && !isInternalDaemonChildProcess()) {
|
|
2379
|
+
await runDetachedDaemonProcess(options);
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
try {
|
|
2383
|
+
await runRootCommand(options, isInternalDaemonChildProcess()
|
|
2384
|
+
? {
|
|
2385
|
+
onDaemonReady: () => {
|
|
2386
|
+
sendDaemonParentMessage({ type: "daemon-ready" });
|
|
2387
|
+
},
|
|
2388
|
+
}
|
|
2389
|
+
: undefined);
|
|
2390
|
+
}
|
|
2391
|
+
catch (error) {
|
|
2392
|
+
if (isInternalDaemonChildProcess()) {
|
|
2393
|
+
sendDaemonParentMessage({ type: "daemon-error", message: toErrorMessage(error) });
|
|
2394
|
+
}
|
|
2395
|
+
throw error;
|
|
2396
|
+
}
|
|
2076
2397
|
});
|
|
2077
2398
|
}
|