copilot-hub 0.1.17 → 0.1.19

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 (32) hide show
  1. package/apps/agent-engine/.env.example +1 -1
  2. package/apps/agent-engine/dist/agent-worker.js +9 -1
  3. package/apps/agent-engine/dist/codex-account-refresh.js +42 -0
  4. package/apps/agent-engine/dist/config.js +2 -4
  5. package/apps/agent-engine/dist/index.js +34 -49
  6. package/apps/agent-engine/dist/test/codex-account-refresh.test.js +48 -0
  7. package/apps/control-plane/.env.example +1 -1
  8. package/apps/control-plane/dist/agent-worker.js +9 -1
  9. package/apps/control-plane/dist/channels/hub-model-utils.js +34 -0
  10. package/apps/control-plane/dist/channels/hub-ops-commands.js +134 -67
  11. package/apps/control-plane/dist/channels/telegram-channel.js +1 -0
  12. package/apps/control-plane/dist/config.js +2 -4
  13. package/apps/control-plane/dist/test/hub-model-utils.test.js +25 -1
  14. package/package.json +1 -1
  15. package/packages/core/dist/agent-supervisor.d.ts +2 -0
  16. package/packages/core/dist/agent-supervisor.js +64 -36
  17. package/packages/core/dist/agent-supervisor.js.map +1 -1
  18. package/packages/core/dist/bot-manager.d.ts +1 -0
  19. package/packages/core/dist/bot-manager.js +4 -0
  20. package/packages/core/dist/bot-manager.js.map +1 -1
  21. package/packages/core/dist/bot-runtime.d.ts +4 -0
  22. package/packages/core/dist/bot-runtime.js +81 -20
  23. package/packages/core/dist/bot-runtime.js.map +1 -1
  24. package/packages/core/dist/codex-app-client.js +4 -0
  25. package/packages/core/dist/codex-app-client.js.map +1 -1
  26. package/packages/core/dist/codex-app-utils.d.ts +1 -0
  27. package/packages/core/dist/codex-app-utils.js +43 -2
  28. package/packages/core/dist/codex-app-utils.js.map +1 -1
  29. package/packages/core/dist/provider-options.d.ts +1 -0
  30. package/packages/core/dist/provider-options.js +47 -0
  31. package/packages/core/dist/provider-options.js.map +1 -0
  32. package/packages/core/package.json +4 -0
@@ -15,7 +15,7 @@ DEFAULT_PROVIDER_KIND=codex
15
15
  CODEX_HOME_DIR=
16
16
  CODEX_SANDBOX=danger-full-access
17
17
  CODEX_APPROVAL_POLICY=never
18
- TURN_ACTIVITY_TIMEOUT_MS=3600000
18
+ TURN_ACTIVITY_TIMEOUT_MS=0
19
19
  MAX_THREAD_MESSAGES=200
20
20
 
21
21
  AGENT_HEARTBEAT_ENABLED=true
@@ -1,7 +1,7 @@
1
1
  import { BotRuntime } from "@copilot-hub/core/bot-runtime";
2
2
  const rawBotConfig = String(process.env.AGENT_BOT_CONFIG_JSON ?? "").trim();
3
3
  const rawProviderDefaults = String(process.env.AGENT_PROVIDER_DEFAULTS_JSON ?? "").trim();
4
- const turnActivityTimeoutMs = Number.parseInt(String(process.env.AGENT_TURN_ACTIVITY_TIMEOUT_MS ?? "3600000"), 10);
4
+ const turnActivityTimeoutMs = Number.parseInt(String(process.env.AGENT_TURN_ACTIVITY_TIMEOUT_MS ?? "0"), 10);
5
5
  const maxMessages = Number.parseInt(String(process.env.AGENT_MAX_MESSAGES ?? "200"), 10);
6
6
  const initialWebPublicBaseUrl = String(process.env.AGENT_WEB_PUBLIC_BASE_URL ?? "http://127.0.0.1:8787").trim();
7
7
  const kernelRequestTimeoutMs = Number.parseInt(String(process.env.AGENT_KERNEL_REQUEST_TIMEOUT_MS ?? "20000"), 10);
