@companyhelm/runner 0.1.3 → 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.
@@ -1 +1 @@
1
- 0.0.11
1
+ 0.0.12
@@ -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;
@@ -83,14 +85,9 @@ const auth_js_1 = require("./sdk/codex/auth.js");
83
85
  const COMMAND_CHANNEL_CONNECT_RETRY_DELAY_MS = 1000;
84
86
  const COMMAND_CHANNEL_OPEN_TIMEOUT_MS = 5000;
85
87
  const TURN_COMPLETION_TIMEOUT_MS = 2 * 60 * 60000;
86
- const GITHUB_INSTALLATIONS_SYNC_INTERVAL_MS = 5 * 60000;
87
- const GITHUB_INSTALLATIONS_MIN_SYNC_INTERVAL_MS = 30000;
88
- const GITHUB_INSTALLATIONS_REFRESH_WINDOW_MS = 15 * 60000;
89
88
  const WORKSPACE_INSTALLATIONS_DIRECTORY = ".companyhelm";
90
- const WORKSPACE_INSTALLATIONS_FILENAME = "installations.json";
91
89
  const THREAD_GIT_SKILLS_CONFIG_FILENAME = "thread-git-skills.json";
92
90
  const THREAD_MCP_CONFIG_FILENAME = "thread-mcp.json";
93
- const THREAD_AGENT_CLI_CONFIG_FILENAME = "thread-agent-cli.json";
94
91
  const THREAD_MCP_BEARER_TOKEN_ENV_PREFIX = "COMPANYHELM_MCP_TOKEN_";
95
92
  const THREAD_MCP_AUTH_TYPE_BEARER_TOKEN = 2;
96
93
  const THREAD_MCP_STARTUP_TIMEOUT_SECONDS = 60;
