@companyhelm/runner 0.1.1 → 0.2.0

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.
Files changed (38) hide show
  1. package/RUNTIME_IMAGE_VERSION +1 -1
  2. package/dist/commands/doctor.js +44 -0
  3. package/dist/commands/register-commands.js +2 -0
  4. package/dist/commands/root.js +172 -247
  5. package/dist/commands/runner/common.js +3 -1
  6. package/dist/commands/runner/start.js +3 -0
  7. package/dist/config.js +8 -2
  8. package/dist/preflight/check.js +2 -0
  9. package/dist/preflight/checks/linux/apparmor_restrict_unprivileged_userns_check.js +96 -0
  10. package/dist/preflight/entrypoints.js +53 -0
  11. package/dist/preflight/runner_preflight.js +56 -0
  12. package/dist/provisioning/host_provisioning/thread_metadata_store.js +249 -0
  13. package/dist/provisioning/host_provisioning/thread_metadata_types.js +2 -0
  14. package/dist/provisioning/host_provisioning/thread_workspace_provisioner.js +57 -0
  15. package/dist/provisioning/runtime_provisioning/script_renderer.js +120 -0
  16. package/dist/provisioning/runtime_provisioning/system_prompt.js +44 -0
  17. package/dist/provisioning/template_renderer.js +29 -0
  18. package/dist/service/companyhelm_api_client.js +0 -48
  19. package/dist/service/docker/app_server_container.js +16 -1
  20. package/dist/service/sdk/refresh_models.js +8 -0
  21. package/dist/service/thread_lifecycle.js +30 -41
  22. package/dist/service/thread_turn_state.js +1 -0
  23. package/dist/templates/provisioning/runtime_agent_metadata.sh.j2 +8 -0
  24. package/dist/templates/provisioning/runtime_bashrc.sh.j2 +7 -0
  25. package/dist/templates/provisioning/runtime_codex_config.sh.j2 +7 -0
  26. package/dist/templates/provisioning/runtime_git_config.sh.j2 +28 -0
  27. package/dist/templates/provisioning/runtime_identity.sh.j2 +65 -0
  28. package/dist/templates/provisioning/runtime_thread_git_skills_clone.sh.j2 +11 -0
  29. package/dist/templates/provisioning/runtime_thread_git_skills_link.sh.j2 +7 -0
  30. package/dist/templates/provisioning/runtime_tooling_validation.sh.j2 +32 -0
  31. package/dist/templates/system_prompts/common.md.j2 +37 -0
  32. package/dist/templates/system_prompts/dedicated_workspace.md.j2 +5 -0
  33. package/dist/templates/system_prompts/shared_workspace.md.j2 +6 -0
  34. package/dist/testing/vitest_reporter.js +23 -0
  35. package/dist/utils/daemon_startup_watchdog.js +27 -0
  36. package/package.json +2 -2
  37. package/dist/service/workspace_agents.js +0 -82
  38. package/dist/templates/runtime_agents.md.j2 +0 -50
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.toErrorMessage = toErrorMessage;
37
+ exports.formatWorkspaceStartupMessage = formatWorkspaceStartupMessage;
37
38
  exports.isRetryableApiConnectionError = isRetryableApiConnectionError;
38
39
  exports.formatApiConnectionFailureMessage = formatApiConnectionFailureMessage;
39
40
  exports.formatApiConnectionFailureDiagnostics = formatApiConnectionFailureDiagnostics;
@@ -42,6 +43,7 @@ exports.isNoActiveTurnSteerError = isNoActiveTurnSteerError;
42
43
  exports.isNoRunningTurnInterruptError = isNoRunningTurnInterruptError;
43
44
  exports.normalizeThreadAgentApiUrlForRuntime = normalizeThreadAgentApiUrlForRuntime;
44
45
  exports.extractThreadNameUpdateFromNotification = extractThreadNameUpdateFromNotification;
46
+ exports.extractThreadTokenUsageUpdateFromNotification = extractThreadTokenUsageUpdateFromNotification;
45
47
  exports.runCommandLoop = runCommandLoop;
46
48
  exports.isInternalDaemonChildProcess = isInternalDaemonChildProcess;
47
49
  exports.runDetachedDaemonProcess = runDetachedDaemonProcess;
@@ -74,19 +76,18 @@ const daemon_js_1 = require("../utils/daemon.js");
74
76
  const logger_js_1 = require("../utils/logger.js");
75
77
  const path_js_1 = require("../utils/path.js");
76
78
  const terminal_js_1 = require("../utils/terminal.js");
77
- const workspace_agents_js_1 = require("../service/workspace_agents.js");
79
+ const daemon_startup_watchdog_js_1 = require("../utils/daemon_startup_watchdog.js");
80
+ const thread_metadata_store_js_1 = require("../provisioning/host_provisioning/thread_metadata_store.js");
81
+ const thread_workspace_provisioner_js_1 = require("../provisioning/host_provisioning/thread_workspace_provisioner.js");
82
+ const system_prompt_js_1 = require("../provisioning/runtime_provisioning/system_prompt.js");
83
+ const entrypoints_js_1 = require("../preflight/entrypoints.js");
78
84
  const auth_js_1 = require("./sdk/codex/auth.js");
79
85
  const COMMAND_CHANNEL_CONNECT_RETRY_DELAY_MS = 1000;
80
86
  const COMMAND_CHANNEL_OPEN_TIMEOUT_MS = 5000;
81
87
  const TURN_COMPLETION_TIMEOUT_MS = 2 * 60 * 60000;
82
- const GITHUB_INSTALLATIONS_SYNC_INTERVAL_MS = 5 * 60000;
83
- const GITHUB_INSTALLATIONS_MIN_SYNC_INTERVAL_MS = 30000;
84
- const GITHUB_INSTALLATIONS_REFRESH_WINDOW_MS = 15 * 60000;
85
88
  const WORKSPACE_INSTALLATIONS_DIRECTORY = ".companyhelm";
86
- const WORKSPACE_INSTALLATIONS_FILENAME = "installations.json";
87
89
  const THREAD_GIT_SKILLS_CONFIG_FILENAME = "thread-git-skills.json";