@@ -120,6 +120,14 @@ async function handleWorkerRequest(message) {
120
120
  result = await runtime.reloadCapabilities(capabilityDefinitions);
121
121
  break;
122
122
  }
123
+ case "setProviderOptions": {
124
+ result = await runtime.setProviderOptions(payload);
125
+ break;
126
+ }
127
+ case "refreshProviderSession": {
128
+ result = await runtime.refreshProviderSession(String(payload.reason ?? ""));
129
+ break;
130
+ }
123
131
  case "setProjectRoot": {
124
132
  const projectRoot = String(payload.projectRoot ?? "").trim();
125
133
  if (!projectRoot) {
@@ -0,0 +1,42 @@
1
+ export async function refreshRunningBotProviders({ botManager, reason = "codex account switched", }) {
2
+ const statuses = await botManager.listBotsLive();
3
+ const runningBotIds = collectRunningBotIds(statuses);
4
+ const refreshedBotIds = [];
5
+ const failures = [];
6
+ for (const botId of runningBotIds) {
7
+ try {
8
+ await botManager.refreshBotProviderSession(botId, reason);
9
+ refreshedBotIds.push(botId);
10
+ }
11
+ catch (error) {
12
+ failures.push({
13
+ botId,
14
+ error: sanitizeError(error),
15
+ });
16
+ }
17
+ }
18
+ return {
19
+ refreshedBotIds,
20
+ failures,
21
+ };
22
+ }
23
+ export function collectRunningBotIds(statuses) {
24
+ const runningBotIds = [];
25
+ for (const entry of Array.isArray(statuses) ? statuses : []) {
26
+ if (!isRecord(entry) || entry.running !== true) {
27
+ continue;
28
+ }
29
+ const botId = String(entry.id ?? "").trim();
30
+ if (botId) {
31
+ runningBotIds.push(botId);
32
+ }
33
+ }
34
+ return runningBotIds;
35
+ }
36
+ function isRecord(value) {
37
+ return value !== null && typeof value === "object";
38
+ }
39
+ function sanitizeError(error) {
40
+ const raw = error instanceof Error ? error.message : String(error);
41
+ return raw.split(/\r?\n/).slice(0, 12).join("\n");
42
+ }
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import dotenv from "dotenv";
4
4
  import { createWorkspaceBoundaryPolicy, assertWorkspaceAllowed, parseWorkspaceAllowedRoots, } from "@copilot-hub/core/workspace-policy";
5
+ import { parseTurnActivityTimeoutSetting } from "@copilot-hub/core/codex-app-utils";
5
6
  import { getDefaultExternalWorkspaceBasePath, getKernelRootPath, } from "@copilot-hub/core/workspace-paths";
6
7
  dotenv.config();
7
8
  const kernelRootPath = getKernelRootPath();
@@ -36,10 +37,7 @@ const codexBin = resolveCodexBin(process.env.CODEX_BIN);
36
37
  const codexHomeDir = resolveOptionalPath(process.env.CODEX_HOME_DIR);
37
38
  const codexSandbox = normalizeCodexSandbox(process.env.CODEX_SANDBOX ?? "danger-full-access");
38
39
  const codexApprovalPolicy = normalizeApprovalPolicy(process.env.CODEX_APPROVAL_POLICY ?? "never");
39
- const turnActivityTimeoutMs = Number.parseInt(process.env.TURN_ACTIVITY_TIMEOUT_MS ?? "3600000", 10);
40
- if (!Number.isFinite(turnActivityTimeoutMs) || turnActivityTimeoutMs < 10000) {
41
- throw new Error("TURN_ACTIVITY_TIMEOUT_MS must be an integer >= 10000.");
42
- }
40
+ const turnActivityTimeoutMs = parseTurnActivityTimeoutSetting(process.env.TURN_ACTIVITY_TIMEOUT_MS ?? "0", 0);
43
41
  const maxMessages = Number.parseInt(process.env.MAX_THREAD_MESSAGES ?? "200", 10);
44
42
  if (!Number.isFinite(maxMessages) || maxMessages < 20) {
45
43
  throw new Error("MAX_THREAD_MESSAGES must be an integer >= 20.");
@@ -11,6 +11,7 @@ import { InstanceLock } from "@copilot-hub/core/instance-lock";
11
11
  import { KernelControlPlane } from "@copilot-hub/core/kernel-control-plane";
12
12
  import { CONTROL_ACTIONS } from "@copilot-hub/core/control-plane-actions";
13
13
  import { KernelSecretStore } from "@copilot-hub/core/secret-store";
14
+ import { refreshRunningBotProviders } from "./codex-account-refresh.js";
14
15
  let activeWebPort = config.webPort;
15
16
  let runtimeWebPublicBaseUrl = config.webPublicBaseUrl;
16
17
  let shuttingDown = false;
@@ -211,15 +212,19 @@ function buildApiApp({ botManager, controlPlane, registryFilePath, }) {
211
212
  });
212
213
  return;
213
214
  }
214
- const restarted = await restartRunningBots();
215
+ const refreshed = await refreshRunningBotProviders({
216
+ botManager: requireBotManager(),
217
+ });
215
218
  res.json({
216
219
  ok: true,
217
220
  switched: true,
218
221
  configured: true,
219
222
  codexBin: status.codexBin,
220
223
  detail: status.detail,
221
- restartedBots: restarted.restartedBotIds,
222
- restartFailures: restarted.failures,
224
+ refreshedBots: refreshed.refreshedBotIds,
225
+ refreshFailures: refreshed.failures,
226
+ restartedBots: refreshed.refreshedBotIds,
227
+ restartFailures: refreshed.failures,
223
228
  });
224
229
  }));
