copilot-hub 0.1.13 → 0.1.16

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 (66) hide show
  1. package/apps/agent-engine/dist/agent-worker.js +28 -20
  2. package/apps/agent-engine/dist/config.js +1 -2
  3. package/apps/agent-engine/dist/index.js +146 -41
  4. package/apps/control-plane/dist/agent-worker.js +28 -20
  5. package/apps/control-plane/dist/channels/channel-factory.js +1 -2
  6. package/apps/control-plane/dist/channels/hub-model-utils.js +280 -0
  7. package/apps/control-plane/dist/channels/hub-ops-commands.js +859 -143
  8. package/apps/control-plane/dist/channels/telegram-channel.js +197 -53
  9. package/apps/control-plane/dist/channels/whatsapp-channel.js +6 -1
  10. package/apps/control-plane/dist/config.js +1 -2
  11. package/apps/control-plane/dist/copilot-hub.js +2 -3
  12. package/apps/control-plane/dist/index.js +41 -23
  13. package/apps/control-plane/dist/kernel/admin-contract.js +1 -2
  14. package/apps/control-plane/dist/test/hub-model-utils.test.js +170 -0
  15. package/package.json +4 -2
  16. package/packages/core/dist/agent-supervisor.d.ts +109 -28
  17. package/packages/core/dist/agent-supervisor.js +99 -45
  18. package/packages/core/dist/agent-supervisor.js.map +1 -1
  19. package/packages/core/dist/bot-manager.d.ts +86 -45
  20. package/packages/core/dist/bot-manager.js +17 -3
  21. package/packages/core/dist/bot-manager.js.map +1 -1
  22. package/packages/core/dist/bot-runtime.d.ts +225 -125
  23. package/packages/core/dist/bot-runtime.js +240 -52
  24. package/packages/core/dist/bot-runtime.js.map +1 -1
  25. package/packages/core/dist/bridge-service.d.ts +158 -31
  26. package/packages/core/dist/bridge-service.js +33 -22
  27. package/packages/core/dist/bridge-service.js.map +1 -1
  28. package/packages/core/dist/capability-manager.d.ts +60 -12
  29. package/packages/core/dist/capability-manager.js +74 -37
  30. package/packages/core/dist/capability-manager.js.map +1 -1
  31. package/packages/core/dist/channel-factory.d.ts +9 -4
  32. package/packages/core/dist/channel-factory.js +1 -2
  33. package/packages/core/dist/channel-factory.js.map +1 -1
  34. package/packages/core/dist/codex-app-client.d.ts +110 -43
  35. package/packages/core/dist/codex-app-client.js +182 -333
  36. package/packages/core/dist/codex-app-client.js.map +1 -1
  37. package/packages/core/dist/codex-app-events.d.ts +30 -0
  38. package/packages/core/dist/codex-app-events.js +266 -0
  39. package/packages/core/dist/codex-app-events.js.map +1 -0
  40. package/packages/core/dist/codex-app-utils.d.ts +28 -0
  41. package/packages/core/dist/codex-app-utils.js +164 -0
  42. package/packages/core/dist/codex-app-utils.js.map +1 -0
  43. package/packages/core/dist/codex-provider.d.ts +36 -27
  44. package/packages/core/dist/codex-provider.js +12 -11
  45. package/packages/core/dist/codex-provider.js.map +1 -1
  46. package/packages/core/dist/codex-quota-display.d.ts +12 -0
  47. package/packages/core/dist/codex-quota-display.js +56 -0
  48. package/packages/core/dist/codex-quota-display.js.map +1 -0
  49. package/packages/core/dist/extension-contract.d.ts +1 -1
  50. package/packages/core/dist/extension-contract.js +0 -1
  51. package/packages/core/dist/extension-contract.js.map +1 -1
  52. package/packages/core/dist/kernel-control-plane.d.ts +52 -12
  53. package/packages/core/dist/kernel-control-plane.js +95 -32
  54. package/packages/core/dist/kernel-control-plane.js.map +1 -1
  55. package/packages/core/dist/provider-factory.d.ts +20 -6
  56. package/packages/core/dist/provider-factory.js +20 -8
  57. package/packages/core/dist/provider-factory.js.map +1 -1
  58. package/packages/core/dist/telegram-channel.d.ts +103 -16
  59. package/packages/core/dist/telegram-channel.js +195 -59
  60. package/packages/core/dist/telegram-channel.js.map +1 -1
  61. package/packages/core/dist/whatsapp-channel.d.ts +21 -20
  62. package/packages/core/dist/whatsapp-channel.js +6 -1
  63. package/packages/core/dist/whatsapp-channel.js.map +1 -1
  64. package/packages/core/package.json +4 -0
  65. package/scripts/dist/daemon.mjs +41 -2
  66. package/scripts/src/daemon.mts +45 -2