88
90
  const THREAD_MCP_CONFIG_FILENAME = "thread-mcp.json";
89
- const THREAD_AGENT_CLI_CONFIG_FILENAME = "thread-agent-cli.json";
90
91
  const THREAD_MCP_BEARER_TOKEN_ENV_PREFIX = "COMPANYHELM_MCP_TOKEN_";
91
92
  const THREAD_MCP_AUTH_TYPE_BEARER_TOKEN = 2;
92
93
  const THREAD_MCP_STARTUP_TIMEOUT_SECONDS = 60;
@@ -95,7 +96,7 @@ const YOLO_SANDBOX_MODE = "danger-full-access";
95
96
  const YOLO_SANDBOX_POLICY = { type: "dangerFullAccess" };
96
97
  const DOCKER_INTERNAL_HOSTNAME = "host.docker.internal";
97
98
  const LOCALHOST_HOSTNAMES = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]);
98
- const DAEMON_STARTUP_TIMEOUT_MS = 15000;
99
+ const DAEMON_STARTUP_TIMEOUT_MS = 60000;
99
100
  class RootCommandInterruptedError extends Error {
100
101
  constructor(message = "Root command interrupted.") {
101
102
  super(message);
@@ -258,6 +259,20 @@ function normalizeReasoningLevels(value) {
258
259
  function toErrorMessage(error) {
259
260
  return error instanceof Error ? error.message : String(error);
260
261
  }
262
+ function formatWorkspaceStartupMessage(cfg) {
263
+ if (cfg.use_dedicated_workspaces) {
264
+ const workspacesDirectory = (0, thread_lifecycle_js_1.resolveThreadsRootDirectory)(cfg.config_directory, cfg.workspaces_directory);
265
+ return `Workspace modality: dedicated (workspaces dir: ${workspacesDirectory})`;
266
+ }
267
+ const workspaceDirectory = (0, thread_workspace_provisioner_js_1.resolveThreadWorkspaceDirectory)({
268
+ configDirectory: cfg.config_directory,
269
+ workspacesDirectory: cfg.workspaces_directory,
270
+ workspacePath: cfg.workspace_path,
271
+ useDedicatedWorkspaces: false,
272
+ threadId: "startup",
273
+ });
274
+ return `Workspace modality: shared (workspace: ${workspaceDirectory})`;
275
+ }
261
276
  function getGrpcStatusCode(error) {
262
277
  if (!error || typeof error !== "object" || !("code" in error)) {
263
278
  return undefined;
@@ -358,6 +373,36 @@ function normalizeNonEmptyString(value) {
358
373
  const trimmed = value.trim();
359
374
  return trimmed.length > 0 ? trimmed : undefined;
360
375
  }
376
+ function normalizeNonNegativeNumber(value) {
377
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
378
+ return undefined;
379
+ }
380
+ return Math.floor(value);
381
+ }
382
+ function resolveTokenUsageBreakdown(value) {
383
+ if (!isRecord(value)) {
384
+ return null;
385
+ }
386
+ const inputTokens = normalizeNonNegativeNumber(value.inputTokens ?? value.input_tokens);
387
+ const cachedInputTokens = normalizeNonNegativeNumber(value.cachedInputTokens ?? value.cached_input_tokens);
388
+ const outputTokens = normalizeNonNegativeNumber(value.outputTokens ?? value.output_tokens);
389
+ const reasoningOutputTokens = normalizeNonNegativeNumber(value.reasoningOutputTokens ?? value.reasoning_output_tokens);
390
+ const totalTokens = normalizeNonNegativeNumber(value.totalTokens ?? value.total_tokens);
391
+ if (inputTokens === undefined ||
392
+ cachedInputTokens === undefined ||
393
+ outputTokens === undefined ||
394
+ reasoningOutputTokens === undefined ||
395
+ totalTokens === undefined) {
396
+ return null;
397
+ }
398
+ return {
399
+ inputTokens,
400
+ cachedInputTokens,
401
+ outputTokens,
402
+ reasoningOutputTokens,
403
+ totalTokens,
404
+ };
405
+ }
361
406
  function rewriteLocalTargetForDockerRuntime(target) {
362
407
  const trimmed = target.trim();
363
408
  if (!trimmed) {
@@ -447,89 +492,34 @@ function extractThreadNameUpdateFromNotification(notification) {
447
492
  normalizeNonEmptyString(params.thread_name);
448
493
  return { sdkThreadId, threadName };
449
494
  }
450
- function isGrpcServiceError(error) {
451
- return Boolean(error && typeof error === "object" && "code" in error);
452
- }
453
- function isUnimplementedGrpcMethod(error) {
454
- return isGrpcServiceError(error) && error.code === grpc.status.UNIMPLEMENTED;
455
- }
456
- function normalizeAccessTokenExpiration(accessTokenExpiresUnixTimeMs) {
457
- const rawUnixTimeMs = Number(accessTokenExpiresUnixTimeMs);
458
- const expirationUnixTimeMs = Number.isFinite(rawUnixTimeMs) && rawUnixTimeMs > 0
459
- ? Math.floor(rawUnixTimeMs)
460
- : Date.now() + 60 * 60000;
461
- return {
462
- accessTokenExpiresUnixTimeMs: expirationUnixTimeMs.toString(),
463
- accessTokenExpiration: new Date(expirationUnixTimeMs).toISOString(),
464
- };
465
- }
466
- async function loadRuntimeGithubInstallations(apiClient, options, logger) {
467
- let installationIds = [];
468
- try {
469
- const listResponse = await apiClient.listGithubInstallationsForRunner(options);
470
- installationIds = listResponse.installations.map((installation) => installation.installationId);
495
+ function extractThreadTokenUsageUpdateFromNotification(notification) {
496
+ if (notification.method !== "thread/tokenUsage/updated") {
497
+ return null;
471
498
  }
472
- catch (error) {
473
- const warning = isUnimplementedGrpcMethod(error)
474
- ? "CompanyHelm API does not implement listGithubInstallationsForRunner yet."
475
- : `Failed to fetch GitHub installations: ${toErrorMessage(error)}`;
476
- logger.warn(warning);
477
- return [];
499
+ const rawParams = notification.params;
500
+ const sdkThreadId = normalizeNonEmptyString(rawParams.threadId) ??
501
+ normalizeNonEmptyString(rawParams.thread_id);
502
+ const sdkTurnId = normalizeNonEmptyString(rawParams.turnId) ??
503
+ normalizeNonEmptyString(rawParams.turn_id);
504
+ const tokenUsage = isRecord(rawParams.tokenUsage) ? rawParams.tokenUsage : rawParams.token_usage;
505
+ if (!sdkThreadId || !sdkTurnId || !isRecord(tokenUsage)) {
506
+ return null;
478
507
  }
479
- const installationDetails = [];
480
- for (const installationId of installationIds) {
481
- try {
482
- const accessTokenResponse = await apiClient.getGithubInstallationAccessTokenForRunner(installationId, options);
483
- const accessToken = accessTokenResponse.accessToken.trim();
484
- if (!accessToken) {
485
- logger.warn(`Received empty GitHub access token for installation ${installationId.toString()}; skipping.`);
486
- continue;
487
- }
488
- const expiration = normalizeAccessTokenExpiration(accessTokenResponse.accessTokenExpiresUnixTimeMs);
489
- const repositories = [...new Set(accessTokenResponse.repositories.filter((repository) => repository.trim().length > 0))]
490
- .sort((left, right) => left.localeCompare(right));
491
- installationDetails.push({
492
- installationId: accessTokenResponse.installationId.toString(),
493
- accessToken,
494
- accessTokenExpiresUnixTimeMs: expiration.accessTokenExpiresUnixTimeMs,
495
- accessTokenExpiration: expiration.accessTokenExpiration,
496
- repositories,
497
- });
498
- }
499
- catch (error) {
500
- const warning = isUnimplementedGrpcMethod(error)
501
- ? "CompanyHelm API does not implement getGithubInstallationAccessTokenForRunner yet."
502
- : `Failed to fetch GitHub access token for installation ${installationId.toString()}: ${toErrorMessage(error)}`;
503
- logger.warn(warning);
504
- }
508
+ const totalUsage = resolveTokenUsageBreakdown(tokenUsage.total);
509
+ const lastUsage = resolveTokenUsageBreakdown(tokenUsage.last);
510
+ if (!totalUsage || !lastUsage) {
511
+ return null;
505
512
  }
506
- return installationDetails;
507
- }
508
- function buildWorkspaceGithubInstallationsPayload(installations) {
509
513
  return {
510
- synced_at: new Date().toISOString(),
511
- installations: installations.map((installation) => ({
512
- installation_id: installation.installationId,
513
- access_token: installation.accessToken,
514
- access_token_expires_unix_time_ms: installation.accessTokenExpiresUnixTimeMs,
515
- access_token_expiration: installation.accessTokenExpiration,
516
- repositories: installation.repositories,
517
- })),
514
+ sdkThreadId,
515
+ sdkTurnId,
516
+ totalUsage,
517
+ lastUsage,
518
+ modelContextWindow: normalizeNonNegativeNumber(tokenUsage.modelContextWindow ?? tokenUsage.model_context_window) ?? null,
518
519
  };
519
520
  }
520
- function writeWorkspaceGithubInstallationsPayload(workspaceDirectory, payload, logger) {
521
- const installationsDirectory = (0, node_path_1.join)(workspaceDirectory, WORKSPACE_INSTALLATIONS_DIRECTORY);
522
- const installationsPath = (0, node_path_1.join)(installationsDirectory, WORKSPACE_INSTALLATIONS_FILENAME);
523
- const temporaryPath = `${installationsPath}.tmp`;
524
- const serializedPayload = `${JSON.stringify(payload, null, 2)}\n`;
525
- try {
526
- (0, node_fs_1.mkdirSync)(installationsDirectory, { recursive: true });
527
- (0, node_fs_1.writeFileSync)(temporaryPath, serializedPayload, "utf8");
528
- (0, node_fs_1.renameSync)(temporaryPath, installationsPath);
529
- }
530
- catch (error) {
531
- logger.warn(`Failed writing GitHub installations file for workspace '${workspaceDirectory}': ${toErrorMessage(error)}`);
532
- }
521
+ function isGrpcServiceError(error) {
522
+ return Boolean(error && typeof error === "object" && "code" in error);
533
523
  }
534
524
  function isHttpsRepositoryUrl(value) {
535
525
  try {
@@ -848,65 +838,6 @@ function readWorkspaceThreadMcpConfig(workspaceDirectory, logger) {
848
838
  return [];
849
839
  }
850
840
  }
851
- function resolveThreadAgentCliConfigPath(workspaceDirectory) {
852
- return (0, node_path_1.join)(workspaceDirectory, WORKSPACE_INSTALLATIONS_DIRECTORY, THREAD_AGENT_CLI_CONFIG_FILENAME);
853
- }
854
- function parseThreadAgentCliConfig(content) {
855
- if (!isRecord(content)) {
856
- return null;
857
- }
858
- const agentApiUrl = normalizeNonEmptyString(content.agent_api_url);
859
- const token = normalizeNonEmptyString(content.token);
860
- if (!agentApiUrl || !token) {
861
- return null;
862
- }
863
- return {
864
- agent_api_url: agentApiUrl,
865
- token,
866
- };
867
- }
868
- function writeWorkspaceThreadAgentCliConfig(workspaceDirectory, cliSecret, agentApiUrl, logger) {
869
- const configPath = resolveThreadAgentCliConfigPath(workspaceDirectory);
870
- const configDirectory = (0, node_path_1.join)(workspaceDirectory, WORKSPACE_INSTALLATIONS_DIRECTORY);
871
- const temporaryPath = `${configPath}.tmp`;
872
- try {
873
- (0, node_fs_1.mkdirSync)(configDirectory, { recursive: true });
874
- if (cliSecret.length === 0) {
875
- (0, node_fs_1.rmSync)(configPath, { force: true });
876
- (0, node_fs_1.rmSync)(temporaryPath, { force: true });
877
- return;
878
- }
879
- const payload = {
880
- agent_api_url: normalizeThreadAgentApiUrlForRuntime(agentApiUrl),
881
- token: cliSecret,
882
- };
883
- (0, node_fs_1.writeFileSync)(temporaryPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
884
- (0, node_fs_1.renameSync)(temporaryPath, configPath);
885
- }
886
- catch (error) {
887
- logger.warn(`Failed writing thread agent CLI config for workspace '${workspaceDirectory}': ${toErrorMessage(error)}`);
888
- }
889
- }
890
- function readWorkspaceThreadAgentCliConfig(workspaceDirectory, logger) {
891
- const configPath = resolveThreadAgentCliConfigPath(workspaceDirectory);
892
- try {
893
- const rawContent = (0, node_fs_1.readFileSync)(configPath, "utf8");
894
- const parsedContent = JSON.parse(rawContent);
895
- const parsedConfig = parseThreadAgentCliConfig(parsedContent);
896
- if (!parsedConfig) {
897
- logger.warn(`Thread agent CLI config has invalid shape at '${configPath}'.`);
898
- return null;
899
- }
900
- return parsedConfig;
901
- }
902
- catch (error) {
903
- if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
904
- return null;
905
- }
906
- logger.warn(`Failed reading thread agent CLI config at '${configPath}': ${toErrorMessage(error)}`);
907
- return null;
908
- }
909
- }
910
841
  function escapeTomlString(value) {
911
842
  return JSON.stringify(value);
912
843
  }
@@ -1056,7 +987,7 @@ function readWorkspaceThreadGitSkillsConfig(workspaceDirectory, logger) {
1056
987
  }
1057
988
  }
1058
989
  async function ensureThreadGitSkillsInRuntime(cfg, threadState, containerService, logger) {
1059
- const packages = readWorkspaceThreadGitSkillsConfig(threadState.workspace, logger);
990
+ const packages = new thread_metadata_store_js_1.ThreadMetadataStore(cfg.config_directory, logger).readThreadGitSkillsConfig(threadState.id);
1060
991
  if (packages.length === 0) {
1061
992
  return;
1062
993
  }
@@ -1104,8 +1035,10 @@ async function reconcileThreadRunningStateBeforeUserMessage(cfg, threadState, lo
1104
1035
  isCurrentTurnRunning: false,
1105
1036
  };
1106
1037
  }
1107
- const threadMcpSetup = buildThreadCodexMcpSetup(readWorkspaceThreadMcpConfig(threadState.workspace, logger));
1108
- const threadAgentCliConfig = readWorkspaceThreadAgentCliConfig(threadState.workspace, logger);
1038
+ const metadataStore = new thread_metadata_store_js_1.ThreadMetadataStore(cfg.config_directory, logger);
1039
+ const persistedThreadMcpServers = metadataStore.readThreadMcpConfig(threadState.id);
1040
+ const persistedThreadGitSkillPackages = metadataStore.readThreadGitSkillsConfig(threadState.id);
1041
+ const threadMcpSetup = buildThreadCodexMcpSetup(persistedThreadMcpServers);
1109
1042
  const appServerSession = await getOrCreateThreadAppServerSession(threadState.id, threadState.runtimeContainer, threadMcpSetup.appServerEnv, cfg.codex.app_server_client_name, logger);
1110
1043
  const runtimeUser = buildThreadRuntimeUser(cfg, threadState);
1111
1044
  await (0, thread_runtime_js_1.ensureThreadRuntimeReady)({
@@ -1117,9 +1050,10 @@ async function reconcileThreadRunningStateBeforeUserMessage(cfg, threadState, lo
1117
1050
  user: runtimeUser,
1118
1051
  });
1119
1052
  await ensureThreadGitSkillsInRuntime(cfg, threadState, containerService, logger);
1120
- if (threadAgentCliConfig) {
1121
- await containerService.ensureRuntimeContainerAgentCliConfig(threadState.runtimeContainer, runtimeUser, threadAgentCliConfig);
1122
- }
1053
+ await containerService.ensureRuntimeContainerThreadMetadata(threadState.runtimeContainer, runtimeUser, {
1054
+ mcpServers: persistedThreadMcpServers,
1055
+ gitSkillPackages: persistedThreadGitSkillPackages,
1056
+ });
1123
1057
  if (!appServerSession.started) {
1124
1058
  await containerService.ensureRuntimeContainerCodexConfig(threadState.runtimeContainer, runtimeUser, threadMcpSetup.configToml);
1125
1059
  }
@@ -1151,49 +1085,6 @@ async function reconcileThreadRunningStateBeforeUserMessage(cfg, threadState, lo
1151
1085
  isCurrentTurnRunning: false,
1152
1086
  };
1153
1087
  }
1154
- async function listTrackedThreadWorkspaces(cfg, logger) {
1155
- const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
1156
- try {
1157
- const rows = await db.select({ workspace: schema_js_1.threads.workspace }).from(schema_js_1.threads);
1158
- return [...new Set(rows.map((row) => row.workspace.trim()).filter((workspace) => workspace.length > 0))];
1159
- }
1160
- catch (error) {
1161
- logger.warn(`Failed to list tracked thread workspaces for GitHub installation sync: ${toErrorMessage(error)}`);
1162
- return [];
1163
- }
1164
- finally {
1165
- client.close();
1166
- }
1167
- }
1168
- function resolveGithubInstallationsSyncDelayMs(installations) {
1169
- let syncDelayMs = GITHUB_INSTALLATIONS_SYNC_INTERVAL_MS;
1170
- const now = Date.now();
1171
- for (const installation of installations) {
1172
- const expirationUnixTimeMs = Number(installation.accessTokenExpiresUnixTimeMs);
1173
- if (!Number.isFinite(expirationUnixTimeMs) || expirationUnixTimeMs <= 0) {
1174
- continue;
1175
- }
1176
- const refreshInMs = expirationUnixTimeMs - now - GITHUB_INSTALLATIONS_REFRESH_WINDOW_MS;
1177
- const boundedRefreshDelayMs = Math.max(GITHUB_INSTALLATIONS_MIN_SYNC_INTERVAL_MS, Math.min(GITHUB_INSTALLATIONS_SYNC_INTERVAL_MS, refreshInMs));
1178
- syncDelayMs = Math.min(syncDelayMs, boundedRefreshDelayMs);
1179
- }
1180
- return Math.max(GITHUB_INSTALLATIONS_MIN_SYNC_INTERVAL_MS, Math.min(GITHUB_INSTALLATIONS_SYNC_INTERVAL_MS, syncDelayMs));
1181
- }
1182
- async function syncGithubInstallationsForWorkspaces(apiClient, options, workspaceDirectories, logger) {
1183
- const uniqueWorkspaces = [
1184
- ...new Set(workspaceDirectories.map((workspace) => workspace.trim()).filter((workspace) => workspace.length > 0)),
1185
- ];
1186
- if (uniqueWorkspaces.length === 0) {
1187
- return [];
1188
- }
1189
- const installations = await loadRuntimeGithubInstallations(apiClient, options, logger);
1190
- const payload = buildWorkspaceGithubInstallationsPayload(installations);
1191
- for (const workspaceDirectory of uniqueWorkspaces) {
1192
- writeWorkspaceGithubInstallationsPayload(workspaceDirectory, payload, logger);
1193
- }
1194
- logger.debug(`Synced ${installations.length} GitHub installation token(s) to ${uniqueWorkspaces.length} workspace(s).`);
1195
- return installations;
1196
- }
1197
1088
  async function waitForAbort(signal, delayMs) {
1198
1089
  if (signal.aborted) {
1199
1090
  return;
@@ -1211,21 +1102,6 @@ async function waitForAbort(signal, delayMs) {
1211
1102
  signal.addEventListener("abort", handleAbort);
1212
1103
  });
1213
1104
  }
1214
- async function runGithubInstallationsSyncLoop(cfg, apiClient, options, logger, signal) {
1215
- while (!signal.aborted) {
1216
- let nextDelayMs = GITHUB_INSTALLATIONS_SYNC_INTERVAL_MS;
1217
- try {
1218
- const workspaces = await listTrackedThreadWorkspaces(cfg, logger);
1219
- const installations = await syncGithubInstallationsForWorkspaces(apiClient, options, workspaces, logger);
1220
- nextDelayMs = resolveGithubInstallationsSyncDelayMs(installations);
1221
- }
1222
- catch (error) {
1223
- logger.warn(`GitHub installation sync loop iteration failed: ${toErrorMessage(error)}`);
1224
- nextDelayMs = GITHUB_INSTALLATIONS_MIN_SYNC_INTERVAL_MS;
1225
- }
1226
- await waitForAbort(signal, nextDelayMs);
1227
- }
1228
- }
1229
1105
  function normalizeReasoningEffort(value) {
1230
1106
  if (!value) {
1231
1107
  return null;
@@ -1243,8 +1119,14 @@ function normalizeAdditionalModelInstructions(value) {
1243
1119
  const trimmed = value.trim();
1244
1120
  return trimmed.length > 0 ? trimmed : null;
1245
1121
  }
1246
- function buildThreadDeveloperInstructions(additionalModelInstructions) {
1247
- return normalizeAdditionalModelInstructions(additionalModelInstructions);
1122
+ function buildThreadDeveloperInstructions(threadId, cfg, additionalModelInstructions, cliSecret) {
1123
+ return (0, system_prompt_js_1.buildCodexDeveloperInstructions)(additionalModelInstructions, {
1124
+ homeDirectory: cfg.agent_home_directory,
1125
+ agentApiUrl: normalizeThreadAgentApiUrlForRuntime(cfg.agent_api_url),
1126
+ agentToken: normalizeNonEmptyString(cliSecret) ?? "<thread-secret>",
1127
+ threadId,
1128
+ workspaceMode: cfg.use_dedicated_workspaces ? "dedicated" : "shared",
1129
+ });
1248
1130
  }
1249
1131
  function buildUserTextInput(text) {
1250
1132
  return [
@@ -1493,6 +1375,32 @@ async function sendThreadNameUpdate(commandChannel, threadId, threadName) {
1493
1375
  });
1494
1376
  await commandChannel.send(message);
1495
1377
  }
1378
+ function toProtoTokenUsageBreakdown(usage) {
1379
+ return {
1380
+ inputTokens: BigInt(usage.inputTokens),
1381
+ cachedInputTokens: BigInt(usage.cachedInputTokens),
1382
+ outputTokens: BigInt(usage.outputTokens),
1383
+ reasoningOutputTokens: BigInt(usage.reasoningOutputTokens),
1384
+ totalTokens: BigInt(usage.totalTokens),
1385
+ };
1386
+ }
1387
+ async function sendThreadTokenUsageUpdate(commandChannel, threadId, tokenUsageUpdate) {
1388
+ const message = (0, protobuf_1.create)(protos_1.ClientMessageSchema, {
1389
+ payload: {
1390
+ case: "threadTokenUsageUpdate",
1391
+ value: {
1392
+ threadId,
1393
+ sdkTurnId: tokenUsageUpdate.sdkTurnId,
1394
+ totalUsage: toProtoTokenUsageBreakdown(tokenUsageUpdate.totalUsage),
1395
+ lastUsage: toProtoTokenUsageBreakdown(tokenUsageUpdate.lastUsage),
1396
+ modelContextWindow: tokenUsageUpdate.modelContextWindow === null
1397
+ ? undefined
1398
+ : BigInt(tokenUsageUpdate.modelContextWindow),
1399
+ },
1400
+ },
1401
+ });
1402
+ await commandChannel.send(message);
1403
+ }
1496
1404
  async function sendTurnExecutionUpdate(commandChannel, threadId, sdkTurnId, status, requestId) {
1497
1405
  const message = (0, protobuf_1.create)(protos_1.ClientMessageSchema, {
1498
1406
  requestId,
@@ -1598,16 +1506,22 @@ async function loadCodexSdkState(cfg) {
1598
1506
  client.close();
1599
1507
  }
1600
1508
  }
1601
- async function refreshCodexModelsForRegistration(cfg, logger) {
1509
+ async function refreshCodexModelsForRegistration(cfg, logger, reportProgress) {
1602
1510
  const codexSdk = await loadCodexSdkState(cfg);
1603
1511
  if (!codexSdk || codexSdk.status !== "configured" || codexSdk.authentication === "unauthenticated") {
1604
1512
  logger.info("Codex is not configured; registering runner with unconfigured Codex SDK state.");
1605
1513
  return null;
1606
1514
  }
1607
1515
  try {
1608
- const results = await (0, refresh_models_js_1.refreshSdkModels)({ sdk: "codex", logger });
1516
+ reportProgress?.("Refreshing Codex models from the local app-server.");
1517
+ const results = await (0, refresh_models_js_1.refreshSdkModels)({
1518
+ sdk: "codex",
1519
+ logger,
1520
+ imageStatusReporter: reportProgress,
1521
+ });
1609
1522
  const modelCount = results[0]?.modelCount ?? 0;
1610
1523
  logger.info(`Refreshed Codex models from container app-server (${modelCount} models).`);
1524
+ reportProgress?.(`Refreshed Codex models from container app-server (${modelCount} models).`);
1611
1525
  return null;
1612
1526
  }
1613
1527
  catch (error) {
@@ -1679,7 +1593,9 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
1679
1593
  return;
1680
1594
  }
1681
1595
  const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
1682
- const threadDirectory = (0, thread_lifecycle_js_1.resolveThreadDirectory)(cfg.config_directory, cfg.workspaces_directory, threadId);
1596
+ const workspaceProvisioner = new thread_workspace_provisioner_js_1.ThreadWorkspaceProvisioner(cfg.config_directory, cfg.workspaces_directory, cfg.workspace_path, cfg.use_dedicated_workspaces);
1597
+ const metadataStore = new thread_metadata_store_js_1.ThreadMetadataStore(cfg.config_directory, logger);
1598
+ const threadDirectory = workspaceProvisioner.resolveWorkspaceDirectory(threadId);
1683
1599
  const containerNames = (0, thread_lifecycle_js_1.buildThreadContainerNames)(threadId);
1684
1600
  const hostInfo = (0, host_js_1.getHostInfo)(cfg.codex.codex_auth_path);
1685
1601
  const normalizedAdditionalModelInstructions = normalizeAdditionalModelInstructions(request.additionalModelInstructions);
@@ -1741,12 +1657,9 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
1741
1657
  finally {
1742
1658
  client.close();
1743
1659
  }
1744
- (0, node_fs_1.mkdirSync)(threadDirectory, { recursive: true });
1745
- (0, workspace_agents_js_1.ensureWorkspaceAgentsMd)(threadDirectory, cfg.agent_home_directory);
1746
- writeWorkspaceThreadGitSkillsConfig(threadDirectory, threadGitSkillPackages, logger);
1747
- writeWorkspaceThreadMcpConfig(threadDirectory, threadMcpServers, logger);
1748
- writeWorkspaceThreadAgentCliConfig(threadDirectory, cliSecret, cfg.agent_api_url, logger);
1749
- await syncGithubInstallationsForWorkspaces(apiClient, apiCallOptions, [threadDirectory], logger);
1660
+ workspaceProvisioner.ensureWorkspaceDirectory(threadId);
1661
+ metadataStore.writeThreadGitSkillsConfig(threadId, threadGitSkillPackages);
1662
+ metadataStore.writeThreadMcpConfig(threadId, threadMcpServers);
1750
1663
  logger.debug(`Thread '${threadId}' workspace initialized at '${threadDirectory}'.`);
1751
1664
  const containerService = new thread_lifecycle_js_1.ThreadContainerService();
1752
1665
  const mounts = (0, thread_lifecycle_js_1.buildSharedThreadMounts)({
@@ -1796,8 +1709,9 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
1796
1709
  if (!threadState) {
1797
1710
  throw new Error(`Thread '${threadId}' disappeared before SDK bootstrap.`);
1798
1711
  }
1799
- const threadMcpSetup = buildThreadCodexMcpSetup(readWorkspaceThreadMcpConfig(threadState.workspace, logger));
1800
- const threadAgentCliConfig = readWorkspaceThreadAgentCliConfig(threadState.workspace, logger);
1712
+ const persistedThreadMcpServers = metadataStore.readThreadMcpConfig(threadState.id);
1713
+ const persistedThreadGitSkillPackages = metadataStore.readThreadGitSkillsConfig(threadState.id);
1714
+ const threadMcpSetup = buildThreadCodexMcpSetup(persistedThreadMcpServers);
1801
1715
  const appServerSession = await getOrCreateThreadAppServerSession(threadId, threadState.runtimeContainer, threadMcpSetup.appServerEnv, cfg.codex.app_server_client_name, logger);
1802
1716
  const runtimeUser = {
1803
1717
  uid: threadState.uid,
@@ -1814,14 +1728,15 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
1814
1728
  user: runtimeUser,
1815
1729
  });
1816
1730
  await ensureThreadGitSkillsInRuntime(cfg, threadState, containerService, logger);
1817
- if (threadAgentCliConfig) {
1818
- await containerService.ensureRuntimeContainerAgentCliConfig(threadState.runtimeContainer, runtimeUser, threadAgentCliConfig);
1819
- }
1731
+ await containerService.ensureRuntimeContainerThreadMetadata(threadState.runtimeContainer, runtimeUser, {
1732
+ mcpServers: persistedThreadMcpServers,
1733
+ gitSkillPackages: persistedThreadGitSkillPackages,
1734
+ });
1820
1735
  if (!appServerSession.started) {
1821
1736
  await containerService.ensureRuntimeContainerCodexConfig(threadState.runtimeContainer, runtimeUser, threadMcpSetup.configToml);
1822
1737
  }
1823
1738
  await ensureThreadAppServerSessionStarted(appServerSession);
1824
- const developerInstructions = buildThreadDeveloperInstructions(threadState.additionalModelInstructions);
1739
+ const developerInstructions = buildThreadDeveloperInstructions(threadId, cfg, threadState.additionalModelInstructions, threadState.cliSecret);
1825
1740
  logger.debug(`Starting app-server thread '${threadId}' with developer instructions: ${JSON.stringify(developerInstructions)}.`);
1826
1741
  const threadStartResponse = await appServerSession.appServer.startThreadWithResponse({
1827
1742
  model: threadState.model,
@@ -1904,6 +1819,7 @@ async function deleteThreadWithCleanup(cfg, request) {
1904
1819
  const containerService = new thread_lifecycle_js_1.ThreadContainerService();
1905
1820
  try {
1906
1821
  const containerNames = (0, thread_lifecycle_js_1.buildThreadContainerNames)(existingThread.id);
1822
+ const workspaceProvisioner = new thread_workspace_provisioner_js_1.ThreadWorkspaceProvisioner(cfg.config_directory, cfg.workspaces_directory, cfg.workspace_path, cfg.use_dedicated_workspaces);
1907
1823
  await stopThreadAppServerSession(request.threadId);
1908
1824
  threadRolloutPaths.delete(request.threadId);
1909
1825
  await containerService.forceRemoveContainer(existingThread.runtimeContainer);
@@ -1912,7 +1828,8 @@ async function deleteThreadWithCleanup(cfg, request) {
1912
1828
  }
1913
1829
  await containerService.forceRemoveVolume(containerNames.home);
1914
1830
  await containerService.forceRemoveVolume(containerNames.tmp);
1915
- removeWorkspaceDirectory(existingThread.workspace);
1831
+ workspaceProvisioner.removeWorkspaceDirectory(existingThread.id, existingThread.workspace);
1832
+ new thread_metadata_store_js_1.ThreadMetadataStore(cfg.config_directory, (0, logger_js_1.createLogger)("ERROR")).removeThreadMetadata(existingThread.id);
1916
1833
  }
1917
1834
  catch (error) {
1918
1835
  return {
@@ -2031,6 +1948,12 @@ async function waitForThreadTurnCompletion(stateDbPath, appServer, commandChanne
2031
1948
  receivedThreadNameUpdate = true;
2032
1949
  await sendThreadNameUpdate(commandChannel, threadId, threadNameUpdate.threadName);
2033
1950
  }
1951
+ const tokenUsageUpdate = extractThreadTokenUsageUpdateFromNotification(notification);
1952
+ if (tokenUsageUpdate &&
1953
+ tokenUsageUpdate.sdkThreadId === sdkThreadId &&
1954
+ tokenUsageUpdate.sdkTurnId === sdkTurnId) {
1955
+ await sendThreadTokenUsageUpdate(commandChannel, threadId, tokenUsageUpdate);
1956
+ }
2034
1957
  if (notification.method === "item/started" &&
2035
1958
  notification.params.threadId === sdkThreadId &&
2036
1959
  notification.params.turnId === sdkTurnId) {
@@ -2071,8 +1994,10 @@ async function waitForThreadTurnCompletion(stateDbPath, appServer, commandChanne
2071
1994
  }
2072
1995
  async function executeCreateUserMessageRequest(cfg, commandChannel, request, requestId, threadState, startedFromIdle, trackTurnCompletion, logger) {
2073
1996
  const containerService = new thread_lifecycle_js_1.ThreadContainerService();
2074
- const threadMcpSetup = buildThreadCodexMcpSetup(readWorkspaceThreadMcpConfig(threadState.workspace, logger));
2075
- const threadAgentCliConfig = readWorkspaceThreadAgentCliConfig(threadState.workspace, logger);
1997
+ const metadataStore = new thread_metadata_store_js_1.ThreadMetadataStore(cfg.config_directory, logger);
1998
+ const persistedThreadMcpServers = metadataStore.readThreadMcpConfig(threadState.id);
1999
+ const persistedThreadGitSkillPackages = metadataStore.readThreadGitSkillsConfig(threadState.id);
2000
+ const threadMcpSetup = buildThreadCodexMcpSetup(persistedThreadMcpServers);
2076
2001
  const appServerSession = await getOrCreateThreadAppServerSession(request.threadId, threadState.runtimeContainer, threadMcpSetup.appServerEnv, cfg.codex.app_server_client_name, logger);
2077
2002
  const appServer = appServerSession.appServer;
2078
2003
  const runtimeUser = buildThreadRuntimeUser(cfg, threadState);
@@ -2093,9 +2018,10 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
2093
2018
  user: runtimeUser,
2094
2019
  });
2095
2020
  await ensureThreadGitSkillsInRuntime(cfg, threadState, containerService, logger);
2096
- if (threadAgentCliConfig) {
2097
- await containerService.ensureRuntimeContainerAgentCliConfig(threadState.runtimeContainer, runtimeUser, threadAgentCliConfig);
2098
- }
2021
+ await containerService.ensureRuntimeContainerThreadMetadata(threadState.runtimeContainer, runtimeUser, {
2022
+ mcpServers: persistedThreadMcpServers,
2023
+ gitSkillPackages: persistedThreadGitSkillPackages,
2024
+ });
2099
2025
  if (!appServerSession.started) {
2100
2026
  await containerService.ensureRuntimeContainerCodexConfig(threadState.runtimeContainer, runtimeUser, threadMcpSetup.configToml);
2101
2027
  }
@@ -2119,7 +2045,7 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
2119
2045
  await updateThreadTurnState(cfg, request.threadId, { sdkThreadId });
2120
2046
  }
2121
2047
  else {
2122
- const developerInstructions = buildThreadDeveloperInstructions(threadState.additionalModelInstructions);
2048
+ const developerInstructions = buildThreadDeveloperInstructions(request.threadId, cfg, threadState.additionalModelInstructions, threadState.cliSecret);
2123
2049
  const threadStartParams = {
2124
2050
  model: request.model ?? threadState.model,
2125
2051
  modelProvider: null,
@@ -2498,20 +2424,20 @@ async function runDetachedDaemonProcess(options) {
2498
2424
  windowsHide: true,
2499
2425
  });
2500
2426
  let settled = false;
2501
- const timeout = setTimeout(() => {
2427
+ const startupWatchdog = new daemon_startup_watchdog_js_1.DaemonStartupWatchdog(DAEMON_STARTUP_TIMEOUT_MS, () => {
2502
2428
  if (settled) {
2503
2429
  return;
2504
2430
  }
2505
2431
  settled = true;
2506
2432
  child.kill();
2507
2433
  reject(new Error(`Timed out waiting for daemon startup confirmation. See ${logPath}.`));
2508
- }, DAEMON_STARTUP_TIMEOUT_MS);
2434
+ });
2509
2435
  const finish = (callback) => {
2510
2436
  if (settled) {
2511
2437
  return;
2512
2438
  }
2513
2439
  settled = true;
2514
- clearTimeout(timeout);
2440
+ startupWatchdog.finish();
2515
2441
  callback();
2516
2442
  };
2517
2443
  child.once("error", (error) => {
@@ -2527,6 +2453,10 @@ async function runDetachedDaemonProcess(options) {
2527
2453
  return;
2528
2454
  }
2529
2455
  const type = message.type;
2456
+ if (type === "daemon-progress") {
2457
+ startupWatchdog.bump();
2458
+ return;
2459
+ }
2530
2460
  if (type === "daemon-ready") {
2531
2461
  finish(() => {
2532
2462
  if (child.connected) {
@@ -2558,11 +2488,13 @@ function sendDaemonParentMessage(message) {
2558
2488
  async function runRootCommand(options, runtimeOptions) {
2559
2489
  const logger = (0, logger_js_1.createLogger)(options.logLevel ?? "INFO", { daemonMode: options.daemon ?? false });
2560
2490
  const cfg = buildRootConfig(options);
2491
+ logger.info(formatWorkspaceStartupMessage(cfg));
2492
+ await (0, entrypoints_js_1.ensureRunnerStartupPreflight)(cfg);
2561
2493
  await (0, auth_js_1.ensureCodexRunnerStartState)(cfg, {
2562
2494
  useDedicatedAuth: options.useDedicatedAuth,
2563
2495
  logInfo: (message) => logger.info(message),
2564
2496
  });
2565
- const codexRefreshErrorMessage = await refreshCodexModelsForRegistration(cfg, logger);
2497
+ const codexRefreshErrorMessage = await refreshCodexModelsForRegistration(cfg, logger, runtimeOptions?.onDaemonProgress);
2566
2498
  const registerRequest = await buildRegisterRunnerRequest(cfg, logger, codexRefreshErrorMessage);
2567
2499
  const apiCallOptions = buildGrpcAuthCallOptions(options.secret);
2568
2500
  if (options.daemon) {
@@ -2587,8 +2519,6 @@ async function runRootCommand(options, runtimeOptions) {
2587
2519
  const apiClient = new companyhelm_api_client_js_1.CompanyhelmApiClient({ apiUrl: cfg.companyhelm_api_url, logger });
2588
2520
  activeApiClient = apiClient;
2589
2521
  let commandChannel = null;
2590
- let githubInstallationsSyncAbortController = null;
2591
- let githubInstallationsSyncTask = null;
2592
2522
  try {
2593
2523
  reconnectAttempt += 1;
2594
2524
  commandChannel = await apiClient.connect(registerRequest, apiCallOptions);
@@ -2603,12 +2533,6 @@ async function runRootCommand(options, runtimeOptions) {
2603
2533
  logger.info(`Connected to CompanyHelm API at ${cfg.companyhelm_api_url}`);
2604
2534
  }
2605
2535
  reconnectAttempt = 0;
2606
- githubInstallationsSyncAbortController = new AbortController();
2607
- githubInstallationsSyncTask = runGithubInstallationsSyncLoop(cfg, apiClient, apiCallOptions, logger, githubInstallationsSyncAbortController.signal).catch((error) => {
2608
- if (!githubInstallationsSyncAbortController?.signal.aborted) {
2609
- logger.warn(`GitHub installation sync loop exited unexpectedly: ${toErrorMessage(error)}`);
2610
- }
2611
- });
2612
2536
  await raceWithAbort(runCommandLoop(cfg, commandChannel, commandMessageSink, apiClient, apiCallOptions, logger), interruptState.signal);
2613
2537
  logger.warn("CompanyHelm API command channel closed. Reconnecting...");
2614
2538
  }
@@ -2628,10 +2552,6 @@ async function runRootCommand(options, runtimeOptions) {
2628
2552
  "Retrying...");
2629
2553
  }
2630
2554
  finally {
2631
- if (githubInstallationsSyncAbortController) {
2632
- githubInstallationsSyncAbortController.abort();
2633
- }
2634
- void githubInstallationsSyncTask;
2635
2555
  if (commandChannel) {
2636
2556
  commandChannel.cancel();
2637
2557
  commandMessageSink.unbind(commandChannel);
@@ -2666,8 +2586,13 @@ async function runRootCommand(options, runtimeOptions) {
2666
2586
  }
2667
2587
  }
2668
2588
  function buildRootConfig(options) {
2589
+ if (options.useDedicatedWorkspaces && typeof options.workspacePath === "string" && options.workspacePath.trim().length > 0) {
2590
+ throw new Error("--workspace-path and --use-dedicated-workspaces cannot be used together.");
2591
+ }
2669
2592
  return config_js_1.config.parse({
2670
2593
  config_directory: options.configPath,
2594
+ workspace_path: options.workspacePath,
2595
+ use_dedicated_workspaces: options.useDedicatedWorkspaces,
2671
2596
  state_db_path: options.stateDbPath,
2672
2597
  companyhelm_api_url: options.serverUrl,
2673
2598
  agent_api_url: options.agentApiUrl,