@zergai/ztc 0.2.6-dev.36101352252 → 0.2.7-dev.36236183737

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 (112) hide show
  1. package/README.md +43 -17
  2. package/bin/zmux-darwin-arm64 +0 -0
  3. package/bin/ztc-tui-darwin-arm64 +0 -0
  4. package/bin/zterm-darwin-arm64 +0 -0
  5. package/dist/App.js +19 -5
  6. package/dist/agent/agent.d.ts +5 -1
  7. package/dist/agent/agent.js +51 -8
  8. package/dist/agent/backends/account_pool.d.ts +5 -6
  9. package/dist/agent/backends/account_pool.js +39 -14
  10. package/dist/agent/backends/account_rotation.d.ts +3 -0
  11. package/dist/agent/backends/account_rotation.js +14 -3
  12. package/dist/agent/backends/codex_account_status.d.ts +42 -0
  13. package/dist/agent/backends/codex_account_status.js +196 -0
  14. package/dist/agent/backends/codex_app_server.d.ts +12 -2
  15. package/dist/agent/backends/codex_app_server.js +66 -6
  16. package/dist/agent/backends/index.d.ts +2 -0
  17. package/dist/agent/backends/index.js +13 -5
  18. package/dist/agent/backends/openai.d.ts +2 -2
  19. package/dist/agent/backends/openai.js +4 -3
  20. package/dist/agent/backends/openai_compatible.js +10 -10
  21. package/dist/agent/backends/openai_responses.d.ts +17 -5
  22. package/dist/agent/backends/openai_responses.js +2 -4
  23. package/dist/agent/backends/token_usage.js +1 -1
  24. package/dist/agent/backends/types.d.ts +3 -0
  25. package/dist/agent/commands/accounts.d.ts +7 -1
  26. package/dist/agent/commands/accounts.js +299 -101
  27. package/dist/agent/commands/config.js +11 -3
  28. package/dist/agent/commands/config_profile_switch.d.ts +2 -0
  29. package/dist/agent/commands/config_profile_switch.js +22 -0
  30. package/dist/agent/commands/context.js +4 -3
  31. package/dist/agent/commands/index.js +1 -2
  32. package/dist/agent/commands/input_mode.js +1 -1
  33. package/dist/agent/commands/model.js +7 -2
  34. package/dist/agent/commands/plan.d.ts +12 -0
  35. package/dist/agent/commands/plan.js +39 -28
  36. package/dist/agent/commands/profile.d.ts +1 -1
  37. package/dist/agent/commands/profile.js +34 -257
  38. package/dist/agent/commands/provider_choices.d.ts +12 -1
  39. package/dist/agent/commands/provider_choices.js +29 -2
  40. package/dist/agent/commands/provider_selection.d.ts +3 -0
  41. package/dist/agent/commands/provider_selection.js +63 -0
  42. package/dist/agent/commands/settings.d.ts +2 -0
  43. package/dist/agent/commands/settings.js +655 -50
  44. package/dist/agent/commands/types.d.ts +19 -2
  45. package/dist/agent/tools/ask_user.js +2 -2
  46. package/dist/app/agent_runner.d.ts +1 -1
  47. package/dist/app/agent_runner.js +2 -2
  48. package/dist/app/ask_user_response.d.ts +3 -0
  49. package/dist/app/ask_user_response.js +16 -0
  50. package/dist/app/command_context.js +29 -13
  51. package/dist/app/controller.d.ts +17 -2
  52. package/dist/app/controller.js +24 -1
  53. package/dist/app/selection_completion.d.ts +9 -3
  54. package/dist/app/selection_completion.js +54 -22
  55. package/dist/cli_args.js +10 -1
  56. package/dist/cli_main.js +85 -2
  57. package/dist/cli_ratatui.d.ts +22 -4
  58. package/dist/cli_ratatui.js +421 -146
  59. package/dist/config/defaults.d.ts +6 -0
  60. package/dist/config/defaults.js +20 -0
  61. package/dist/config/profiles.d.ts +9 -0
  62. package/dist/config/profiles.js +87 -0
  63. package/dist/config/types.d.ts +2 -0
  64. package/dist/config.d.ts +7 -0
  65. package/dist/config.js +283 -40
  66. package/dist/headless.js +14 -0
  67. package/dist/panes/model_catalog.d.ts +1 -1
  68. package/dist/panes/model_catalog.js +24 -2
  69. package/dist/panes/ztc_launch.js +6 -0
  70. package/dist/router_auth.js +3 -2
  71. package/dist/session.d.ts +2 -0
  72. package/dist/session.js +3 -0
  73. package/dist/types.d.ts +23 -2
  74. package/dist/ui/views/app.d.ts +3 -1
  75. package/dist/ui/views/app.js +3 -2
  76. package/dist/ui/views/completion_menu.js +6 -6
  77. package/dist/ui/views/input_area.d.ts +2 -1
  78. package/dist/ui/views/input_area.js +9 -8
  79. package/dist/ui/views/status_bar.d.ts +2 -0
  80. package/dist/ui/views/status_bar.js +15 -4
  81. package/dist/ui/widgets/color_panel.d.ts +9 -0
  82. package/dist/ui/widgets/color_panel.js +41 -26
  83. package/dist/ui/widgets/navigator_panel.d.ts +9 -0
  84. package/dist/ui/widgets/navigator_panel.js +17 -16
  85. package/dist/ui/widgets/panel_frame.d.ts +83 -3
  86. package/dist/ui/widgets/panel_frame.js +107 -9
  87. package/dist/ui/widgets/questionnaire.d.ts +9 -0
  88. package/dist/ui/widgets/questionnaire.js +214 -16
  89. package/dist/ui/widgets/resume_panel.d.ts +9 -0
  90. package/dist/ui/widgets/resume_panel.js +18 -17
  91. package/dist/ui/widgets/rewind_panel.d.ts +9 -0
  92. package/dist/ui/widgets/rewind_panel.js +15 -16
  93. package/dist/ui/widgets/select.d.ts +6 -0
  94. package/dist/ui/widgets/select.js +47 -12
  95. package/dist/ui/widgets/settings_panel.d.ts +75 -0
  96. package/dist/ui/widgets/settings_panel.js +197 -39
  97. package/dist/ui/widgets/status_line_panel.d.ts +9 -0
  98. package/dist/ui/widgets/status_line_panel.js +18 -19
  99. package/dist/ui/widgets/transcript_preview.d.ts +8 -0
  100. package/dist/ui/widgets/transcript_preview.js +10 -0
  101. package/dist/utils/context.js +2 -0
  102. package/dist/utils/fireworks.d.ts +20 -0
  103. package/dist/utils/fireworks.js +23 -0
  104. package/dist/utils/models.d.ts +2 -1
  105. package/dist/utils/models.js +19 -0
  106. package/dist/web/accounts_mirror.d.ts +7 -0
  107. package/dist/web/accounts_mirror.js +59 -7
  108. package/dist/web/mirror_stream.js +19 -5
  109. package/dist/zlm-embed.mjs +5734 -5362
  110. package/package.json +2 -2
  111. package/dist/agent/commands/provider.d.ts +0 -2
  112. package/dist/agent/commands/provider.js +0 -75