@@ -1,7 +1,21 @@
1
1
  import { CodexProvider } from "./codex-provider.js";
2
- export declare function createAssistantProvider({ providerConfig, providerDefaults, workspaceRoot, turnActivityTimeoutMs, }: {
3
- providerConfig: any;
4
- providerDefaults: any;
5
- workspaceRoot: any;
6
- turnActivityTimeoutMs: any;
7
- }): CodexProvider;
2
+ type ProviderConfig = {
3
+ kind?: unknown;
4
+ options?: unknown;
5
+ } | null;
6
+ type ProviderDefaults = {
7
+ defaultKind?: unknown;
8
+ codexBin?: unknown;
9
+ codexHomeDir?: unknown;
10
+ codexSandbox?: unknown;
11
+ codexApprovalPolicy?: unknown;
12
+ codexModel?: unknown;
13
+ } | null;
14
+ type CreateAssistantProviderParams = {
15
+ providerConfig?: ProviderConfig;
16
+ providerDefaults?: ProviderDefaults;
17
+ workspaceRoot: string;
18
+ turnActivityTimeoutMs?: number;
19
+ };
20
+ export declare function createAssistantProvider({ providerConfig, providerDefaults, workspaceRoot, turnActivityTimeoutMs, }: CreateAssistantProviderParams): CodexProvider;
21
+ export {};
@@ -1,21 +1,33 @@
1
- // @ts-nocheck
2
1
  import { CodexProvider } from "./codex-provider.js";
3
2
  export function createAssistantProvider({ providerConfig, providerDefaults, workspaceRoot, turnActivityTimeoutMs, }) {
4
- const defaults = providerDefaults ?? {};
5
- const kind = String(providerConfig?.kind ?? defaults.defaultKind ?? "codex")
3
+ const defaults = asRecord(providerDefaults);
4
+ const provider = asRecord(providerConfig);
5
+ const kind = String(provider.kind ?? defaults.defaultKind ?? "codex")
6
6
  .trim()
7
7
  .toLowerCase();
8
- const options = providerConfig?.options ?? {};
8
+ const options = asRecord(provider.options);
9
9
  if (kind === "codex") {
10
- return new CodexProvider({
10
+ const codexProviderConfig = {
11
11
  codexBin: String(options.codexBin ?? defaults.codexBin ?? "codex"),
12
- codexHomeDir: options.codexHomeDir ?? defaults.codexHomeDir ?? null,
12
+ codexHomeDir: normalizeOptionalString(options.codexHomeDir ?? defaults.codexHomeDir),
13
13
  sandboxMode: String(options.sandboxMode ?? defaults.codexSandbox ?? "danger-full-access"),
14
14
  approvalPolicy: String(options.approvalPolicy ?? defaults.codexApprovalPolicy ?? "never"),
15
+ model: normalizeOptionalString(options.model ?? defaults.codexModel),
15
16
  workspaceRoot,
16
- turnActivityTimeoutMs,
17
- });
17
+ ...(turnActivityTimeoutMs === undefined ? {} : { turnActivityTimeoutMs }),
18
+ };
19
+ return new CodexProvider(codexProviderConfig);
18
20
  }
19
21
  throw new Error(`Unknown assistant provider kind '${kind}'.`);
20
22
  }