225
230
  app.get("/api/extensions/contract", wrapAsync(async (req, res) => {
@@ -454,7 +459,7 @@ function parseDeleteModeFromRequest(body) {
454
459
  return "soft";
455
460
  }
456
461
  function startCodexDeviceAuthSession() {
457
- if (codexDeviceAuthSession && isDeviceAuthActive(codexDeviceAuthSession.status)) {
462
+ if (codexDeviceAuthSession && isDeviceAuthBusy(codexDeviceAuthSession.status)) {
458
463
  return codexDeviceAuthSession;
459
464
  }
460
465
  const codexBin = String(config.codexBin ?? "codex").trim() || "codex";
@@ -468,8 +473,8 @@ function startCodexDeviceAuthSession() {
468
473
  logLines: [],
469
474
  error: "",
470
475
  child: null,
471
- restartedBots: [],
472
- restartFailures: [],
476
+ refreshedBots: [],
477
+ refreshFailures: [],
473
478
  };
474
479
  const child = spawn(codexBin, ["login", "--device-auth"], {
475
480
  cwd: config.kernelRootPath,
@@ -497,14 +502,19 @@ function startCodexDeviceAuthSession() {
497
502
  return;
498
503
  }
499
504
  if (code === 0) {
500
- session.status = "succeeded";
501
- void restartRunningBots()
502
- .then((restarted) => {
503
- session.restartedBots = restarted.restartedBotIds;
504
- session.restartFailures = restarted.failures;
505
+ session.status = "applying";
506
+ void refreshRunningBotProviders({
507
+ botManager: requireBotManager(),
508
+ })
509
+ .then((refreshed) => {
510
+ session.refreshedBots = refreshed.refreshedBotIds;
511
+ session.refreshFailures = refreshed.failures;
512
+ session.status = "succeeded";
505
513
  })
506
514
  .catch((error) => {
507
- session.restartFailures = [{ botId: "*", error: sanitizeError(error) }];
515
+ session.refreshFailures = [{ botId: "*", error: sanitizeError(error) }];
516
+ session.status = "failed";
517
+ session.error = sanitizeError(error);
508
518
  });
509
519
  return;
510
520
  }
@@ -516,7 +526,7 @@ function startCodexDeviceAuthSession() {
516
526
  return session;
517
527
  }
518
528
  function cancelCodexDeviceAuthSession() {
519
- if (!codexDeviceAuthSession || !isDeviceAuthActive(codexDeviceAuthSession.status)) {
529
+ if (!codexDeviceAuthSession || !isDeviceAuthCancelable(codexDeviceAuthSession.status)) {
520
530
  return false;
521
531
  }
522
532
  codexDeviceAuthSession.status = "canceled";
@@ -541,8 +551,10 @@ function getCodexDeviceAuthSnapshot() {
541
551
  ...(session.loginUrl ? { loginUrl: session.loginUrl } : {}),
542
552
  ...(session.code ? { code: session.code } : {}),
543
553
  ...(session.error ? { detail: session.error } : {}),
544
- restartedBots: session.restartedBots,
545
- restartFailures: session.restartFailures,
554
+ refreshedBots: session.refreshedBots,
555
+ refreshFailures: session.refreshFailures,
556
+ restartedBots: session.refreshedBots,
557
+ restartFailures: session.refreshFailures,
546
558
  };
547
559
  }
548
560
  async function waitForDeviceCode(session, timeoutMs) {
@@ -620,7 +632,13 @@ function findDeviceCode(lines) {
620
632
  function stripAnsi(value) {
621
633
  return String(value ?? "").replace(ANSI_ESCAPE_PATTERN, "");
622
634
  }
623
- function isDeviceAuthActive(status) {
635
+ function isDeviceAuthBusy(status) {
636
+ const value = String(status ?? "")
637
+ .trim()
638
+ .toLowerCase();
639
+ return value === "starting" || value === "pending" || value === "applying";
640
+ }
641
+ function isDeviceAuthCancelable(status) {
624
642
  const value = String(status ?? "")
625
643
  .trim()
626
644
  .toLowerCase();
@@ -781,36 +799,3 @@ function isRecord(value) {
781
799
  function isErrnoException(error) {
782
800
  return isRecord(error);
783
801
  }
784
- async function restartRunningBots() {
785
- const manager = requireBotManager();
786
- const statuses = await manager.listBotsLive();
787
- const runningBotIds = [];
788
- for (const entry of statuses) {
789
- if (!isRecord(entry) || entry.running !== true) {
790
- continue;
791
- }
792
- const botId = String(entry.id ?? "").trim();
793
- if (botId) {
794
- runningBotIds.push(botId);
795
- }
796
- }
797
- const restartedBotIds = [];
798
- const failures = [];
799
- for (const botId of runningBotIds) {
800
- try {
801
- await manager.stopBot(botId);
802
- await manager.startBot(botId);
803
- restartedBotIds.push(botId);
804
- }
805
- catch (error) {
806
- failures.push({
807
- botId,
808
- error: sanitizeError(error),
809
- });
810
- }
811
- }
812
- return {
813
- restartedBotIds,
814
- failures,
815
- };
816
- }
@@ -0,0 +1,48 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ let refreshHelpersPromise = null;
4
+ async function loadRefreshHelpers() {
5
+ if (!refreshHelpersPromise) {
6
+ const specifier = ["..", "codex-account-refresh.js"].join("/");
7
+ refreshHelpersPromise = import(specifier);
8
+ }
9
+ return refreshHelpersPromise;
10
+ }
11
+ test("collectRunningBotIds keeps only running bots with ids", async () => {
12
+ const { collectRunningBotIds } = await loadRefreshHelpers();
13
+ const botIds = collectRunningBotIds([
14
+ { id: "agent-a", running: true },
15
+ { id: "agent-b", running: false },
16
+ { id: "agent-c", running: true },
17
+ { running: true },
18
+ null,
19
+ ]);
20
+ assert.deepEqual(botIds, ["agent-a", "agent-c"]);
21
+ });
22
+ test("refreshRunningBotProviders refreshes running bots and collects failures", async () => {
23
+ const { refreshRunningBotProviders } = await loadRefreshHelpers();
24
+ const refreshed = [];
25
+ const result = await refreshRunningBotProviders({
26
+ botManager: {
27
+ async listBotsLive() {
28
+ return [
29
+ { id: "agent-a", running: true },
30
+ { id: "agent-b", running: false },
31
+ { id: "agent-c", running: true },
32
+ ];
33
+ },
34
+ async refreshBotProviderSession(botId) {
35
+ if (botId === "agent-c") {
36
+ throw new Error("refresh failed");
37
+ }
38
+ refreshed.push(botId);
39
+ },
40
+ },
41
+ reason: "codex account switched",
42
+ });
43
+ assert.deepEqual(refreshed, ["agent-a"]);
44
+ assert.deepEqual(result.refreshedBotIds, ["agent-a"]);
45
+ assert.equal(result.failures.length, 1);
46
+ assert.equal(result.failures[0]?.botId, "agent-c");
47
+ assert.match(result.failures[0]?.error ?? "", /refresh failed/);
48
+ });
@@ -19,6 +19,6 @@ WORKSPACE_ALLOWED_ROOTS=
19
19
  DEFAULT_PROVIDER_KIND=codex
20
20
  CODEX_SANDBOX=danger-full-access
21
21
  CODEX_APPROVAL_POLICY=never
22
- TURN_ACTIVITY_TIMEOUT_MS=3600000
22
+ TURN_ACTIVITY_TIMEOUT_MS=0
23
23
  MAX_THREAD_MESSAGES=200
24
24
 
@@ -2,7 +2,7 @@ import { BotRuntime } from "@copilot-hub/core/bot-runtime";
2
2
  import { createChannelAdapter } from "./channels/channel-factory.js";
3
3
  const rawBotConfig = String(process.env.AGENT_BOT_CONFIG_JSON ?? "").trim();
4
4
  const rawProviderDefaults = String(process.env.AGENT_PROVIDER_DEFAULTS_JSON ?? "").trim();
5
- const turnActivityTimeoutMs = Number.parseInt(String(process.env.AGENT_TURN_ACTIVITY_TIMEOUT_MS ?? "3600000"), 10);
5
+ const turnActivityTimeoutMs = Number.parseInt(String(process.env.AGENT_TURN_ACTIVITY_TIMEOUT_MS ?? "0"), 10);
6
6
  const maxMessages = Number.parseInt(String(process.env.AGENT_MAX_MESSAGES ?? "200"), 10);
7
7
  const initialWebPublicBaseUrl = String(process.env.AGENT_WEB_PUBLIC_BASE_URL ?? "http://127.0.0.1:8787").trim();
8
8
  const kernelRequestTimeoutMs = Number.parseInt(String(process.env.AGENT_KERNEL_REQUEST_TIMEOUT_MS ?? "20000"), 10);
@@ -122,6 +122,14 @@ async function handleWorkerRequest(message) {
122
122
  result = await runtime.reloadCapabilities(capabilityDefinitions);
123
123
  break;
124
124
  }
125
+ case "setProviderOptions": {
126
+ result = await runtime.setProviderOptions(payload);
127
+ break;
128
+ }
129
+ case "refreshProviderSession": {
130
+ result = await runtime.refreshProviderSession(String(payload.reason ?? ""));
131
+ break;
132
+ }
125
133
  case "setProjectRoot": {
126
134
  const projectRoot = String(payload.projectRoot ?? "").trim();
127
135
  if (!projectRoot) {
@@ -245,6 +245,40 @@ export async function applyBotModelPolicy({ apiPost, botId, botState, model, })
245
245
  model,
246
246
  });
247
247
  }
248
+ export function getRuntimeModel(runtime) {
249
+ if (!runtime || typeof runtime.getProviderOptions !== "function") {
250
+ return null;
251
+ }
252
+ const options = runtime.getProviderOptions();
253
+ if (!isObject(options)) {
254
+ return null;
255
+ }
256
+ const model = String(options.model ?? "").trim();
257
+ return model || null;
258
+ }
259
+ export async function applyRuntimeModelPolicy({ runtime, model, }) {
260
+ if (!runtime || typeof runtime.setProviderOptions !== "function") {
261
+ throw new Error("Hub model update is not available on this runtime.");
262
+ }
263
+ await runtime.setProviderOptions({ model });
264
+ }
265
+ export function resolveSharedModel(models) {
266
+ let normalizedModel;
267
+ for (const entry of Array.isArray(models) ? models : []) {
268
+ const nextModel = String(entry ?? "").trim() || null;
269
+ if (normalizedModel === undefined) {
270
+ normalizedModel = nextModel;
271
+ continue;
272
+ }
273
+ if (normalizedModel !== nextModel) {
274
+ return { mode: "mixed" };
275
+ }
276
+ }
277
+ return {
278
+ mode: "uniform",
279
+ model: normalizedModel ?? null,
280
+ };
281
+ }
248
282
  export async function applyModelPolicyToBots({ apiPost, bots, model, }) {
249
283
  const updatedBotIds = [];
250
284
  const failures = [];