@@ -0,0 +1,196 @@
1
+ import { CodexAppServerClient, CODEX_SUBSCRIPTION_CONFIG_ARGS, buildCodexAppServerEnv } from './codex_app_server.js';
2
+ /**
3
+ * Minimum spacing between probes of one credential home, even when forced, for
4
+ * the shared mirror cache. A forced read spawns the Codex App Server and
5
+ * refreshes the login's token, so repeated refresh clicks (or a looping client)
6
+ * share one recent probe.
7
+ */
8
+ export const CODEX_STATUS_MIN_FORCED_INTERVAL_MS = 30_000;
9
+ /** Cache official Codex status per credential home so a mirror poll does not spawn a CLI each time. */
10
+ export class CodexStatusCache {
11
+ probe;
12
+ entries = new Map();
13
+ pending = new Map();
14
+ generations = new Map();
15
+ minForcedIntervalMs;
16
+ constructor(probe, options = {}) {
17
+ this.probe = probe;
18
+ this.minForcedIntervalMs = options.minForcedIntervalMs ?? 0;
19
+ }
20
+ async read(home, force = false) {
21
+ const pending = this.pending.get(home);
22
+ if (pending)
23
+ return pending;
24
+ const entry = this.entries.get(home);
25
+ if (entry && Date.now() < entry.nextReadAt
26
+ && (!force || Date.now() - entry.probedAt < this.minForcedIntervalMs)) {
27
+ return entry.status;
28
+ }
29
+ const generation = this.generations.get(home) ?? 0;
30
+ const work = (async () => {
31
+ let status;
32
+ try {
33
+ status = await this.probe(home);
34
+ if (status.state === 'quota_unavailable' && entry?.status.windows.length && status.windows.length === 0) {
35
+ status = { ...status, windows: entry.status.windows,
36
+ ...(entry.status.observedAt !== undefined ? { observedAt: entry.status.observedAt } : {}) };
37
+ }
38
+ }
39
+ catch {
40
+ status = {
41
+ state: 'unavailable',
42
+ windows: entry?.status.windows ?? [],
43
+ ...(entry?.status.observedAt !== undefined ? { observedAt: entry.status.observedAt } : {}),
44
+ ...(entry?.status.email ? { email: entry.status.email } : {}),
45
+ ...(entry?.status.plan ? { plan: entry.status.plan } : {}),
46
+ ...(entry?.status.usage ? { usage: entry.status.usage, usageStale: true } : {}),
47
+ };
48
+ }
49
+ if (status.usageStale && !status.usage && entry?.status.usage) {
50
+ status = { ...status, usage: entry.status.usage };
51
+ }
52
+ if ((this.generations.get(home) ?? 0) === generation) {
53
+ const probedAt = Date.now();
54
+ this.entries.set(home, { status, probedAt, nextReadAt: probedAt
55
+ + (status.state === 'unavailable' || status.state === 'quota_unavailable' ? 15_000 : 5 * 60_000) });
56
+ }
57
+ return status;
58
+ })();
59
+ this.pending.set(home, work);
60
+ try {
61
+ return await work;
62
+ }
63
+ finally {
64
+ if (this.pending.get(home) === work)
65
+ this.pending.delete(home);
66
+ }
67
+ }
68
+ invalidate(home) {
69
+ this.entries.delete(home);
70
+ this.pending.delete(home);
71
+ this.generations.set(home, (this.generations.get(home) ?? 0) + 1);
72
+ }
73
+ }
74
+ function record(value) {
75
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
76
+ ? value : null;
77
+ }
78
+ function projectWindow(name, value, reached) {
79
+ const bucket = record(value);
80
+ if (!bucket)
81
+ return null;
82
+ const used = bucket.usedPercent;
83
+ const remaining = bucket.remainingPercent;
84
+ const percent = reached ? 0 : typeof remaining === 'number' ? remaining
85
+ : typeof used === 'number' ? 100 - used : NaN;
86
+ if (!Number.isFinite(percent) || percent < 0 || percent > 100)
87
+ return null;
88
+ const rawReset = bucket.resetsAt ?? bucket.resetAt;
89
+ const duration = bucket.windowDurationMins;
90
+ const label = typeof duration === 'number' && Number.isSafeInteger(duration) && duration > 0
91
+ ? duration % 1_440 === 0 ? `${duration / 1_440}d`
92
+ : duration % 60 === 0 ? `${duration / 60}h` : `${duration}m`
93
+ : name;
94
+ const resetAt = typeof rawReset === 'number'
95
+ ? (rawReset < 10_000_000_000 ? rawReset * 1000 : rawReset)
96
+ : typeof rawReset === 'string' ? Date.parse(rawReset) : NaN;
97
+ return { name: label, remainingPercent: percent,
98
+ ...(Number.isFinite(resetAt) && resetAt > 0 ? { resetAt } : {}) };
99
+ }
100
+ function projectWindows(value) {
101
+ const root = record(value);
102
+ if (!root)
103
+ return [];
104
+ const main = record(root.rateLimits);
105
+ const byId = record(root.rateLimitsByLimitId);
106
+ const windows = [];
107
+ const addBucket = (bucket, prefix) => {
108
+ const reached = bucket.rateLimitReachedType === 'rate_limit_reached';
109
+ for (const key of ['primary', 'secondary']) {
110
+ if (windows.length >= 12)
111
+ return;
112
+ const window = projectWindow(key, bucket[key], reached);
113
+ if (window)
114
+ windows.push({ ...window,
115
+ name: prefix ? `${prefix} ${window.name}` : window.name });
116
+ }
117
+ };
118
+ const codex = record(byId?.codex);
119
+ if (codex)
120
+ addBucket(codex, '');
121
+ else if (main && (main.limitId === undefined || main.limitId === 'codex'))
122
+ addBucket(main, '');
123
+ if (byId) {
124
+ for (const [id, raw] of Object.entries(byId)) {
125
+ if (id === 'codex' || !/^[A-Za-z0-9_-]{1,40}$/.test(id))
126
+ continue;
127
+ const bucket = record(raw);
128
+ if (!bucket)
129
+ continue;
130
+ const label = typeof bucket.limitName === 'string'
131
+ && bucket.limitName.length > 0 && bucket.limitName.length <= 120
132
+ && !/[\u0000-\u001f\u007f]/.test(bucket.limitName)
133
+ ? bucket.limitName : id;
134
+ addBucket(bucket, label);
135
+ if (windows.length >= 12)
136
+ break;
137
+ }
138
+ }
139
+ return windows;
140
+ }
141
+ function projectUsage(value, observedAt) {
142
+ const root = record(value);
143
+ const summary = record(root?.summary);
144
+ const lifetime = summary?.lifetimeTokens;
145
+ const lifetimeTokens = typeof lifetime === 'number' && Number.isSafeInteger(lifetime) && lifetime >= 0
146
+ ? lifetime : undefined;
147
+ const today = new Date(observedAt).toISOString().slice(0, 10);
148
+ let todayTokens;
149
+ if (Array.isArray(root?.dailyUsageBuckets)) {
150
+ for (const value of root.dailyUsageBuckets) {
151
+ const bucket = record(value);
152
+ const tokens = bucket?.tokens;
153
+ if (bucket?.startDate === today && typeof tokens === 'number'
154
+ && Number.isSafeInteger(tokens) && tokens >= 0) {
155
+ todayTokens = Math.max(todayTokens ?? 0, tokens);
156
+ }
157
+ }
158
+ }
159
+ return lifetimeTokens === undefined && todayTokens === undefined ? undefined
160
+ : { ...(lifetimeTokens === undefined ? {} : { lifetimeTokens }),
161
+ ...(todayTokens === undefined ? {} : { todayTokens }), observedAt };
162
+ }
163
+ /** Read identity and quota through the official Codex App Server under one isolated CODEX_HOME. */
164
+ export async function inspectCodexAccount(home) {
165
+ const client = new CodexAppServerClient({
166
+ args: [...CODEX_SUBSCRIPTION_CONFIG_ARGS, 'app-server'],
167
+ env: buildCodexAppServerEnv(home), requestTimeoutMs: 6_000,
168
+ });
169
+ try {
170
+ const result = await client.readAccount(true);
171
+ const account = record(result.account);
172
+ if (!account || account.type !== 'chatgpt' || result.requiresOpenaiAuth !== true) {
173
+ return { state: 'reauth_required', windows: [] };
174
+ }
175
+ const [limitsResult, usageResult] = await Promise.allSettled([
176
+ client.readRateLimits(), client.readUsage(),
177
+ ]);
178
+ const observedAt = Date.now();
179
+ const quotaAvailable = limitsResult.status === 'fulfilled';
180
+ const usageAvailable = usageResult.status === 'fulfilled';
181
+ return {
182
+ state: quotaAvailable ? 'ready' : 'quota_unavailable',
183
+ windows: projectWindows(quotaAvailable ? limitsResult.value : null),
184
+ ...(quotaAvailable ? { observedAt } : {}),
185
+ ...(usageAvailable ? { usage: projectUsage(usageResult.value, observedAt) } : { usageStale: true }),
186
+ ...(typeof account.email === 'string' ? { email: account.email } : {}),
187
+ ...(typeof account.planType === 'string' ? { plan: account.planType } : {}),
188
+ };
189
+ }
190
+ finally {
191
+ client.close();
192
+ }
193
+ }
194
+ export const codexStatusCache = new CodexStatusCache(inspectCodexAccount, {
195
+ minForcedIntervalMs: CODEX_STATUS_MIN_FORCED_INTERVAL_MS,
196
+ });
@@ -1,5 +1,5 @@
1
1
  import { RateLimitError } from './rate_limit.js';