23
+ function asRecord(value) {
24
+ return value && typeof value === "object" ? value : {};
25
+ }
26
+ function normalizeOptionalString(value) {
27
+ if (value === null || value === undefined) {
28
+ return null;
29
+ }
30
+ const normalized = String(value).trim();
31
+ return normalized || null;
32
+ }
21
33
  //# sourceMappingURL=provider-factory.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"provider-factory.js","sourceRoot":"","sources":["../src/provider-factory.ts"],"names":[],"mappings":"AAAA,cAAc;AACd,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,MAAM,UAAU,uBAAuB,CAAC,EACtC,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,qBAAqB,GACtB;IACC,MAAM,QAAQ,GAAG,gBAAgB,IAAI,EAAE,CAAC;IACxC,MAAM,IAAI,GAAG,MAAM,CAAC,cAAc,EAAE,IAAI,IAAI,QAAQ,CAAC,WAAW,IAAI,OAAO,CAAC;SACzE,IAAI,EAAE;SACN,WAAW,EAAE,CAAC;IACjB,MAAM,OAAO,GAAG,cAAc,EAAE,OAAO,IAAI,EAAE,CAAC;IAE9C,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACrB,OAAO,IAAI,aAAa,CAAC;YACvB,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,QAAQ,IAAI,QAAQ,CAAC,QAAQ,IAAI,OAAO,CAAC;YAClE,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,QAAQ,CAAC,YAAY,IAAI,IAAI;YACnE,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC,WAAW,IAAI,QAAQ,CAAC,YAAY,IAAI,oBAAoB,CAAC;YACzF,cAAc,EAAE,MAAM,CAAC,OAAO,CAAC,cAAc,IAAI,QAAQ,CAAC,mBAAmB,IAAI,OAAO,CAAC;YACzF,aAAa;YACb,qBAAqB;SACtB,CAAC,CAAC;IACL,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,oCAAoC,IAAI,IAAI,CAAC,CAAC;AAChE,CAAC"}