@@ -262,6 +259,20 @@ function normalizeReasoningLevels(value) {
262
259
  function toErrorMessage(error) {
263
260
  return error instanceof Error ? error.message : String(error);
264
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
+ }
265
276
  function getGrpcStatusCode(error) {
266
277
  if (!error || typeof error !== "object" || !("code" in error)) {
267
278
  return undefined;
@@ -362,6 +373,36 @@ function normalizeNonEmptyString(value) {
362
373
  const trimmed = value.trim();
363
374
  return trimmed.length > 0 ? trimmed : undefined;
364
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
+ }
365
406
  function rewriteLocalTargetForDockerRuntime(target) {
366
407
  const trimmed = target.trim();
367
408
  if (!trimmed) {
@@ -451,89 +492,34 @@ function extractThreadNameUpdateFromNotification(notification) {
451
492
  normalizeNonEmptyString(params.thread_name);
452
493
  return { sdkThreadId, threadName };
453
494
  }
454
- function isGrpcServiceError(error) {
455
- return Boolean(error && typeof error === "object" && "code" in error);
456
- }
457
- function isUnimplementedGrpcMethod(error) {
458
- return isGrpcServiceError(error) && error.code === grpc.status.UNIMPLEMENTED;
459
- }
460
- function normalizeAccessTokenExpiration(accessTokenExpiresUnixTimeMs) {
461
- const rawUnixTimeMs = Number(accessTokenExpiresUnixTimeMs);
462
- const expirationUnixTimeMs = Number.isFinite(rawUnixTimeMs) && rawUnixTimeMs > 0
463
- ? Math.floor(rawUnixTimeMs)
464
- : Date.now() + 60 * 60000;
465
- return {
466
- accessTokenExpiresUnixTimeMs: expirationUnixTimeMs.toString(),
467
- accessTokenExpiration: new Date(expirationUnixTimeMs).toISOString(),
468
- };
469
- }
470
- async function loadRuntimeGithubInstallations(apiClient, options, logger) {
471
- let installationIds = [];
472
- try {
473
- const listResponse = await apiClient.listGithubInstallationsForRunner(options);
474
- installationIds = listResponse.installations.map((installation) => installation.installationId);
495
+ function extractThreadTokenUsageUpdateFromNotification(notification) {
496
+ if (notification.method !== "thread/tokenUsage/updated") {
497
+ return null;
475
498
  }
476
- catch (error) {
477
- const warning = isUnimplementedGrpcMethod(error)
478
- ? "CompanyHelm API does not implement listGithubInstallationsForRunner yet."
479
- : `Failed to fetch GitHub installations: ${toErrorMessage(error)}`;
480
- logger.warn(warning);
481
- 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;
482
507
  }
483
- const installationDetails = [];
484
- for (const installationId of installationIds) {
485
- try {
486
- const accessTokenResponse = await apiClient.getGithubInstallationAccessTokenForRunner(installationId, options);
487
- const accessToken = accessTokenResponse.accessToken.trim();
488
- if (!accessToken) {
489
- logger.warn(`Received empty GitHub access token for installation ${installationId.toString()}; skipping.`);
490
- continue;
491
- }
492
- const expiration = normalizeAccessTokenExpiration(accessTokenResponse.accessTokenExpiresUnixTimeMs);
493
- const repositories = [...new Set(accessTokenResponse.repositories.filter((repository) => repository.trim().length > 0))]
494
- .sort((left, right) => left.localeCompare(right));
495
- installationDetails.push({
496
- installationId: accessTokenResponse.installationId.toString(),
497
- accessToken,
498
- accessTokenExpiresUnixTimeMs: expiration.accessTokenExpiresUnixTimeMs,
499
- accessTokenExpiration: expiration.accessTokenExpiration,
500
- repositories,
501
- });
502
- }
503
- catch (error) {
504
- const warning = isUnimplementedGrpcMethod(error)
505
- ? "CompanyHelm API does not implement getGithubInstallationAccessTokenForRunner yet."
506
- : `Failed to fetch GitHub access token for installation ${installationId.toString()}: ${toErrorMessage(error)}`;
507
- logger.warn(warning);
508
- }
508
+ const totalUsage = resolveTokenUsageBreakdown(tokenUsage.total);
509
+ const lastUsage = resolveTokenUsageBreakdown(tokenUsage.last);
510
+ if (!totalUsage || !lastUsage) {
511
+ return null;
509
512
  }
510
- return installationDetails;
511
- }
512
- function buildWorkspaceGithubInstallationsPayload(installations) {
513
513
  return {
514
- synced_at: new Date().toISOString(),
515
- installations: installations.map((installation) => ({
516
- installation_id: installation.installationId,
517
- access_token: installation.accessToken,
518
- access_token_expires_unix_time_ms: installation.accessTokenExpiresUnixTimeMs,
519
- access_token_expiration: installation.accessTokenExpiration,
520
- repositories: installation.repositories,
521
- })),
514
+ sdkThreadId,
515
+ sdkTurnId,
516
+ totalUsage,
517
+ lastUsage,
518
+ modelContextWindow: normalizeNonNegativeNumber(tokenUsage.modelContextWindow ?? tokenUsage.model_context_window) ?? null,
522
519
  };
523
520
  }
524
- function writeWorkspaceGithubInstallationsPayload(workspaceDirectory, payload, logger) {
525
- const installationsDirectory = (0, node_path_1.join)(workspaceDirectory, WORKSPACE_INSTALLATIONS_DIRECTORY);
526
- const installationsPath = (0, node_path_1.join)(installationsDirectory, WORKSPACE_INSTALLATIONS_FILENAME);
527
- const temporaryPath = `${installationsPath}.tmp`;
528
- const serializedPayload = `${JSON.stringify(payload, null, 2)}\n`;
529
- try {
530
- (0, node_fs_1.mkdirSync)(installationsDirectory, { recursive: true });
531
- (0, node_fs_1.writeFileSync)(temporaryPath, serializedPayload, "utf8");
532
- (0, node_fs_1.renameSync)(temporaryPath, installationsPath);
533
- }
534
- catch (error) {
535
- logger.warn(`Failed writing GitHub installations file for workspace '${workspaceDirectory}': ${toErrorMessage(error)}`);
536
- }
521
+ function isGrpcServiceError(error) {
522
+ return Boolean(error && typeof error === "object" && "code" in error);
537
523
  }
538
524
  function isHttpsRepositoryUrl(value) {
539
525
  try {
@@ -852,65 +838,6 @@ function readWorkspaceThreadMcpConfig(workspaceDirectory, logger) {
852
838
  return [];
853
839
  }
854
840
  }
855
- function resolveThreadAgentCliConfigPath(workspaceDirectory) {
856
- return (0, node_path_1.join)(workspaceDirectory, WORKSPACE_INSTALLATIONS_DIRECTORY, THREAD_AGENT_CLI_CONFIG_FILENAME);
857
- }
858
- function parseThreadAgentCliConfig(content) {
859
- if (!isRecord(content)) {
860
- return null;
861
- }
862
- const agentApiUrl = normalizeNonEmptyString(content.agent_api_url);
863
- const token = normalizeNonEmptyString(content.token);
864
- if (!agentApiUrl || !token) {
865
- return null;
866
- }
867
- return {
868
- agent_api_url: agentApiUrl,
869
- token,
870
- };
871
- }
872
- function writeWorkspaceThreadAgentCliConfig(workspaceDirectory, cliSecret, agentApiUrl, logger) {
873
- const configPath = resolveThreadAgentCliConfigPath(workspaceDirectory);
874
- const configDirectory = (0, node_path_1.join)(workspaceDirectory, WORKSPACE_INSTALLATIONS_DIRECTORY);
875
- const temporaryPath = `${configPath}.tmp`;
876
- try {
877
- (0, node_fs_1.mkdirSync)(configDirectory, { recursive: true });
878
- if (cliSecret.length === 0) {
879
- (0, node_fs_1.rmSync)(configPath, { force: true });
880
- (0, node_fs_1.rmSync)(temporaryPath, { force: true });
881
- return;
882
- }
883
- const payload = {
884
- agent_api_url: normalizeThreadAgentApiUrlForRuntime(agentApiUrl),
885
- token: cliSecret,
886
- };
887
- (0, node_fs_1.writeFileSync)(temporaryPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
888
- (0, node_fs_1.renameSync)(temporaryPath, configPath);
889
- }
890
- catch (error) {
891
- logger.warn(`Failed writing thread agent CLI config for workspace '${workspaceDirectory}': ${toErrorMessage(error)}`);
892
- }
893
- }
894
- function readWorkspaceThreadAgentCliConfig(workspaceDirectory, logger) {
895
- const configPath = resolveThreadAgentCliConfigPath(workspaceDirectory);
896
- try {
897
- const rawContent = (0, node_fs_1.readFileSync)(configPath, "utf8");
898
- const parsedContent = JSON.parse(rawContent);
899
- const parsedConfig = parseThreadAgentCliConfig(parsedContent);
900
- if (!parsedConfig) {
901
- logger.warn(`Thread agent CLI config has invalid shape at '${configPath}'.`);
902
- return null;
903
- }
904
- return parsedConfig;
905
- }
906
- catch (error) {
907
- if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
908
- return null;
909
- }
910
- logger.warn(`Failed reading thread agent CLI config at '${configPath}': ${toErrorMessage(error)}`);
911
- return null;
912
- }
913
- }
914
841
  function escapeTomlString(value) {
915
842
  return JSON.stringify(value);
916
843
  }
@@ -1112,7 +1039,6 @@ async function reconcileThreadRunningStateBeforeUserMessage(cfg, threadState, lo
1112
1039
  const persistedThreadMcpServers = metadataStore.readThreadMcpConfig(threadState.id);
1113
1040
  const persistedThreadGitSkillPackages = metadataStore.readThreadGitSkillsConfig(threadState.id);
1114
1041
  const threadMcpSetup = buildThreadCodexMcpSetup(persistedThreadMcpServers);
1115
- const threadAgentCliConfig = buildThreadAgentCliConfig(threadState.cliSecret, cfg.agent_api_url);
1116
1042
  const appServerSession = await getOrCreateThreadAppServerSession(threadState.id, threadState.runtimeContainer, threadMcpSetup.appServerEnv, cfg.codex.app_server_client_name, logger);
1117
1043
  const runtimeUser = buildThreadRuntimeUser(cfg, threadState);
1118
1044
  await (0, thread_runtime_js_1.ensureThreadRuntimeReady)({
@@ -1127,11 +1053,7 @@ async function reconcileThreadRunningStateBeforeUserMessage(cfg, threadState, lo
1127
1053
  await containerService.ensureRuntimeContainerThreadMetadata(threadState.runtimeContainer, runtimeUser, {
1128
1054
  mcpServers: persistedThreadMcpServers,
1129
1055
  gitSkillPackages: persistedThreadGitSkillPackages,
1130
- threadAgentCliConfig,
1131
1056
  });
1132
- if (threadAgentCliConfig) {
1133
- await containerService.ensureRuntimeContainerAgentCliConfig(threadState.runtimeContainer, runtimeUser, threadAgentCliConfig);
1134
- }
1135
1057
  if (!appServerSession.started) {
1136
1058
  await containerService.ensureRuntimeContainerCodexConfig(threadState.runtimeContainer, runtimeUser, threadMcpSetup.configToml);
1137
1059
  }
@@ -1163,73 +1085,6 @@ async function reconcileThreadRunningStateBeforeUserMessage(cfg, threadState, lo
1163
1085
  isCurrentTurnRunning: false,
1164
1086
  };
1165
1087
  }
1166
- async function listTrackedThreadRuntimeTargets(cfg, logger) {
1167
- const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
1168
- try {
1169
- return await db
1170
- .select({
1171
- threadId: schema_js_1.threads.id,
1172
- runtimeContainer: schema_js_1.threads.runtimeContainer,
1173
- homeDirectory: schema_js_1.threads.homeDirectory,
1174
- uid: schema_js_1.threads.uid,
1175
- gid: schema_js_1.threads.gid,
1176
- })
1177
- .from(schema_js_1.threads)
1178
- .all();
1179
- }
1180
- catch (error) {
1181
- logger.warn(`Failed to list tracked thread runtimes for GitHub installation sync: ${toErrorMessage(error)}`);
1182
- return [];
1183
- }
1184
- finally {
1185
- client.close();
1186
- }
1187
- }
1188
- function resolveGithubInstallationsSyncDelayMs(installations) {
1189
- let syncDelayMs = GITHUB_INSTALLATIONS_SYNC_INTERVAL_MS;
1190
- const now = Date.now();
1191
- for (const installation of installations) {
1192
- const expirationUnixTimeMs = Number(installation.accessTokenExpiresUnixTimeMs);
1193
- if (!Number.isFinite(expirationUnixTimeMs) || expirationUnixTimeMs <= 0) {
1194
- continue;
1195
- }
1196
- const refreshInMs = expirationUnixTimeMs - now - GITHUB_INSTALLATIONS_REFRESH_WINDOW_MS;
1197
- const boundedRefreshDelayMs = Math.max(GITHUB_INSTALLATIONS_MIN_SYNC_INTERVAL_MS, Math.min(GITHUB_INSTALLATIONS_SYNC_INTERVAL_MS, refreshInMs));
1198
- syncDelayMs = Math.min(syncDelayMs, boundedRefreshDelayMs);
1199
- }
1200
- return Math.max(GITHUB_INSTALLATIONS_MIN_SYNC_INTERVAL_MS, Math.min(GITHUB_INSTALLATIONS_SYNC_INTERVAL_MS, syncDelayMs));
1201
- }
1202
- async function syncGithubInstallationsForRuntimeTargets(cfg, apiClient, options, runtimeTargets, logger) {
1203
- const uniqueTargets = [
1204
- ...new Map(runtimeTargets
1205
- .filter((target) => target.runtimeContainer.trim().length > 0)
1206
- .map((target) => [target.runtimeContainer, target])).values(),
1207
- ];
1208
- if (uniqueTargets.length === 0) {
1209
- return [];
1210
- }
1211
- const installations = await loadRuntimeGithubInstallations(apiClient, options, logger);
1212
- const payload = buildWorkspaceGithubInstallationsPayload(installations);
1213
- const containerService = new thread_lifecycle_js_1.ThreadContainerService();
1214
- for (const target of uniqueTargets) {
1215
- try {
1216
- if (!await containerService.isContainerRunning(target.runtimeContainer)) {
1217
- continue;
1218
- }
1219
- await containerService.ensureRuntimeContainerGithubInstallations(target.runtimeContainer, {
1220
- uid: target.uid,
1221
- gid: target.gid,
1222
- agentUser: cfg.agent_user,
1223
- agentHomeDirectory: target.homeDirectory,
1224
- }, payload);
1225
- }
1226
- catch (error) {
1227
- logger.warn(`Failed syncing GitHub installations into runtime container '${target.runtimeContainer}': ${toErrorMessage(error)}`);
1228
- }
1229
- }
1230
- logger.debug(`Synced ${installations.length} GitHub installation token(s) to ${uniqueTargets.length} runtime container(s).`);
1231
- return installations;
1232
- }
1233
1088
  async function waitForAbort(signal, delayMs) {
1234
1089
  if (signal.aborted) {
1235
1090
  return;
@@ -1247,21 +1102,6 @@ async function waitForAbort(signal, delayMs) {
1247
1102
  signal.addEventListener("abort", handleAbort);
1248
1103
  });
1249
1104
  }
1250
- async function runGithubInstallationsSyncLoop(cfg, apiClient, options, logger, signal) {
1251
- while (!signal.aborted) {
1252
- let nextDelayMs = GITHUB_INSTALLATIONS_SYNC_INTERVAL_MS;
1253
- try {
1254
- const runtimeTargets = await listTrackedThreadRuntimeTargets(cfg, logger);
1255
- const installations = await syncGithubInstallationsForRuntimeTargets(cfg, apiClient, options, runtimeTargets, logger);
1256
- nextDelayMs = resolveGithubInstallationsSyncDelayMs(installations);
1257
- }
1258
- catch (error) {
1259
- logger.warn(`GitHub installation sync loop iteration failed: ${toErrorMessage(error)}`);
1260
- nextDelayMs = GITHUB_INSTALLATIONS_MIN_SYNC_INTERVAL_MS;
1261
- }
1262
- await waitForAbort(signal, nextDelayMs);
1263
- }
1264
- }
1265
1105
  function normalizeReasoningEffort(value) {
1266
1106
  if (!value) {
1267
1107
  return null;
@@ -1279,21 +1119,12 @@ function normalizeAdditionalModelInstructions(value) {
1279
1119
  const trimmed = value.trim();
1280
1120
  return trimmed.length > 0 ? trimmed : null;
1281
1121
  }
1282
- function buildThreadAgentCliConfig(cliSecret, agentApiUrl) {
1283
- const normalizedSecret = normalizeNonEmptyString(cliSecret);
1284
- if (!normalizedSecret) {
1285
- return null;
1286
- }
1287
- return {
1288
- agent_api_url: normalizeThreadAgentApiUrlForRuntime(agentApiUrl),
1289
- token: normalizedSecret,
1290
- };
1291
- }
1292
- function buildThreadDeveloperInstructions(cfg, additionalModelInstructions, cliSecret) {
1122
+ function buildThreadDeveloperInstructions(threadId, cfg, additionalModelInstructions, cliSecret) {
1293
1123
  return (0, system_prompt_js_1.buildCodexDeveloperInstructions)(additionalModelInstructions, {
1294
1124
  homeDirectory: cfg.agent_home_directory,
1295
1125
  agentApiUrl: normalizeThreadAgentApiUrlForRuntime(cfg.agent_api_url),
1296
1126
  agentToken: normalizeNonEmptyString(cliSecret) ?? "<thread-secret>",
1127
+ threadId,
1297
1128
  workspaceMode: cfg.use_dedicated_workspaces ? "dedicated" : "shared",
1298
1129
  });
1299
1130
  }
@@ -1544,6 +1375,32 @@ async function sendThreadNameUpdate(commandChannel, threadId, threadName) {
1544
1375
  });
1545
1376
  await commandChannel.send(message);
1546
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
+ }
1547
1404
  async function sendTurnExecutionUpdate(commandChannel, threadId, sdkTurnId, status, requestId) {
1548
1405
  const message = (0, protobuf_1.create)(protos_1.ClientMessageSchema, {
1549
1406
  requestId,
@@ -1855,7 +1712,6 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
1855
1712
  const persistedThreadMcpServers = metadataStore.readThreadMcpConfig(threadState.id);
1856
1713
  const persistedThreadGitSkillPackages = metadataStore.readThreadGitSkillsConfig(threadState.id);
1857
1714
  const threadMcpSetup = buildThreadCodexMcpSetup(persistedThreadMcpServers);
1858
- const threadAgentCliConfig = buildThreadAgentCliConfig(threadState.cliSecret, cfg.agent_api_url);
1859
1715
  const appServerSession = await getOrCreateThreadAppServerSession(threadId, threadState.runtimeContainer, threadMcpSetup.appServerEnv, cfg.codex.app_server_client_name, logger);
1860
1716
  const runtimeUser = {
1861
1717
  uid: threadState.uid,
@@ -1875,25 +1731,12 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
1875
1731
  await containerService.ensureRuntimeContainerThreadMetadata(threadState.runtimeContainer, runtimeUser, {
1876
1732
  mcpServers: persistedThreadMcpServers,
1877
1733
  gitSkillPackages: persistedThreadGitSkillPackages,
1878
- threadAgentCliConfig,
1879
1734
  });
1880
- if (threadAgentCliConfig) {
1881
- await containerService.ensureRuntimeContainerAgentCliConfig(threadState.runtimeContainer, runtimeUser, threadAgentCliConfig);
1882
- }
1883
- await syncGithubInstallationsForRuntimeTargets(cfg, apiClient, apiCallOptions, [
1884
- {
1885
- threadId: threadState.id,
1886
- runtimeContainer: threadState.runtimeContainer,
1887
- homeDirectory: threadState.homeDirectory,
1888
- uid: threadState.uid,
1889
- gid: threadState.gid,
1890
- },
1891
- ], logger);
1892
1735
  if (!appServerSession.started) {
1893
1736
  await containerService.ensureRuntimeContainerCodexConfig(threadState.runtimeContainer, runtimeUser, threadMcpSetup.configToml);
1894
1737
  }
1895
1738
  await ensureThreadAppServerSessionStarted(appServerSession);
1896
- const developerInstructions = buildThreadDeveloperInstructions(cfg, threadState.additionalModelInstructions, threadState.cliSecret);
1739
+ const developerInstructions = buildThreadDeveloperInstructions(threadId, cfg, threadState.additionalModelInstructions, threadState.cliSecret);
1897
1740
  logger.debug(`Starting app-server thread '${threadId}' with developer instructions: ${JSON.stringify(developerInstructions)}.`);
1898
1741
  const threadStartResponse = await appServerSession.appServer.startThreadWithResponse({
1899
1742
  model: threadState.model,
@@ -2105,6 +1948,12 @@ async function waitForThreadTurnCompletion(stateDbPath, appServer, commandChanne
2105
1948
  receivedThreadNameUpdate = true;
2106
1949
  await sendThreadNameUpdate(commandChannel, threadId, threadNameUpdate.threadName);
2107
1950
  }
1951
+ const tokenUsageUpdate = extractThreadTokenUsageUpdateFromNotification(notification);
1952
+ if (tokenUsageUpdate &&
1953
+ tokenUsageUpdate.sdkThreadId === sdkThreadId &&
1954
+ tokenUsageUpdate.sdkTurnId === sdkTurnId) {
1955
+ await sendThreadTokenUsageUpdate(commandChannel, threadId, tokenUsageUpdate);
1956
+ }
2108
1957
  if (notification.method === "item/started" &&
2109
1958
  notification.params.threadId === sdkThreadId &&
2110
1959
  notification.params.turnId === sdkTurnId) {
@@ -2149,7 +1998,6 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
2149
1998
  const persistedThreadMcpServers = metadataStore.readThreadMcpConfig(threadState.id);
2150
1999
  const persistedThreadGitSkillPackages = metadataStore.readThreadGitSkillsConfig(threadState.id);
2151
2000
  const threadMcpSetup = buildThreadCodexMcpSetup(persistedThreadMcpServers);
2152
- const threadAgentCliConfig = buildThreadAgentCliConfig(threadState.cliSecret, cfg.agent_api_url);
2153
2001
  const appServerSession = await getOrCreateThreadAppServerSession(request.threadId, threadState.runtimeContainer, threadMcpSetup.appServerEnv, cfg.codex.app_server_client_name, logger);
2154
2002
  const appServer = appServerSession.appServer;
2155
2003
  const runtimeUser = buildThreadRuntimeUser(cfg, threadState);
@@ -2173,11 +2021,7 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
2173
2021
  await containerService.ensureRuntimeContainerThreadMetadata(threadState.runtimeContainer, runtimeUser, {
2174
2022
  mcpServers: persistedThreadMcpServers,
2175
2023
  gitSkillPackages: persistedThreadGitSkillPackages,
2176
- threadAgentCliConfig,
2177
2024
  });
2178
- if (threadAgentCliConfig) {
2179
- await containerService.ensureRuntimeContainerAgentCliConfig(threadState.runtimeContainer, runtimeUser, threadAgentCliConfig);
2180
- }
2181
2025
  if (!appServerSession.started) {
2182
2026
  await containerService.ensureRuntimeContainerCodexConfig(threadState.runtimeContainer, runtimeUser, threadMcpSetup.configToml);
2183
2027
  }
@@ -2201,7 +2045,7 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
2201
2045
  await updateThreadTurnState(cfg, request.threadId, { sdkThreadId });
2202
2046
  }
2203
2047
  else {
2204
- const developerInstructions = buildThreadDeveloperInstructions(cfg, threadState.additionalModelInstructions, threadState.cliSecret);
2048
+ const developerInstructions = buildThreadDeveloperInstructions(request.threadId, cfg, threadState.additionalModelInstructions, threadState.cliSecret);
2205
2049
  const threadStartParams = {
2206
2050
  model: request.model ?? threadState.model,
2207
2051
  modelProvider: null,
@@ -2644,6 +2488,7 @@ function sendDaemonParentMessage(message) {
2644
2488
  async function runRootCommand(options, runtimeOptions) {
2645
2489
  const logger = (0, logger_js_1.createLogger)(options.logLevel ?? "INFO", { daemonMode: options.daemon ?? false });
2646
2490
  const cfg = buildRootConfig(options);
2491
+ logger.info(formatWorkspaceStartupMessage(cfg));
2647
2492
  await (0, entrypoints_js_1.ensureRunnerStartupPreflight)(cfg);
2648
2493
  await (0, auth_js_1.ensureCodexRunnerStartState)(cfg, {
2649
2494
  useDedicatedAuth: options.useDedicatedAuth,
@@ -2674,8 +2519,6 @@ async function runRootCommand(options, runtimeOptions) {
2674
2519
  const apiClient = new companyhelm_api_client_js_1.CompanyhelmApiClient({ apiUrl: cfg.companyhelm_api_url, logger });
2675
2520
  activeApiClient = apiClient;
2676
2521
  let commandChannel = null;
2677
- let githubInstallationsSyncAbortController = null;
2678
- let githubInstallationsSyncTask = null;
2679
2522
  try {
2680
2523
  reconnectAttempt += 1;
2681
2524
  commandChannel = await apiClient.connect(registerRequest, apiCallOptions);
@@ -2690,12 +2533,6 @@ async function runRootCommand(options, runtimeOptions) {
2690
2533
  logger.info(`Connected to CompanyHelm API at ${cfg.companyhelm_api_url}`);
2691
2534
  }
2692
2535
  reconnectAttempt = 0;
2693
- githubInstallationsSyncAbortController = new AbortController();
2694
- githubInstallationsSyncTask = runGithubInstallationsSyncLoop(cfg, apiClient, apiCallOptions, logger, githubInstallationsSyncAbortController.signal).catch((error) => {
2695
- if (!githubInstallationsSyncAbortController?.signal.aborted) {
2696
- logger.warn(`GitHub installation sync loop exited unexpectedly: ${toErrorMessage(error)}`);
2697
- }
2698
- });
2699
2536
  await raceWithAbort(runCommandLoop(cfg, commandChannel, commandMessageSink, apiClient, apiCallOptions, logger), interruptState.signal);
2700
2537
  logger.warn("CompanyHelm API command channel closed. Reconnecting...");
2701
2538
  }
@@ -2715,10 +2552,6 @@ async function runRootCommand(options, runtimeOptions) {
2715
2552
  "Retrying...");
2716
2553
  }
2717
2554
  finally {
2718
- if (githubInstallationsSyncAbortController) {
2719
- githubInstallationsSyncAbortController.abort();
2720
- }
2721
- void githubInstallationsSyncTask;
2722
2555
  if (commandChannel) {
2723
2556
  commandChannel.cancel();
2724
2557
  commandMessageSink.unbind(commandChannel);
@@ -42,16 +42,6 @@ class RuntimeProvisioningScriptRenderer {
42
42
  config_content: shellQuote(configToml),
43
43
  });
44
44
  }
45
- renderAgentCliConfigScript(user, config) {
46
- const configDirectory = (0, node_path_1.join)(user.agentHomeDirectory, ".config", "companyhelm-agent-cli");
47
- const configPath = (0, node_path_1.join)(configDirectory, "config.json");
48
- const configContent = `${JSON.stringify(config, null, 2)}\n`;
49
- return this.templateRenderer.render("provisioning/runtime_agent_cli_config.sh.j2", {
50
- config_dir: shellQuote(configDirectory),
51
- config_path: shellQuote(configPath),
52
- config_content: shellQuote(configContent),
53
- });
54
- }
55
45
  renderGitConfigScript(gitUserName, gitUserEmail) {
56
46
  return this.templateRenderer.render("provisioning/runtime_git_config.sh.j2", {
57
47
  default_git_user_name: shellQuote(gitUserName),
@@ -20,6 +20,7 @@ class RuntimeSystemPromptRenderer {
20
20
  home_directory: options.homeDirectory,
21
21
  agent_api_url: options.agentApiUrl,
22
22
  agent_token: options.agentToken,
23
+ thread_id: options.threadId,
23
24
  };
24
25
  const common = this.templateRenderer.render("system_prompts/common.md.j2", context).trim();
25
26
  const workspaceSpecificTemplate = options.workspaceMode === "dedicated"
@@ -120,24 +120,6 @@ function createAgentRunnerControlServiceDefinition(pathPrefix = "") {
120
120
  responseSerialize: (response) => Buffer.from((0, protobuf_1.toBinary)(protos_1.ServerMessageSchema, response)),
121
121
  responseDeserialize: (bytes) => (0, protobuf_1.fromBinary)(protos_1.ServerMessageSchema, bytes),
122
122
  },
123
- listGithubInstallationsForRunner: {
124
- path: buildRpcPath(methods.listGithubInstallations.name, pathPrefix),
125
- requestStream: false,
126
- responseStream: false,
127
- requestSerialize: (request) => Buffer.from((0, protobuf_1.toBinary)(protos_1.ListGithubInstallationsRequestSchema, request)),
128
- requestDeserialize: (bytes) => (0, protobuf_1.fromBinary)(protos_1.ListGithubInstallationsRequestSchema, bytes),
129
- responseSerialize: (response) => Buffer.from((0, protobuf_1.toBinary)(protos_1.ListGithubInstallationsResponseSchema, response)),
130
- responseDeserialize: (bytes) => (0, protobuf_1.fromBinary)(protos_1.ListGithubInstallationsResponseSchema, bytes),
131
- },
132
- getGithubInstallationAccessTokenForRunner: {
133
- path: buildRpcPath(methods.githubInstallationAccessToken.name, pathPrefix),
134
- requestStream: false,
135
- responseStream: false,
136
- requestSerialize: (request) => Buffer.from((0, protobuf_1.toBinary)(protos_1.GithubInstallationAccessTokenRequestSchema, request)),
137
- requestDeserialize: (bytes) => (0, protobuf_1.fromBinary)(protos_1.GithubInstallationAccessTokenRequestSchema, bytes),
138
- responseSerialize: (response) => Buffer.from((0, protobuf_1.toBinary)(protos_1.GithubInstallationAccessTokenResponseSchema, response)),
139
- responseDeserialize: (bytes) => (0, protobuf_1.fromBinary)(protos_1.GithubInstallationAccessTokenResponseSchema, bytes),
140
- },
141
123
  };
142
124
  }
143
125
  function createAgentRunnerControlClient(endpoint, credentials, channelOptions) {
@@ -300,36 +282,6 @@ class CompanyhelmApiClient {
300
282
  const stream = this.client.controlChannel(options?.metadata, options?.callOptions);
301
283
  return new CompanyhelmCommandChannel(stream, this.logger);
302
284
  }
303
- listGithubInstallationsForRunner(options) {
304
- const metadata = options?.metadata ?? new grpc.Metadata();
305
- const callOptions = options?.callOptions ?? {};
306
- const request = (0, protobuf_1.create)(protos_1.ListGithubInstallationsRequestSchema, {});
307
- return new Promise((resolve, reject) => {
308
- this.client.listGithubInstallationsForRunner(request, metadata, callOptions, (error, response) => {
309
- if (error) {
310
- reject(error);
311
- return;
312
- }
313
- resolve(response ?? (0, protobuf_1.create)(protos_1.ListGithubInstallationsResponseSchema, {}));
314
- });
315
- });
316
- }
317
- getGithubInstallationAccessTokenForRunner(installationId, options) {
318
- const metadata = options?.metadata ?? new grpc.Metadata();
319
- const callOptions = options?.callOptions ?? {};
320
- const request = (0, protobuf_1.create)(protos_1.GithubInstallationAccessTokenRequestSchema, {
321
- installationId,
322
- });
323
- return new Promise((resolve, reject) => {
324
- this.client.getGithubInstallationAccessTokenForRunner(request, metadata, callOptions, (error, response) => {
325
- if (error) {
326
- reject(error);
327
- return;
328
- }
329
- resolve(response ?? (0, protobuf_1.create)(protos_1.GithubInstallationAccessTokenResponseSchema, {}));
330
- });
331
- });
332
- }
333
285
  close() {
334
286
  this.client.close();
335
287
  }
@@ -313,21 +313,6 @@ function buildRuntimeThreadGitSkillsLinkScript(user, options) {
313
313
  }
314
314
  return scriptLines.join("\n");
315
315
  }
316
- function buildRuntimeAgentCliConfigScript(user, config) {
317
- const configDirectory = (0, node_path_1.join)(user.agentHomeDirectory, ".config", "companyhelm-agent-cli");
318
- const configPath = (0, node_path_1.join)(configDirectory, "config.json");
319
- const configContent = `${JSON.stringify(config, null, 2)}\n`;
320
- return [
321
- "set -euo pipefail",
322
- `CONFIG_DIR=${shellQuote(configDirectory)}`,
323
- `CONFIG_PATH=${shellQuote(configPath)}`,
324
- `CONFIG_CONTENT=${shellQuote(configContent)}`,
325
- "",
326
- 'install -d -m 0755 "$CONFIG_DIR"',
327
- 'printf \'%s\' "$CONFIG_CONTENT" > "$CONFIG_PATH"',
328
- 'chmod 0600 "$CONFIG_PATH"',
329
- ].join("\n");
330
- }
331
316
  function buildDindContainerOptions(options) {
332
317
  return {
333
318
  name: options.names.dind,
@@ -627,10 +612,6 @@ class ThreadContainerService {
627
612
  const script = this.scriptRenderer.renderCodexConfigScript(user, configToml);
628
613
  this.runDockerExecScript(["exec", "-u", user.agentUser, name, "bash", "-lc", script], `Failed to write runtime Codex config.toml in container '${name}'`);
629
614
  }
630
- async ensureRuntimeContainerAgentCliConfig(name, user, config) {
631
- const script = this.scriptRenderer.renderAgentCliConfigScript(user, config);
632
- this.runDockerExecScript(["exec", "-u", user.agentUser, name, "bash", "-lc", script], `Failed to write runtime agent config in container '${name}'`);
633
- }
634
615
  async ensureRuntimeContainerGitConfig(name, user, gitUserName, gitUserEmail) {
635
616
  const script = this.scriptRenderer.renderGitConfigScript(gitUserName, gitUserEmail);
636
617
  this.runDockerExecScript(["exec", "-u", user.agentUser, name, "bash", "-lc", script], `Failed to configure git author defaults in runtime container '${name}'`);
@@ -651,14 +632,6 @@ class ThreadContainerService {
651
632
  const script = this.scriptRenderer.renderAgentMetadataScript(user, files);
652
633
  this.runDockerExecScript(["exec", "-u", user.agentUser, name, "bash", "-lc", script], contextMessage);
653
634
  }
654
- async ensureRuntimeContainerGithubInstallations(name, user, payload) {
655
- await this.ensureRuntimeContainerAgentMetadataFiles(name, user, [
656
- {
657
- filename: "installations.json",
658
- content: `${JSON.stringify(payload, null, 2)}\n`,
659
- },
660
- ], `Failed to write runtime GitHub installations metadata in container '${name}'`);
661
- }
662
635
  async ensureRuntimeContainerThreadMetadata(name, user, payload) {
663
636
  const files = [
664
637
  {
@@ -670,12 +643,6 @@ class ThreadContainerService {
670
643
  content: `${JSON.stringify({ packages: payload.gitSkillPackages }, null, 2)}\n`,
671
644
  },
672
645
  ];
673
- if (payload.threadAgentCliConfig) {
674
- files.push({
675
- filename: "thread-agent-cli.json",
676
- content: `${JSON.stringify(payload.threadAgentCliConfig, null, 2)}\n`,
677
- });
678
- }
679
646
  await this.ensureRuntimeContainerAgentMetadataFiles(name, user, files, `Failed to write runtime thread metadata in container '${name}'`);
680
647
  }
681
648
  async stopContainer(name) {
@@ -1,5 +1,10 @@
1
1
  # Agent Instructions
2
2
 
3
+ ## Identity
4
+
5
+ - Your CompanyHelm API thread id is `{{thread_id}}`.
6
+ - Use this thread id when you need to correlate your own runtime actions with CompanyHelm thread state.
7
+
3
8
  ## Workspace Structure
4
9
 
5
10
  - You are running in a thread-specific container and workspace.
@@ -14,22 +19,8 @@ Docker is available, the docker host runs in a separate container. The network i
14
19
  - Nested DinD is not supported in this environment because the outer runtime already uses rootless DinD.
15
20
  - If you need Docker in Docker, mount the configured host runtime instead of trying nested DinD.
16
21
 
17
- ## GitHub Installations
18
-
19
- - Synced GitHub installation credentials are stored at `{{home_directory}}/.companyhelm/agent/installations.json`.
20
- - Use `list-installations` to inspect installation IDs, repository scopes, tokens, and expiration timestamps.
21
- - Use `gh-use-installation <installation-id>` to configure `gh` authentication for a specific installation.
22
-
23
- ```bash
24
- list-installations
25
- gh-use-installation {installation_id}
26
- gh auth status --hostname github.com
27
- ```
28
-
29
22
  ## Available CLI Tools
30
23
 
31
- - `list-installations`: list synced GitHub installations with repositories, access tokens, and expirations.
32
- - `gh-use-installation <installation-id>`: configure `gh` authentication for a selected GitHub installation token.
33
24
  - `aws`: AWS CLI is pre-installed and available in `PATH`.
34
25
  - For scripted PR creation and updates, use `gh pr create --body-file <path>` and `gh pr edit --body-file <path>`.
35
26
  - Playwright CLI is already installed and available with Chromium pre-installed.
@@ -3,3 +3,4 @@
3
3
  - This runtime is using a shared host workspace mounted directly at `/workspace`.
4
4
  - Changes made in `/workspace` may be visible to multiple threads that use the same runner workspace path.
5
5
  - Do not assume `/workspace` is isolated per thread.
6
+ - you need to use git worktrees to isolate your working directory from the shared workspace.
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VitestReporterResolver = void 0;
4
+ class VitestReporterResolver {
5
+ constructor(options) {
6
+ this.stdoutIsTTY = options.stdoutIsTTY;
7
+ this.env = options.env ?? process.env;
8
+ }
9
+ resolve() {
10
+ const configured = this.env.COMPANYHELM_VITEST_REPORTER?.trim();
11
+ if (configured && configured.length > 0) {
12
+ return configured
13
+ .split(",")
14
+ .map((value) => value.trim())
15
+ .filter((value) => value.length > 0);
16
+ }
17
+ if (this.stdoutIsTTY) {
18
+ return ["basic"];
19
+ }
20
+ return undefined;
21
+ }
22
+ }
23
+ exports.VitestReporterResolver = VitestReporterResolver;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companyhelm/runner",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Run the CompanyHelm runner in fully isolated Docker sandboxes.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,7 +32,7 @@
32
32
  "dependencies": {
33
33
  "@bufbuild/protobuf": "^2.11.0",
34
34
  "@clack/prompts": "^1.0.1",
35
- "@companyhelm/protos": "^0.5.20",
35
+ "@companyhelm/protos": "^0.5.26",
36
36
  "@grpc/grpc-js": "^1.14.3",
37
37
  "@libsql/client": "^0.17.0",
38
38
  "commander": "^14.0.0",
@@ -1,8 +0,0 @@
1
- set -euo pipefail
2
- CONFIG_DIR={{config_dir}}
3
- CONFIG_PATH={{config_path}}
4
- CONFIG_CONTENT={{config_content}}
5
-
6
- install -d -m 0755 "$CONFIG_DIR"
7
- printf '%s' "$CONFIG_CONTENT" > "$CONFIG_PATH"
8
- chmod 0600 "$CONFIG_PATH"