2
- import type { ToolDefinition } from '../../types.js';
2
+ import type { Message, ToolDefinition } from '../../types.js';
3
3
  import type { AgentBackend, BackendObservation, BackendRequest, BackendResponse, ClientToolCallResult, LlmMessage, StreamEvent } from './types.js';
4
4
  type JsonObject = Record<string, unknown>;
5
5
  export interface CodexAppServerTransport {
@@ -9,6 +9,8 @@ export interface CodexAppServerTransport {
9
9
  }
10
10
  export declare const MAX_CODEX_APP_SERVER_FRAME_BYTES = 1048576;
11
11
  export declare const MAX_CODEX_APP_SERVER_QUEUED_FRAMES = 1024;
12
+ /** Keep managed profiles on their own auth.json and ChatGPT subscription. */
13
+ export declare const CODEX_SUBSCRIPTION_CONFIG_ARGS: readonly ["-c", "cli_auth_credentials_store=\"file\"", "-c", "forced_login_method=\"chatgpt\""];
12
14
  /** Codex App Server `SandboxMode` for model-executed commands and edits. */
13
15
  export type CodexSandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';
14
16
  export interface CodexAppServerClientOptions {
@@ -29,6 +31,8 @@ export interface CodexAppServerClientOptions {
29
31
  * Ignored when a `transport` is supplied (tests/fakes own their own env).
30
32
  */
31
33
  env?: NodeJS.ProcessEnv;
34
+ /** Require the selected pooled profile to use ChatGPT subscription auth. */
35
+ requireChatGptSubscription?: boolean;
32
36
  /**
33
37
  * Sandbox for the threads ZTC starts. Defaults to `workspace-write`: an
34
38
  * omitted `sandbox` makes the App Server fall back to read-only, and since
@@ -44,7 +48,7 @@ export interface CodexAppServerClientOptions {
44
48
  /**
45
49
  * Environment for the spawned `codex app-server`, pinned to the account's own
46
50
  * credentials. Injects the per-account CODEX_HOME (when given) and SCRUBS a
47
- * stray `OPENAI_API_KEY` / `OPENAI_BASE_URL` from the parent env — otherwise the
51
+ * stray auth overrides from the parent env — otherwise the
48
52
  * codex CLI would bill that API key instead of the account's ChatGPT
49
53
  * subscription (auth.json under CODEX_HOME). This is the codex counterpart to
50
54
  * {@link buildClaudeCliEnv}'s ANTHROPIC_API_KEY scrub, and it applies even with
@@ -118,6 +122,7 @@ export declare class CodexAppServerClient {
118
122
  private command;
119
123
  private args;
120
124
  private env?;
125
+ private requireChatGptSubscription;
121
126
  private requestTimeoutMs;
122
127
  private turnTimeoutMs;
123
128
  /** Sandbox requested on thread/start. Readable so the factory's plan-mode
@@ -142,6 +147,8 @@ export declare class CodexAppServerClient {
142
147
  */
143
148
  private threadState;
144
149
  constructor(options?: CodexAppServerClientOptions);
150
+ /** Submit text to the active Codex turn; return false so the TUI can retry on a new turn. */
151
+ steerUserMessage(message: Message): Promise<boolean>;
145
152
  initialize(onActivity?: () => void): Promise<void>;
146
153
  listModels(): Promise<string[]>;
147
154
  streamTurn(options: CodexTurnOptions): AsyncIterable<StreamEvent>;
@@ -156,6 +163,8 @@ export declare class CodexAppServerClient {
156
163
  account: unknown;
157
164
  requiresOpenaiAuth: boolean;
158
165
  }>;
166
+ readRateLimits(): Promise<unknown>;
167
+ readUsage(): Promise<unknown>;
159
168
  private ensureTransport;
160
169
  private request;
161
170
  private requireAccount;
@@ -209,6 +218,7 @@ export declare class CodexAppServerBackend implements AgentBackend {
209
218
  /** The sandbox this backend's thread runs under. Observable so the factory's
210
219
  * plan-mode read-only downgrade can be asserted. */
211
220
  get sandbox(): CodexSandboxMode;
221
+ steerUserMessage(message: Message): Promise<boolean>;
212
222
  generate(request: BackendRequest): Promise<BackendResponse>;
213
223
  generateStream(request: BackendRequest): AsyncIterable<StreamEvent>;
214
224
  close(): void;
@@ -4,6 +4,10 @@ import { RateLimitError, parseResetAt } from './rate_limit.js';
4
4
  import { CODEX_APP_SERVER_PROVIDER } from './provider_ids.js';
5
5
  export const MAX_CODEX_APP_SERVER_FRAME_BYTES = 1_048_576;
6
6
  export const MAX_CODEX_APP_SERVER_QUEUED_FRAMES = 1_024;
7
+ /** Keep managed profiles on their own auth.json and ChatGPT subscription. */
8
+ export const CODEX_SUBSCRIPTION_CONFIG_ARGS = [
9
+ '-c', 'cli_auth_credentials_store="file"', '-c', 'forced_login_method="chatgpt"',
10
+ ];
7
11
  const MAX_CODEX_APP_SERVER_STDOUT_CHUNK_BYTES = 4 * MAX_CODEX_APP_SERVER_FRAME_BYTES;
8
12
  const DEFAULT_CODEX_APP_SERVER_REQUEST_TIMEOUT_MS = 30_000;
9
13
  const MAX_CODEX_APP_SERVER_REQUEST_TIMEOUT_MS = 120_000;
@@ -15,7 +19,7 @@ const MAX_CODEX_APP_SERVER_MODELS = 2_000;
15
19
  /**
16
20
  * Environment for the spawned `codex app-server`, pinned to the account's own
17
21
  * credentials. Injects the per-account CODEX_HOME (when given) and SCRUBS a
18
- * stray `OPENAI_API_KEY` / `OPENAI_BASE_URL` from the parent env — otherwise the
22
+ * stray auth overrides from the parent env — otherwise the
19
23
  * codex CLI would bill that API key instead of the account's ChatGPT
20
24
  * subscription (auth.json under CODEX_HOME). This is the codex counterpart to
21
25
  * {@link buildClaudeCliEnv}'s ANTHROPIC_API_KEY scrub, and it applies even with
@@ -25,8 +29,11 @@ export function buildCodexAppServerEnv(codexHome, base = process.env) {
25
29
  const env = { ...base };
26
30
  if (codexHome)
27
31
  env.CODEX_HOME = codexHome;
28
- delete env.OPENAI_API_KEY;
29
- delete env.OPENAI_BASE_URL;
32
+ for (const name of [
33
+ 'OPENAI_API_KEY', 'OPENAI_BASE_URL', 'CODEX_ACCESS_TOKEN', 'CODEX_API_KEY',
34
+ 'OPENAI_IDENTITY_TOKEN_FILE', 'OPENAI_FEDERATION_RULE_ID', 'CODEX_SQLITE_HOME',
35
+ ])
36
+ delete env[name];
30
37
  return env;
31
38
  }
32
39
  /** Stable sha1 hex digest of a string — used to fingerprint reusable thread state. */
@@ -348,6 +355,7 @@ export class CodexAppServerClient {
348
355
  command;
349
356
  args;
350
357
  env;
358
+ requireChatGptSubscription;
351
359
  requestTimeoutMs;
352
360
  turnTimeoutMs;
353
361
  /** Sandbox requested on thread/start. Readable so the factory's plan-mode
@@ -377,10 +385,39 @@ export class CodexAppServerClient {
377
385
  this.command = options.command || process.env.ZTC_CODEX_APP_SERVER_COMMAND || 'codex';
378
386
  this.args = options.args || ['app-server'];
379
387
  this.env = options.env;
388
+ this.requireChatGptSubscription = options.requireChatGptSubscription ?? false;
380
389
  this.sandbox = options.sandbox || 'workspace-write';
381
390
  this.requestTimeoutMs = resolveRequestTimeoutMs(options.requestTimeoutMs);
382
391
  this.turnTimeoutMs = resolveTurnTimeoutMs(options.turnTimeoutMs);
383
392
  }
393
+ /** Submit text to the active Codex turn; return false so the TUI can retry on a new turn. */
394
+ async steerUserMessage(message) {
395
+ // The app-server wire input below is text-only. Keep attachments on ZTC's
396
+ // ordinary next-turn path until image steering has an exact conversion.
397
+ if (message.attachments?.length)
398
+ return false;
399
+ const active = this.turnWaiters.filter(waiter => waiter.turnId && !waiter.terminalReceived);
400
+ if (active.length !== 1)
401
+ return false;
402
+ const waiter = active[0];
403
+ try {
404
+ const reply = await this.request('turn/steer', {
405
+ threadId: waiter.threadId,
406
+ expectedTurnId: waiter.turnId,
407
+ input: [{ type: 'text', text: message.content, text_elements: [] }],
408
+ clientUserMessageId: message.id,
409
+ });
410
+ if (reply.turnId !== waiter.turnId)
411
+ return false;
412
+ if (this.threadState?.threadId === waiter.threadId) {
413
+ this.threadState.acceptedSteering.push(message.content);
414
+ }
415
+ return true;
416
+ }
417
+ catch {
418
+ return false;
419
+ }
420
+ }
384
421
  async initialize(onActivity) {
385
422
  if (this.initialized) {
386
423
  return;
@@ -507,10 +544,14 @@ export class CodexAppServerClient {
507
544
  // cached thread is the wrong shape and must not be reused.
508
545
  const signature = this.threadSignature(options, dynamicToolsPayload);
509
546
  const cached = this.threadState;
547
+ const gap = cached ? options.messages.slice(cached.knownCount) : [];
548
+ const userGap = gap.filter(message => message.role === 'user');
510
549
  const canReuse = Boolean(cached
511
550
  && cached.signature === signature
512
551
  && options.messages.length > cached.knownCount
513
- && sha1(JSON.stringify(options.messages.slice(0, cached.knownCount))) === cached.prefixHash);
552
+ && sha1(JSON.stringify(options.messages.slice(0, cached.knownCount))) === cached.prefixHash
553
+ && userGap.length > cached.acceptedSteering.length
554
+ && cached.acceptedSteering.every((text, index) => this.flattenMessageContent(userGap[index]) === text));
514
555
  let threadId;
515
556
  let toolsActive;
516
557
  let input;
@@ -522,9 +563,8 @@ export class CodexAppServerClient {
522
563
  // Send only the user messages added since the thread's last turn, as bare
523
564
  // text. Assistant messages in the gap are the thread's OWN replies — it
524
565
  // already holds them, so they're dropped from the input.
525
- const gap = options.messages.slice(cached.knownCount);
526
566
  input = [
527
- { type: 'text', text: gap.filter((m) => m.role === 'user').map((m) => this.flattenMessageContent(m)).join('\n\n'), text_elements: [] },
567
+ { type: 'text', text: userGap.slice(cached.acceptedSteering.length).map((m) => this.flattenMessageContent(m)).join('\n\n'), text_elements: [] },
528
568
  ...this.collectLocalImages(gap),
529
569
  ];
530
570
  }
@@ -617,6 +657,7 @@ export class CodexAppServerClient {
617
657
  knownCount: options.messages.length,
618
658
  prefixHash: sha1(JSON.stringify(options.messages)),
619
659
  signature: this.threadSignature(options, toolsActive ? dynamicToolsPayload : null),
660
+ acceptedSteering: [],
620
661
  };
621
662
  if (options.signal?.aborted) {
622
663
  abort();
@@ -710,6 +751,14 @@ export class CodexAppServerClient {
710
751
  await this.initialize(onActivity);
711
752
  return await this.request('account/read', { refreshToken }, onActivity);
712
753
  }
754
+ async readRateLimits() {
755
+ await this.initialize();
756
+ return await this.request('account/rateLimits/read', {});
757
+ }
758
+ async readUsage() {
759
+ await this.initialize();
760
+ return await this.request('account/usage/read', {});
761
+ }
713
762
  ensureTransport() {
714
763
  if (!this.transport) {
715
764
  this.transport = this.transportFactory
@@ -770,6 +819,10 @@ export class CodexAppServerClient {
770
819
  if (!result.account) {
771
820
  throw new Error("codex_app_server is not logged in. Run 'codex login status' or 'codex login' before using ZTC's official Codex provider.");
772
821
  }
822
+ if (this.requireChatGptSubscription
823
+ && (result.account.type !== 'chatgpt' || result.requiresOpenaiAuth !== true)) {
824
+ throw new Error('codex_app_server requires a ChatGPT subscription login; an API key or alternate provider is active. Run codex login for this account.');
825
+ }
773
826
  }
774
827
  sendNotification(method) {
775
828
  this.ensureTransport();
@@ -918,6 +971,10 @@ export class CodexAppServerClient {
918
971
  inputTokens,
919
972
  outputTokens,
920
973
  totalTokens,
974
+ ...(typeof last.cachedInputTokens === 'number'
975
+ && Number.isSafeInteger(last.cachedInputTokens)
976
+ && last.cachedInputTokens >= 0
977
+ ? { cachedInputTokens: last.cachedInputTokens } : {}),
921
978
  estimated: false,
922
979
  ...(typeof contextWindow === 'number' && contextWindow > 0 ? { contextWindow } : {}),
923
980
  };
@@ -1257,6 +1314,9 @@ export class CodexAppServerBackend {
1257
1314
  get sandbox() {
1258
1315
  return this.client.sandbox;
1259
1316
  }
1317
+ async steerUserMessage(message) {
1318
+ return this.client.steerUserMessage(message);
1319
+ }
1260
1320
  async generate(request) {
1261
1321
  const content = [];
1262
1322
  let stopReason = 'completed';
@@ -17,6 +17,7 @@ export { ClaudeCliBackend, ClaudeCliError, CLAUDE_CODE_AUTH_SENTINEL, hasUsableC
17
17
  export type { AgentBackend, BackendObservation, BackendRequest, BackendResponse, ContentBlock, TextBlock, ToolUseBlock, LlmMessage, RequestContentBlock, RequestVideoBlock, ToolResultBlock, TokenUsage, StreamEvent } from './types.js';
18
18
  import type { AgentBackend, BackendRequest, BackendResponse, StreamEvent } from './types.js';
19
19
  import { AccountPool, type RotationMode } from './account_rotation.js';
20
+ import type { Message } from '../../types.js';
20
21
  export interface CreateBackendOptions {
21
22
  apiKey: string;
22
23
  baseUrl?: string;
@@ -58,6 +59,7 @@ export declare class TracedBackend implements AgentBackend {
58
59
  private readonly provider;
59
60
  private readonly backend;
60
61
  constructor(provider: string, backend: AgentBackend);
62
+ steerUserMessage(message: Message): Promise<boolean>;
61
63
  /** Forward disposal so a process-backed inner (RotatingBackend, codex child)
62
64
  * is closed when the agent is. Without this, tracing (ZTC_BACKEND_TRACE=1)
63
65
  * leaks the child on every agent swap/dispose. */
@@ -21,7 +21,7 @@ import { OpenAICompatibleBackend } from './openai_compatible.js';
21
21
  import { InceptionBackend } from './inception.js';
22
22
  import { GeminiBackend } from './gemini.js';
23
23
  import { KimiBackend } from './kimi.js';
24
- import { CodexAppServerBackend, buildCodexAppServerEnv } from './codex_app_server.js';
24
+ import { CodexAppServerBackend, CODEX_SUBSCRIPTION_CONFIG_ARGS, buildCodexAppServerEnv } from './codex_app_server.js';
25
25
  import { CodexAuthManager, CodexAuthBackend } from './codex_auth.js';
26
26
  import { ClarifaiBackend } from './clarifai.js';
27
27
  import { ClaudeCodeAuthBackend, ClaudeCodeAuthManager } from './claude_code_auth.js';
@@ -38,6 +38,9 @@ export class TracedBackend {
38
38
  this.provider = provider;
39
39
  this.backend = backend;
40
40
  }
41
+ async steerUserMessage(message) {
42
+ return this.backend.steerUserMessage?.(message) ?? false;
43
+ }
41
44
  /** Forward disposal so a process-backed inner (RotatingBackend, codex child)
42
45
  * is closed when the agent is. Without this, tracing (ZTC_BACKEND_TRACE=1)
43
46
  * leaks the child on every agent swap/dispose. */
@@ -98,14 +101,16 @@ function traced(provider, backend) {
98
101
  * Replaces duplicated switch statements in sim/llm.ts, agent/subagent.ts, optimize/llm.ts, etc.
99
102
  */
100
103
  export function createBackendForProvider(provider, opts) {
101
- // Auto-rotate: when the caller opted in and >1 account is REGISTERED for the
102
- // provider, wrap the concrete backend so a 429 cools the active account and
104
+ // Auto-rotate non-Codex providers only: when the caller opted in and >1
105
+ // account is REGISTERED, wrap the concrete backend so a 429 cools the active account and
103
106
  // fails over. Gate on registered (not ready) count: with one ready account and
104
107
  // the config-active pointer on a different cooling/logged-out one, a ready-only
105
108
  // gate would build directly on the stale active and hard-fail the first turn —
106
109
  // the wrapper's selectActive is what moves off it. The inner build recurses
107
110
  // with rotation cleared + the chosen account's source.
108
- if (opts.rotation && opts.rotationPool && opts.rotationPool.accountsFor(provider).length > 1) {
111
+ if (normalizeProviderId(provider) !== CODEX_APP_SERVER_PROVIDER
112
+ && normalizeProviderId(provider) !== CODEX_LEGACY_PROVIDER
113
+ && opts.rotation && opts.rotationPool && opts.rotationPool.accountsFor(provider).length > 1) {
109
114
  const pool = opts.rotationPool;
110
115
  return traced(provider, new RotatingBackend({
111
116
  provider,
@@ -152,12 +157,15 @@ export function createBackendForProvider(provider, opts) {
152
157
  case CODEX_APP_SERVER_PROVIDER:
153
158
  return traced(provider, new CodexAppServerBackend(undefined, {
154
159
  command: opts.codexAppServerCommand,
155
- args: opts.codexAppServerArgs,
160
+ args: opts.codexHome
161
+ ? [...CODEX_SUBSCRIPTION_CONFIG_ARGS, ...(opts.codexAppServerArgs ?? ['app-server'])]
162
+ : opts.codexAppServerArgs,
156
163
  sandbox: opts.codexAppServerSandbox,
157
164
  // Point the spawned app-server at this account's codex home AND scrub a
158
165
  // stray OPENAI_API_KEY so it uses the account's ChatGPT subscription, not
159
166
  // an API key (mirrors the claude_code ANTHROPIC_API_KEY scrub).
160
167
  env: buildCodexAppServerEnv(opts.codexHome),
168
+ requireChatGptSubscription: opts.codexHome !== undefined,
161
169
  }));
162
170
  case CODEX_LEGACY_PROVIDER: {
163
171
  // Per-account: read this account's auth.json (default ~/.codex when unset).
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * OpenAI backend — smart router between Chat Completions and Responses API.
3
3
  *
4
- * Models that require the Responses API (codex models,
5
- * gpt-5.x-codex-*, etc.) are automatically routed there. Everything else goes through Chat Completions.
4
+ * Codex models and GPT-6 Sol/Luna use Responses so reasoning and tool calls
5
+ * can be combined. Everything else goes through Chat Completions.
6
6
  */
7
7
  import { AgentBackend, BackendRequest, BackendResponse, StreamEvent } from './types.js';
8
8
  interface OpenAIConfig {
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * OpenAI backend — smart router between Chat Completions and Responses API.
3
3
  *
4
- * Models that require the Responses API (codex models,
5
- * gpt-5.x-codex-*, etc.) are automatically routed there. Everything else goes through Chat Completions.
4
+ * Codex models and GPT-6 Sol/Luna use Responses so reasoning and tool calls
5
+ * can be combined. Everything else goes through Chat Completions.
6
6
  */
7
7
  import { OpenAICompatibleBackend } from './openai_compatible.js';
8
8
  import { OpenAIResponsesBackend } from './openai_responses.js';
9
- /** Models that only work with the Responses API. */
9
+ /** Models routed to the Responses API. */
10
10
  const RESPONSES_API_PATTERNS = [
11
11
  /codex/i, // codex-mini-latest, gpt-5.1-codex-mini, gpt-5.2-codex, etc.
12
+ /^gpt-6-(?:sol|luna)$/i,
12
13
  ];
13
14
  function needsResponsesAPI(model) {
14
15
  return RESPONSES_API_PATTERNS.some(p => p.test(model));
@@ -36,11 +36,11 @@ export class OpenAICompatibleBackend {
36
36
  * and generateStream(). generateStream() adds `stream`/`stream_options`. */
37
37
  async buildCall(request) {
38
38
  const url = `${this.baseUrl}${this.apiPath}`;
39
- // O-series (o1, o3, o4-mini) and GPT-5+ use max_completion_tokens instead of
39
+ // O-series (o1, o3, o4-mini) and GPT-5/6 use max_completion_tokens instead of
40
40
  // max_tokens; Moonshot marks max_tokens "Deprecated, please refer to
41
41
  // max_completion_tokens" for the Kimi models too.
42
42
  const useCompletionTokens = /^o\d/.test(request.model)
43
- || /^gpt-5/.test(request.model)
43
+ || /^gpt-[56]/.test(request.model)
44
44
  || isKimiModel(request.model);
45
45
  // Cap max_tokens for models with lower limits (gpt-4o: 16384)
46
46
  const isGpt4o = request.model.startsWith('gpt-4o');
@@ -49,19 +49,19 @@ export class OpenAICompatibleBackend {
49
49
  const tokenParam = effectiveMaxTokens === undefined ? {} : useCompletionTokens
50
50
  ? { max_completion_tokens: effectiveMaxTokens }
51
51
  : { max_tokens: effectiveMaxTokens };
52
+ const isGpt6Model = /^gpt-6-(?:sol|luna)$/i.test(request.model);
53
+ const reasoningEffort = (request.model === 'gpt-5.6-sol' || isGpt6Model) && request.tools.length > 0
54
+ ? 'none'
55
+ : isGpt6Model ? request.codexEffort : undefined;
52
56
  const body = {
53
57
  ...this.extraBody,
54
58
  ...(this.requestBodyExtras?.(request) ?? {}),
55
59
  model: request.model,
56
60
  ...tokenParam,
57
- // OpenAI's gpt-5.6-sol Chat Completions contract rejects function
58
- // tools while its default reasoning mode is active. The Responses API
59
- // supports both, but this backend intentionally speaks Chat
60
- // Completions, so disable reasoning only for the calls that carry tools.
61
- // Tool-free turns keep the model's normal reasoning default.
62
- ...(request.model === 'gpt-5.6-sol' && request.tools.length > 0
63
- ? { reasoning_effort: 'none' }
64
- : {}),
61
+ // Chat Completions accepts GPT-6 Sol/Luna function tools only with
62
+ // reasoning disabled. The direct OpenAI backend uses Responses instead;
63
+ // keep this rule for explicitly configured compatible endpoints.
64
+ ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
65
65
  messages: buildOpenAIMessages(request),
66
66
  ...(request.tools.length > 0 ? { tools: buildOpenAIToolDefs(request.tools) } : {})
67
67
  };
@@ -52,6 +52,22 @@ interface ResponsesReasoning {
52
52
  summary: unknown[];
53
53
  }
54
54
  type ResponsesOutputItem = ResponsesMessage | ResponsesFunctionCall | ResponsesReasoning;
55
+ interface ResponsesAPIResponse {
56
+ id: string;
57
+ status: string;
58
+ incomplete_details?: {
59
+ reason?: string;
60
+ } | null;
61
+ output: ResponsesOutputItem[];
62
+ usage?: {
63
+ input_tokens?: number;
64
+ output_tokens?: number;
65
+ total_tokens?: number;
66
+ input_tokens_details?: {
67
+ cached_tokens?: number;
68
+ };
69
+ };
70
+ }
55
71
  export declare function isValidResponsesOutputItem(value: unknown): value is Record<string, unknown>;
56
72
  export declare function requireValidResponsesOutputItem(value: unknown): asserts value is Record<string, unknown>;
57
73
  /** A terminal event must carry a valid response shape or follow real output. */
@@ -98,11 +114,7 @@ export declare function parseResponsesOutput(data: {
98
114
  reason?: string;
99
115
  } | null;
100
116
  output: ResponsesOutputItem[];
101
- usage?: {
102
- input_tokens?: number;
103
- output_tokens?: number;
104
- total_tokens?: number;
105
- };
117
+ usage?: ResponsesAPIResponse['usage'];
106
118
  }): BackendResponse;
107
119
  /**
108
120
  * Consume a Responses API SSE response into incremental StreamEvents. Yields
@@ -13,6 +13,7 @@
13
13
  * - Token param → `max_output_tokens`
14
14
  */
15
15
  import { BaseBackend } from './base.js';
16
+ import { parseTokenUsage } from './token_usage.js';
16
17
  function isRecord(value) {
17
18
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
18
19
  }
@@ -252,9 +253,6 @@ export function parseResponsesOutput(data) {
252
253
  }
253
254
  }
254
255
  }
255
- const inputTokens = data.usage?.input_tokens ?? 0;
256
- const outputTokens = data.usage?.output_tokens ?? 0;
257
- const totalTokens = data.usage?.total_tokens ?? (inputTokens + outputTokens);
258
256
  // An incomplete terminal response outranks any partial function call it may
259
257
  // contain. Executing that payload as a normal tool_use would turn truncated
260
258
  // JSON into a side effect; the Agent consumes this stop state fail-closed.
@@ -266,7 +264,7 @@ export function parseResponsesOutput(data) {
266
264
  return {
267
265
  content: blocks,
268
266
  stopReason,
269
- usage: data.usage ? { inputTokens, outputTokens, totalTokens } : undefined,
267
+ usage: data.usage ? parseTokenUsage(data.usage) : undefined,
270
268
  ...(reasoningParts.length > 0 ? { reasoning: reasoningParts.join('\n\n') } : {}),
271
269
  };
272
270
  }
@@ -11,7 +11,7 @@ export function parseTokenUsage(value) {
11
11
  const outputTokens = counter(raw.completion_tokens ?? raw.output_tokens) ?? 0;
12
12
  const usage = { inputTokens, outputTokens, totalTokens: counter(raw.total_tokens) ?? inputTokens + outputTokens };
13
13
  const optional = {
14
- cachedInputTokens: counter(raw.cached_input_tokens ?? record(raw.prompt_tokens_details).cached_tokens ?? raw.prompt_cache_hit_tokens),
14
+ cachedInputTokens: counter(raw.cached_input_tokens ?? record(raw.input_tokens_details).cached_tokens ?? record(raw.prompt_tokens_details).cached_tokens ?? raw.prompt_cache_hit_tokens),
15
15
  cacheWriteInputTokens: counter(raw.cache_creation_input_tokens),
16
16
  reasoningTokens: counter(record(raw.completion_tokens_details).reasoning_tokens ?? raw.reasoning_tokens),
17
17
  };
@@ -1,4 +1,5 @@
1
1
  import { ToolDefinition } from '../../types.js';
2
+ import type { Message } from '../../types.js';
2
3
  export interface RequestTextBlock {
3
4
  type: 'text';
4
5
  text: string;
@@ -257,6 +258,8 @@ export type StreamEvent = {
257
258
  stopReason: string;
258
259
  };
259
260
  export interface AgentBackend {
261
+ /** Deliver user input into a provider-owned active turn when supported. */
262
+ steerUserMessage?(message: Message): Promise<boolean>;
260
263
  /**
261
264
  * True when the backend owns a usable authentication source independently
262
265
  * of AgentConfig.apiKey (for example, a host-provided rotating token).