1
+ {"version":3,"file":"provider-factory.js","sourceRoot":"","sources":["../src/provider-factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAyBpD,MAAM,UAAU,uBAAuB,CAAC,EACtC,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,qBAAqB,GACS;IAC9B,MAAM,QAAQ,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IAC5C,MAAM,QAAQ,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;IAC1C,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,WAAW,IAAI,OAAO,CAAC;SAClE,IAAI,EAAE;SACN,WAAW,EAAE,CAAC;IACjB,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAE3C,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACrB,MAAM,mBAAmB,GAAG;YAC1B,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,QAAQ,IAAI,QAAQ,CAAC,QAAQ,IAAI,OAAO,CAAC;YAClE,YAAY,EAAE,uBAAuB,CAAC,OAAO,CAAC,YAAY,IAAI,QAAQ,CAAC,YAAY,CAAC;YACpF,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC,WAAW,IAAI,QAAQ,CAAC,YAAY,IAAI,oBAAoB,CAAC;YACzF,cAAc,EAAE,MAAM,CAAC,OAAO,CAAC,cAAc,IAAI,QAAQ,CAAC,mBAAmB,IAAI,OAAO,CAAC;YACzF,KAAK,EAAE,uBAAuB,CAAC,OAAO,CAAC,KAAK,IAAI,QAAQ,CAAC,UAAU,CAAC;YACpE,aAAa;YACb,GAAG,CAAC,qBAAqB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,qBAAqB,EAAE,CAAC;SAC1E,CAAC;QACF,OAAO,IAAI,aAAa,CAAC,mBAAmB,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,oCAAoC,IAAI,IAAI,CAAC,CAAC;AAChE,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAE,KAAuB,CAAC,CAAC,CAAC,EAAE,CAAC;AAC5E,CAAC;AAED,SAAS,uBAAuB,CAAC,KAAc;IAC7C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC1C,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,OAAO,UAAU,IAAI,IAAI,CAAC;AAC5B,CAAC"}
@@ -1,27 +1,114 @@
1
+ import { Bot } from "grammy";
2
+ type TelegramChannelConfig = {
3
+ kind?: unknown;
4
+ id?: string;
5
+ token?: string;
6
+ allowedChatIds?: string | Array<string | number>;
7
+ };
8
+ type ThreadState = {
9
+ turnCount?: number;
10
+ sessionId?: string;
11
+ updatedAt?: string;
12
+ };
13
+ type PendingApproval = {
14
+ id: string;
15
+ kind: string;
16
+ command?: string;
17
+ cwd?: string;
18
+ reason?: string;
19
+ threadId?: string;
20
+ metadata?: {
21
+ chatId?: string;
22
+ };
23
+ };
24
+ type ApprovalDecision = "accept" | "acceptForSession" | "decline";
25
+ type InterruptResult = {
26
+ interrupted?: boolean;
27
+ method?: string;
28
+ reason?: string;
29
+ error?: string;
30
+ };
31
+ type TurnInputItem = {
32
+ type?: string;
33
+ [key: string]: unknown;
34
+ };
35
+ type ActiveTurn = {
36
+ threadId: string;
37
+ token: string;
38
+ };
39
+ type TurnControlState = {
40
+ token: string;
41
+ threadId: string;
42
+ messageId: number;
43
+ };
44
+ type RuntimeLike = {
45
+ runtimeId?: string;
46
+ runtimeName?: string;
47
+ resolveThreadIdForChannel: (args: {
48
+ channelKind: string;
49
+ channelId: string;
50
+ externalUserId: string;
51
+ }) => Promise<string>;
52
+ resetThread: (threadId: string) => Promise<unknown>;
53
+ getThread: (threadId: string) => Promise<{
54
+ thread: ThreadState;
55
+ }>;
56
+ buildWebBotUrl: () => string;
57
+ listPendingApprovals: (threadId: string) => Promise<PendingApproval[]>;
58
+ resolvePendingApproval: (args: {
59
+ threadId: string;
60
+ approvalId: string;
61
+ decision: ApprovalDecision;
62
+ }) => Promise<unknown>;
63
+ interruptThread: (threadId: string) => Promise<InterruptResult | null>;
64
+ sendTurn: (args: {
65
+ threadId: string;
66
+ prompt: string;
67
+ inputItems?: TurnInputItem[];
68
+ source?: string;
69
+ metadata?: Record<string, unknown>;
70
+ }) => Promise<{
71
+ assistantText?: string;
72
+ }>;
73
+ getWorkspaceRoot?: () => string;
74
+ getProviderUsage?: () => Promise<unknown>;
75
+ };
1
76
  export declare class TelegramChannel {
2
77
  #private;
3
- constructor({ channelConfig, runtime }: {
4
- channelConfig: any;
5
- runtime: any;
78
+ kind: "telegram";
79
+ id: string;
80
+ config: TelegramChannelConfig;
81
+ runtime: RuntimeLike;
82
+ allowedChatIds: Set<string>;
83
+ bot: Bot | null;
84
+ running: boolean;
85
+ error: string | null;
86
+ activeTurnsByChat: Map<string, ActiveTurn>;
87
+ turnControlByChat: Map<string, TurnControlState>;
88
+ nextTurnToken: number;
89
+ constructor({ channelConfig, runtime, }: {
90
+ channelConfig: TelegramChannelConfig;
91
+ runtime: unknown;
6
92
  });
7
93
  start(): Promise<{
8
- kind: any;
9
- id: any;
10
- running: any;
11
- error: any;
94
+ kind: string;
95
+ id: string;
96
+ running: boolean;
97
+ error: string | null;
12
98
  }>;
13
99
  stop(): Promise<{
14
- kind: any;
15
- id: any;
16
- running: any;
17
- error: any;
100
+ kind: string;
101
+ id: string;
102
+ running: boolean;
103
+ error: string | null;
18
104
  }>;
19
105
  shutdown(): Promise<void>;
20
106
  getStatus(): {
21
- kind: any;
22
- id: any;
23
- running: any;
24
- error: any;
107
+ kind: string;
108
+ id: string;
109
+ running: boolean;
110
+ error: string | null;
25
111
  };
26
- notifyApproval(approval: any): Promise<void>;
112
+ notifyApproval(approval: PendingApproval): Promise<void>;
27
113
  }
114
+ export {};
@@ -1,13 +1,27 @@
1
- // @ts-nocheck
2
1
  import fs from "node:fs/promises";
2
+ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { Bot } from "grammy";
5
+ import { formatCodexQuotaLine, hasCodexQuotaWindows } from "./codex-quota-display.js";
6
+ const CODEX_USAGE_CACHE_TTL_MS = 60_000;
7
+ let cachedCodexUsage = null;
5
8
  export class TelegramChannel {
6
- constructor({ channelConfig, runtime }) {
9
+ kind;
10
+ id;
11
+ config;
12
+ runtime;
13
+ allowedChatIds;
14
+ bot;
15
+ running;
16
+ error;
17
+ activeTurnsByChat;
18
+ turnControlByChat;
19
+ nextTurnToken;
20
+ constructor({ channelConfig, runtime, }) {
7
21
  this.kind = "telegram";
8
22
  this.id = String(channelConfig.id ?? "telegram");
9
23
  this.config = channelConfig;
10
- this.runtime = runtime;
24
+ this.runtime = toRuntimeLike(runtime);
11
25
  this.allowedChatIds = normalizeAllowedChatIds(channelConfig.allowedChatIds);
12
26
  this.bot = null;
13
27
  this.running = false;
@@ -320,7 +334,7 @@ export class TelegramChannel {
320
334
  console.error(`[${this.runtime.runtimeId}:${this.id}] Telegram error in update ${error.ctx.update.update_id}: ${details}`);
321
335
  });
322
336
  }
323
- async #routeIncomingPrompt({ chatId, prompt, mode = "auto_message", inputItems = null }) {
337
+ async #routeIncomingPrompt({ chatId, prompt, mode = "auto_message", inputItems = null, }) {
324
338
  const normalizedPrompt = String(prompt ?? "").trim();
325
339
  if (!normalizedPrompt) {
326
340
  if (this.bot) {
@@ -383,7 +397,7 @@ export class TelegramChannel {
383
397
  url: dataUrl,
384
398
  };
385
399
  }
386
- async #buildVoicePrompt({ chatId, message }) {
400
+ async #buildVoicePrompt({ chatId, message, }) {
387
401
  const voice = message?.voice;
388
402
  if (!voice || !voice.file_id) {
389
403
  throw new Error("Telegram update does not contain voice payload.");
@@ -434,7 +448,7 @@ export class TelegramChannel {
434
448
  }
435
449
  return Buffer.from(await response.arrayBuffer());
436
450
  }
437
- async #saveTelegramMediaFile({ chatId, fileId, mediaKind, preferredExtension }) {
451
+ async #saveTelegramMediaFile({ chatId, fileId, mediaKind, preferredExtension, }) {
438
452
  if (!this.bot) {
439
453
  throw new Error("Telegram bot is not running.");
440
454
  }
@@ -468,7 +482,7 @@ export class TelegramChannel {
468
482
  }
469
483
  return path.resolve(process.cwd());
470
484
  }
471
- async #processTurn({ chatId, threadId, prompt, inputItems = null }) {
485
+ async #processTurn({ chatId, threadId, prompt, inputItems = null, }) {
472
486
  if (!this.bot) {
473
487
  return;
474
488
  }
@@ -477,16 +491,19 @@ export class TelegramChannel {
477
491
  let controlStatus = "Generation completed.";
478
492
  try {
479
493
  await this.bot.api.sendChatAction(chatId, "typing");
480
- const result = await this.runtime.sendTurn({
494
+ const payload = {
481
495
  threadId,
482
496
  prompt,
483
- inputItems: Array.isArray(inputItems) ? inputItems : undefined,
484
497
  source: "telegram",
485
498
  metadata: {
486
499
  chatId,
487
500
  channelId: this.id,
488
501
  },
489
- });
502
+ };
503
+ if (Array.isArray(inputItems)) {
504
+ payload.inputItems = inputItems;
505
+ }
506
+ const result = await this.runtime.sendTurn(payload);
490
507
  await sendChunkedMessage(this.bot.api, chatId, result.assistantText || "Assistant returned no text output.");
491
508
  }
492
509
  catch (error) {
@@ -509,7 +526,11 @@ export class TelegramChannel {
509
526
  }
510
527
  }
511
528
  async #handleStopTurn(context) {
512
- const chatId = String(context.chat.id);
529
+ const chatId = getContextChatId(context);
530
+ if (!chatId) {
531
+ await context.reply("Unable to resolve chat.");
532
+ return;
533
+ }
513
534
  if (!this.#isAllowedChat(chatId)) {
514
535
  await context.reply("Chat not allowed for this bot.");
515
536
  return;
@@ -523,12 +544,16 @@ export class TelegramChannel {
523
544
  await context.reply(formatInterruptResult(result));
524
545
  }
525
546
  async #handleSteer(context) {
526
- const chatId = String(context.chat.id);
547
+ const chatId = getContextChatId(context);
548
+ if (!chatId) {
549
+ await context.reply("Unable to resolve chat.");
550
+ return;
551
+ }
527
552
  if (!this.#isAllowedChat(chatId)) {
528
553
  await context.reply("Chat not allowed for this bot.");
529
554
  return;
530
555
  }
531
- const instruction = extractCommandTail(context.message.text);
556
+ const instruction = extractCommandTail(getContextMessageText(context));
532
557
  const threadId = await this.runtime.resolveThreadIdForChannel({
533
558
  channelKind: this.kind,
534
559
  channelId: this.id,
@@ -621,9 +646,9 @@ export class TelegramChannel {
621
646
  const key = String(chatId);
622
647
  await this.#closeTurnControls(chatId, null, null);
623
648
  try {
624
- const quotaLine = await resolveCodexQuotaLine(this.runtime);
625
- const inProgressText = quotaLine
626
- ? ["Generation in progress.", quotaLine].join("\n")
649
+ const quota = await resolveCodexQuotaLine(this.runtime);
650
+ const inProgressText = quota.line
651
+ ? ["Generation in progress.", quota.line].join("\n")
627
652
  : "Generation in progress.";
628
653
  const sent = await this.bot.api.sendMessage(chatId, inProgressText, {
629
654
  reply_markup: buildTurnControlKeyboard(token),
@@ -641,12 +666,12 @@ export class TelegramChannel {
641
666
  }
642
667
  #scheduleTurnControlQuotaRefresh(chatId, token, attempt) {
643
668
  const safeAttempt = Number.isFinite(attempt) ? Number(attempt) : 1;
644
- if (safeAttempt > 4) {
669
+ if (safeAttempt > 12) {
645
670
  return;
646
671
  }
647
672
  setTimeout(() => {
648
673
  void this.#refreshTurnControlQuota(chatId, token, safeAttempt);
649
- }, 1200);
674
+ }, 1500);
650
675
  }
651
676
  async #refreshTurnControlQuota(chatId, token, attempt) {
652
677
  if (!this.bot) {
@@ -657,12 +682,17 @@ export class TelegramChannel {
657
682
  if (!current || String(current.token) !== String(token)) {
658
683
  return;
659
684
  }
660
- const quotaLine = await resolveCodexQuotaLine(this.runtime);
661
- if (!quotaLine) {
685
+ const quota = await resolveCodexQuotaLine(this.runtime);
686
+ if (!quota.line) {
687
+ this.#scheduleTurnControlQuotaRefresh(chatId, token, Number(attempt) + 1);
688
+ return;
689
+ }
690
+ // Keep polling until the real quota percentages arrive (model-only line is not enough).
691
+ if (!quota.hasQuotaWindows) {
662
692
  this.#scheduleTurnControlQuotaRefresh(chatId, token, Number(attempt) + 1);
663
693
  return;
664
694
  }
665
- const inProgressText = ["Generation in progress.", quotaLine].join("\n");
695
+ const inProgressText = ["Generation in progress.", quota.line].join("\n");
666
696
  try {
667
697
  await this.bot.api.editMessageText(chatId, current.messageId, inProgressText, {
668
698
  reply_markup: buildTurnControlKeyboard(token),
@@ -706,7 +736,11 @@ export class TelegramChannel {
706
736
  }
707
737
  }
708
738
  async #handleSingleApproval(context, decision) {
709
- const chatId = String(context.chat.id);
739
+ const chatId = getContextChatId(context);
740
+ if (!chatId) {
741
+ await context.reply("Unable to resolve chat.");
742
+ return;
743
+ }
710
744
  if (!this.#isAllowedChat(chatId)) {
711
745
  await context.reply("Chat not allowed for this bot.");
712
746
  return;
@@ -716,7 +750,7 @@ export class TelegramChannel {
716
750
  channelId: this.id,
717
751
  externalUserId: chatId,
718
752
  });
719
- const requestedId = extractFirstCommandArgument(context.message.text);
753
+ const requestedId = extractFirstCommandArgument(getContextMessageText(context));
720
754
  if (requestedId.toLowerCase() === "all") {
721
755
  const summary = await this.#resolveAllApprovals({ threadId, decision });
722
756
  await context.reply(summary);
@@ -744,7 +778,11 @@ export class TelegramChannel {
744
778
  await context.reply(`Approved '${target.id}'.`);
745
779
  }
746
780
  async #handleBulkApproval(context, decision) {
747
- const chatId = String(context.chat.id);
781
+ const chatId = getContextChatId(context);
782
+ if (!chatId) {
783
+ await context.reply("Unable to resolve chat.");
784
+ return;
785
+ }
748
786
  if (!this.#isAllowedChat(chatId)) {
749
787
  await context.reply("Chat not allowed for this bot.");
750
788
  return;
@@ -760,7 +798,7 @@ export class TelegramChannel {
760
798
  });
761
799
  await context.reply(summary);
762
800
  }
763
- async #resolveAllApprovals({ threadId, decision }) {
801
+ async #resolveAllApprovals({ threadId, decision, }) {
764
802
  const approvals = await this.runtime.listPendingApprovals(threadId);
765
803
  if (approvals.length === 0) {
766
804
  return "No pending approvals.";
@@ -798,6 +836,15 @@ export class TelegramChannel {
798
836
  return set.has(String(chatId));
799
837
  }
800
838
  }
839
+ function toRuntimeLike(runtime) {
840
+ return runtime;
841
+ }
842
+ function getContextChatId(context) {
843
+ return String(context.chat?.id ?? "").trim();
844
+ }
845
+ function getContextMessageText(context) {
846
+ return String(context.message?.text ?? "").trim();
847
+ }
801
848
  async function sendChunkedMessage(botApi, chatId, text) {
802
849
  const max = 3900;
803
850
  for (let start = 0; start < text.length; start += max) {
@@ -832,7 +879,7 @@ function selectApproval(approvals, requestedId) {
832
879
  return approvals.find((approval) => approval.id === wantedId) ?? null;
833
880
  }
834
881
  if (approvals.length === 1) {
835
- return approvals[0];
882
+ return approvals[0] ?? null;
836
883
  }
837
884
  return null;
838
885
  }
@@ -889,12 +936,12 @@ function isTurnInterruptedError(message) {
889
936
  normalized.includes("turn interrupted by user") ||
890
937
  normalized.includes("process stopped while waiting for turn completion"));
891
938
  }
892
- async function buildTurnFailureMessage({ runtime, safeError }) {
939
+ async function buildTurnFailureMessage({ runtime, safeError, }) {
893
940
  if (isQuotaLimitError(safeError)) {
894
- const quotaLine = await resolveCodexQuotaLine(runtime);
941
+ const quota = await resolveCodexQuotaLine(runtime);
895
942
  const base = "Execution paused: Codex quota limit reached for this account.";
896
- if (quotaLine) {
897
- return `${base}\n${quotaLine}`;
943
+ if (quota.line) {
944
+ return `${base}\n${quota.line}`;
898
945
  }
899
946
  return `${base}\nPlease retry after the quota reset window.`;
900
947
  }
@@ -1012,52 +1059,141 @@ function parseTurnControlCallbackData(raw) {
1012
1059
  if (!match) {
1013
1060
  return null;
1014
1061
  }
1062
+ const action = match[1];
1063
+ const token = match[2];
1064
+ if (action !== "stop" || !token) {
1065
+ return null;
1066
+ }
1015
1067
  return {
1016
- action: match[1],
1017
- token: match[2],
1068
+ action,
1069
+ token,
1018
1070
  };
1019
1071
  }
1020
1072
  async function resolveCodexQuotaLine(runtime) {
1021
1073
  if (!runtime || typeof runtime.getProviderUsage !== "function") {
1022
- return "";
1074
+ return resolveCodexQuotaLineFromSnapshot(null);
1023
1075
  }
1024
1076
  try {
1025
1077
  const snapshot = await runtime.getProviderUsage();
1026
- return formatCodexQuotaLine(snapshot);
1078
+ const first = resolveCodexQuotaLineFromSnapshot(snapshot);
1079
+ if (first.hasQuotaWindows) {
1080
+ return first;
1081
+ }
1082
+ const fallbackSnapshot = await fetchCodexQuotaSnapshotFromAuth();
1083
+ if (!fallbackSnapshot) {
1084
+ return first;
1085
+ }
1086
+ const merged = snapshot && typeof snapshot === "object"
1087
+ ? {
1088
+ ...snapshot,
1089
+ ...fallbackSnapshot,
1090
+ }
1091
+ : fallbackSnapshot;
1092
+ return resolveCodexQuotaLineFromSnapshot(merged);
1027
1093
  }
1028
1094
  catch {
1029
- return "";
1095
+ return resolveCodexQuotaLineFromSnapshot(null);
1030
1096
  }
1031
1097
  }
1032
- function formatCodexQuotaLine(snapshot) {
1033
- const windows = [
1034
- formatQuotaWindow("5h", snapshot?.primary),
1035
- formatQuotaWindow("weekly", snapshot?.secondary),
1036
- ].filter(Boolean);
1037
- if (windows.length === 0) {
1038
- return "";
1039
- }
1040
- return `Codex quota: ${windows.join(" | ")}`;
1098
+ function resolveCodexQuotaLineFromSnapshot(snapshot) {
1099
+ const typed = snapshot;
1100
+ return {
1101
+ line: formatCodexQuotaLine(typed),
1102
+ hasQuotaWindows: hasCodexQuotaWindows(typed),
1103
+ };
1041
1104
  }
1042
- function formatQuotaWindow(label, windowSnapshot) {
1043
- const remaining = Number(windowSnapshot?.remainingPercent);
1044
- if (!Number.isFinite(remaining)) {
1045
- return "";
1105
+ async function fetchCodexQuotaSnapshotFromAuth() {
1106
+ const now = Date.now();
1107
+ if (cachedCodexUsage && now < cachedCodexUsage.expiresAt) {
1108
+ return cachedCodexUsage.snapshot;
1109
+ }
1110
+ const auth = await readCodexAuthTokens();
1111
+ if (!auth?.accessToken) {
1112
+ return null;
1113
+ }
1114
+ const controller = new AbortController();
1115
+ const timeout = setTimeout(() => {
1116
+ controller.abort();
1117
+ }, 3500);
1118
+ try {
1119
+ const headers = {
1120
+ Authorization: `Bearer ${auth.accessToken}`,
1121
+ Accept: "application/json",
1122
+ "User-Agent": "CopilotHub",
1123
+ };
1124
+ if (auth.accountId) {
1125
+ headers["ChatGPT-Account-Id"] = auth.accountId;
1126
+ }
1127
+ const response = await fetch("https://chatgpt.com/backend-api/wham/usage", {
1128
+ method: "GET",
1129
+ headers,
1130
+ signal: controller.signal,
1131
+ });
1132
+ if (!response.ok) {
1133
+ return null;
1134
+ }
1135
+ const payload = (await response.json());
1136
+ const primary = normalizeWhamWindow(payload?.rate_limit?.primary_window);
1137
+ const secondary = normalizeWhamWindow(payload?.rate_limit?.secondary_window);
1138
+ const hasData = Number.isFinite(Number(primary?.remainingPercent)) ||
1139
+ Number.isFinite(Number(secondary?.remainingPercent));
1140
+ if (!hasData) {
1141
+ return null;
1142
+ }
1143
+ const snapshot = { primary, secondary };
1144
+ cachedCodexUsage = {
1145
+ expiresAt: now + CODEX_USAGE_CACHE_TTL_MS,
1146
+ snapshot,
1147
+ };
1148
+ return snapshot;
1149
+ }
1150
+ catch {
1151
+ return null;
1152
+ }
1153
+ finally {
1154
+ clearTimeout(timeout);
1046
1155
  }
1047
- const resetAt = Number(windowSnapshot?.resetsAt);
1048
- const resetLabel = Number.isFinite(resetAt) ? `, reset ${formatEpochSeconds(resetAt)}` : "";
1049
- return `${label} ${Math.round(clampPercent(remaining))}%${resetLabel}`;
1050
1156
  }
1051
- function formatEpochSeconds(value) {
1052
- const seconds = Number(value);
1053
- if (!Number.isFinite(seconds) || seconds <= 0) {
1054
- return "";
1157
+ function normalizeWhamWindow(window) {
1158
+ const used = Number(window?.used_percent);
1159
+ const remainingDirect = Number(window?.remaining_percent);
1160
+ let remaining = null;
1161
+ if (Number.isFinite(remainingDirect)) {
1162
+ remaining = clampPercent(remainingDirect);
1163
+ }
1164
+ else if (Number.isFinite(used)) {
1165
+ remaining = clampPercent(100 - used);
1166
+ }
1167
+ const resetSeconds = Number(window?.reset_at ?? window?.resets_at);
1168
+ const resetsAt = Number.isFinite(resetSeconds) ? resetSeconds : null;
1169
+ return {
1170
+ remainingPercent: remaining,
1171
+ resetsAt,
1172
+ };
1173
+ }
1174
+ async function readCodexAuthTokens() {
1175
+ const codexHome = resolveCodexHomeDir();
1176
+ const authPath = path.join(codexHome, "auth.json");
1177
+ try {
1178
+ const raw = await fs.readFile(authPath, "utf8");
1179
+ const parsed = JSON.parse(raw);
1180
+ const accessToken = String(parsed?.tokens?.access_token ?? "").trim();
1181
+ if (!accessToken) {
1182
+ return null;
1183
+ }
1184
+ const accountId = String(parsed?.tokens?.account_id ?? "").trim() || null;
1185
+ return { accessToken, accountId };
1055
1186
  }
1056
- const date = new Date(seconds * 1000);
1057
- if (!Number.isFinite(date.getTime())) {
1058
- return "";
1187
+ catch {
1188
+ return null;
1189
+ }
1190
+ }
1191
+ function resolveCodexHomeDir() {
1192
+ const fromEnv = String(process.env.CODEX_HOME_DIR ?? process.env.CODEX_HOME ?? "").trim();
1193
+ if (fromEnv) {
1194
+ return path.resolve(fromEnv);
1059
1195
  }
1060
- return date.toISOString().replace(".000Z", "Z");
1196
+ return path.join(os.homedir(), ".codex");
1061
1197
  }
1062
1198
  function clampPercent(value) {
1063
1199
  const n = Number(value);